@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,506 +0,0 @@
1
- /**
2
- * What `sous update` does, from the first registry request to the summary.
3
- *
4
- * Every plan is worked out before anything runs: the registry is read, the
5
- * installs are found and `buildUpdatePlans` decides each one's version. Then
6
- * the plans are taken one at a time, in the order the planner returns them
7
- * (the global installs, then the project's): each prints its facts, asks its
8
- * own question (sous-io/sous#135 rules one confirmation per install), and runs
9
- * when the answer is yes. Asking the second question after the first install
10
- * has run means its answer can take the first result into account.
11
- *
12
- * Nothing here decides whether a question may be asked; the caller says so,
13
- * from `src/lib/interactive.ts`. The registry fetch, the command runner that
14
- * asks the package managers where their global installs are, the executor
15
- * that runs an install and the prompt are all injectable, so a test drives the
16
- * whole flow with no network and no package manager.
17
- */
18
-
19
- import { spawn } from "node:child_process";
20
- import path from "node:path";
21
- import { color } from "@oclif/color";
22
- import { binEntryOf, readPackageJson, realpathOr } from "../project-install.mjs";
23
- import { nonInteractiveError } from "../interactive.js";
24
- import type { CommandRunner } from "../repos/providers/git.js";
25
- import type { FetchLike } from "../repos/providers/http.js";
26
- import { confirmPrompt, type ConfirmPromptResult } from "../../utils/confirm-prompt.js";
27
- import {
28
- blankLine,
29
- blankLines,
30
- displayError,
31
- dryRunNotice,
32
- heading,
33
- indent,
34
- keysHelpTip,
35
- log,
36
- note,
37
- section,
38
- showVariables,
39
- subheading,
40
- type VariableEntry,
41
- } from "../../utils/formatting.js";
42
- import { discoverInstalls } from "./installs.js";
43
- import { buildUpdatePlans, type PlannedCommand, type UpdateNotice, type UpdatePlan } from "./plan.js";
44
- import { fetchPublishedVersions, packageMetadataUrl, registryBaseUrl } from "./registry.js";
45
- import type { VersionRequest } from "./versions.js";
46
- import type { UpdateScope } from "./plan.js";
47
-
48
- /**
49
- * Runs one command with the terminal handed straight to it, so a package
50
- * manager's progress and its errors reach the person as it prints them, and
51
- * resolves with its exit code. Tests replace it with one that records calls.
52
- */
53
- export type InstallExecutor = (
54
- command: PlannedCommand,
55
- options?: { env?: NodeJS.ProcessEnv }
56
- ) => Promise<number>;
57
-
58
- /**
59
- * The real executor. On Windows a bare command name is started through the
60
- * shell, so npm's `.cmd` shims resolve; an absolute path (the Node binary a
61
- * post-update build runs under) is started directly. A command that cannot be
62
- * started at all writes the reason to stderr and resolves with 127.
63
- */
64
- export const spawnInstall: InstallExecutor = (command, options = {}) =>
65
- new Promise<number>((resolve) => {
66
- const child = spawn(command.command, command.args, {
67
- cwd: command.cwd,
68
- stdio: "inherit",
69
- env: options.env ?? process.env,
70
- shell: process.platform === "win32" && !path.isAbsolute(command.command),
71
- });
72
- child.on("error", (error: Error) => {
73
- process.stderr.write(`${error.message}\n`);
74
- resolve(127);
75
- });
76
- child.on("close", (code) => resolve(code ?? 1));
77
- });
78
-
79
- /** Asks one yes-or-no question; the shared prompt, replaceable in a test. */
80
- export type UpdateConfirm = (config: {
81
- message: string;
82
- default: boolean;
83
- hint: string;
84
- }) => Promise<ConfirmPromptResult>;
85
-
86
- /** What an update run needs. */
87
- export type SelfUpdateOptions = {
88
- /** Where the project lookup starts. */
89
- cwd: string;
90
- /** The package root of the running copy. */
91
- ownRoot: string;
92
- request: VersionRequest;
93
- /** Whether prereleases count; undefined means the planner's default, per install. */
94
- prerelease?: boolean;
95
- /** `--global` or `--project`; undefined covers both. */
96
- only?: UpdateScope;
97
- dryRun: boolean;
98
- /** True when `--yes` answered every question. */
99
- yes: boolean;
100
- /** Whether a question may be asked (`src/lib/interactive.ts`). */
101
- interactive: boolean;
102
- /** False under `--no-build`. */
103
- build: boolean;
104
- /**
105
- * The directory a post-update build runs in, for a project root, or
106
- * undefined when that project has no sous config.
107
- */
108
- buildDirFor: (projectRoot: string) => string | undefined;
109
- /** Where the registry override is read from, and the environment installs run with. */
110
- env?: NodeJS.ProcessEnv;
111
- fetchImpl?: FetchLike;
112
- /** How the package managers are asked for their global roots. */
113
- run?: CommandRunner;
114
- execute?: InstallExecutor;
115
- confirm?: UpdateConfirm;
116
- };
117
-
118
- /**
119
- * What became of one plan.
120
- *
121
- * - `updated`: the install ran and exited 0.
122
- * - `failed`: the install ran and exited non-zero (`code`).
123
- * - `declined`: the person answered no.
124
- * - `current`: nothing newer to install; nothing was asked.
125
- * - `planned`: a dry run, which runs nothing.
126
- */
127
- export type PlanOutcome = {
128
- plan: UpdatePlan;
129
- result: "updated" | "failed" | "declined" | "current" | "planned";
130
- code?: number;
131
- };
132
-
133
- /**
134
- * What became of the post-update build.
135
- *
136
- * - `built` / `failed`: it ran, and exited 0 or not (`code`).
137
- * - `skipped`: `--no-build`.
138
- * - `no-config`: the project has no sous config to build.
139
- * - `no-bin`: the newly installed copy names no bin sous can run.
140
- */
141
- export type BuildOutcome = {
142
- result: "built" | "failed" | "skipped" | "no-config" | "no-bin";
143
- directory?: string;
144
- code?: number;
145
- };
146
-
147
- /** Everything a run did. */
148
- export type SelfUpdateOutcome = {
149
- outcomes: PlanOutcome[];
150
- notices: UpdateNotice[];
151
- /** Set only when a project install was updated. */
152
- build?: BuildOutcome;
153
- /** True when an install or the build failed; the command then exits 1. */
154
- failed: boolean;
155
- };
156
-
157
- /** The key legend under every question, in the style every sous prompt uses. */
158
- const QUESTION_HINT = keysHelpTip([
159
- ["y/n", "answer"],
160
- ["⏎", "accept default"],
161
- ["⇥", "advanced"],
162
- ]);
163
-
164
- /** The flag spellings the non-interactive remedy names. */
165
- const YES_REMEDY = "pass '--yes' (spelled '-y' or '--force' if you prefer) to update every install listed";
166
-
167
- /**
168
- * Works out every plan, then takes them one at a time: print, ask, run.
169
- *
170
- * @throws ConfigError when the registry cannot be read or a `--version` spec
171
- * matches nothing (before anything runs), and NonInteractiveError when a
172
- * question has to be asked and cannot be.
173
- */
174
- export async function runSelfUpdate(options: SelfUpdateOptions): Promise<SelfUpdateOutcome> {
175
- const env = options.env ?? process.env;
176
- const execute = options.execute ?? spawnInstall;
177
- const confirm: UpdateConfirm = options.confirm ?? ((config) => confirmPrompt(config));
178
-
179
- const metadata = await fetchPublishedVersions({
180
- env,
181
- ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
182
- });
183
- const discovery = await discoverInstalls({
184
- cwd: options.cwd,
185
- ownRoot: options.ownRoot,
186
- ...(options.run === undefined ? {} : { run: options.run }),
187
- });
188
- const { plans, notices } = buildUpdatePlans(discovery, metadata, {
189
- request: options.request,
190
- only: options.only,
191
- ...(options.prerelease === undefined ? {} : { prerelease: options.prerelease }),
192
- });
193
-
194
- if (notices.length > 0) {
195
- section("Not updated");
196
- for (const notice of notices) printNotice(notice);
197
- }
198
-
199
- const actionable = plans.filter((plan) => plan.direction !== "current");
200
- const globalCount = plans.filter((plan) => plan.scope === "global").length;
201
- const outcome: SelfUpdateOutcome = { outcomes: [], notices, failed: false };
202
-
203
- if (plans.length === 0) {
204
- blankLine();
205
- note("There is no install of sous here that sous can update.", { indent: 2 });
206
- return outcome;
207
- }
208
-
209
- heading(options.dryRun ? "Update plan" : "Updating");
210
-
211
- for (const plan of plans) {
212
- const title = planTitle(plan, globalCount);
213
- const buildDir =
214
- plan.scope === "project" && plan.projectRoot !== undefined
215
- ? options.buildDirFor(plan.projectRoot)
216
- : undefined;
217
- subheading(title);
218
- showVariables(planFacts(plan, { dryRun: options.dryRun, build: options.build, buildDir }));
219
-
220
- if (plan.direction === "current") {
221
- outcome.outcomes.push({ plan, result: "current" });
222
- continue;
223
- }
224
- if (options.dryRun) {
225
- outcome.outcomes.push({ plan, result: "planned" });
226
- continue;
227
- }
228
-
229
- const question = questionFor(plan, title);
230
- let proceed = options.yes;
231
- if (!proceed) {
232
- if (!options.interactive) {
233
- throw nonInteractiveError({
234
- prompt: `"${question}"`,
235
- remedy: `${YES_REMEDY}.`,
236
- });
237
- }
238
- const index = actionable.indexOf(plan) + 1;
239
- proceed = await ask(confirm, question, plan, `Question ${index} of ${actionable.length}: ${title}`);
240
- }
241
- if (!proceed) {
242
- outcome.outcomes.push({ plan, result: "declined" });
243
- blankLine();
244
- log(indent(`The ${title.toLowerCase()} was left at ${plan.installed}.`));
245
- continue;
246
- }
247
-
248
- const code = await runCommandAnnounced(execute, plan.command, env);
249
- if (code !== 0) {
250
- outcome.failed = true;
251
- outcome.outcomes.push({ plan, result: "failed", code });
252
- displayError(
253
- `${plan.manager} exited with code ${code}, so the ${title.toLowerCase()} is still at ` +
254
- `${plan.installed}. Its own output is above.`,
255
- (line) => console.error(line)
256
- );
257
- continue;
258
- }
259
- outcome.outcomes.push({ plan, result: "updated" });
260
- blankLine();
261
- log(indent(`The ${title.toLowerCase()} is now at ${plan.target}.`));
262
-
263
- if (plan.scope === "project") {
264
- outcome.build = await buildProject(plan, buildDir, options.build, execute, env);
265
- if (outcome.build.result === "failed" || outcome.build.result === "no-bin") {
266
- outcome.failed = true;
267
- }
268
- }
269
- }
270
-
271
- if (options.dryRun) {
272
- blankLine();
273
- dryRunNotice("Nothing was installed. Run the same command without '--dry-run' to update.");
274
- return outcome;
275
- }
276
-
277
- printSummary(outcome, globalCount);
278
- return outcome;
279
- }
280
-
281
- /** "Global install", "Global install (pnpm)" when there are several, or "Project install". */
282
- function planTitle(plan: UpdatePlan, globalCount: number): string {
283
- if (plan.scope === "project") return "Project install";
284
- return globalCount > 1 ? `Global install (${plan.manager})` : "Global install";
285
- }
286
-
287
- /** The question one plan asks. */
288
- function questionFor(plan: UpdatePlan, title: string): string {
289
- const verb = plan.direction === "downgrade" ? "Downgrade" : "Update";
290
- return `${verb} the ${title.toLowerCase()} from ${plan.installed} to ${plan.target}?`;
291
- }
292
-
293
- /** A command as a person would type it. */
294
- export function formatCommand(command: PlannedCommand): string {
295
- return [command.command, ...command.args].join(" ");
296
- }
297
-
298
- /**
299
- * The facts one plan prints before its question. The command itself is shown
300
- * in a dry run, and behind Tab otherwise, where it is printed again just
301
- * before it runs.
302
- */
303
- function planFacts(
304
- plan: UpdatePlan,
305
- context: { dryRun: boolean; build: boolean; buildDir: string | undefined }
306
- ): VariableEntry[] {
307
- const facts: VariableEntry[] = [];
308
- if (plan.projectRoot !== undefined) facts.push({ label: "Project", value: plan.projectRoot });
309
- facts.push(
310
- plan.pnpRoot === undefined
311
- ? { label: "Location", value: plan.location }
312
- : { label: "Location", value: plan.location, detail: "Yarn Plug'n'Play, which keeps no node_modules copy" }
313
- );
314
- facts.push(
315
- plan.managerAssumed
316
- ? {
317
- label: "Package manager",
318
- value: plan.manager,
319
- detail: "assumed, because the project's files name no package manager",
320
- }
321
- : { label: "Package manager", value: plan.manager }
322
- );
323
- if (plan.declaredIn !== undefined) {
324
- facts.push({
325
- label: "Declared in",
326
- value: plan.declaredIn.section,
327
- detail: `as ${plan.declaredIn.range}`,
328
- });
329
- }
330
- facts.push({ label: "Installed", value: plan.installed });
331
-
332
- if (plan.direction === "current") {
333
- facts.push({
334
- label: "Will install",
335
- value: "nothing",
336
- detail: `${plan.installed} is already the version this update chooses`,
337
- });
338
- return facts;
339
- }
340
- facts.push(
341
- plan.direction === "downgrade"
342
- ? { label: "Will install", value: plan.target, detail: `a downgrade from ${plan.installed}` }
343
- : { label: "Will install", value: plan.target }
344
- );
345
- if (context.dryRun) {
346
- facts.push(commandFact("Command", plan.command));
347
- }
348
- if (plan.scope === "project") facts.push(buildFact(context.build, context.buildDir));
349
- return facts;
350
- }
351
-
352
- /** The row naming a command and, when it has one, where it runs. */
353
- function commandFact(label: string, command: PlannedCommand): VariableEntry {
354
- return command.cwd === undefined
355
- ? { label, value: formatCommand(command) }
356
- : { label, value: formatCommand(command), detail: `in ${command.cwd}` };
357
- }
358
-
359
- /** The row saying whether a project update is followed by a build. */
360
- function buildFact(build: boolean, buildDir: string | undefined): VariableEntry {
361
- if (!build) return { label: "Then builds", value: "no", detail: "--no-build was passed" };
362
- if (buildDir === undefined) {
363
- return { label: "Then builds", value: "no", detail: "the project has no sous config" };
364
- }
365
- return { label: "Then builds", value: "yes", detail: `with the new copy, in ${buildDir}` };
366
- }
367
-
368
- /**
369
- * Asks one plan's question. Tab shows the command the answer runs, and the
370
- * question is asked again.
371
- */
372
- async function ask(
373
- confirm: UpdateConfirm,
374
- question: string,
375
- plan: UpdatePlan,
376
- header: string
377
- ): Promise<boolean> {
378
- for (;;) {
379
- blankLines(2);
380
- log(color.bold(header));
381
- const answered = await confirm({ message: question, default: true, hint: QUESTION_HINT });
382
- if (answered.kind === "value") return answered.value;
383
- blankLine();
384
- showVariables([
385
- commandFact("Command", plan.command),
386
- { label: "Real path", value: realpathOr(plan.location) },
387
- ]);
388
- }
389
- }
390
-
391
- /** Prints a command, then runs it with the terminal handed over, and returns its exit code. */
392
- async function runCommandAnnounced(
393
- execute: InstallExecutor,
394
- command: PlannedCommand,
395
- env: NodeJS.ProcessEnv
396
- ): Promise<number> {
397
- blankLine();
398
- showVariables([commandFact("Running", command)]);
399
- blankLine();
400
- return execute(command, { env });
401
- }
402
-
403
- /**
404
- * Runs the newly installed project copy's own build, so the lockfile's
405
- * `core/sous-skills` pin moves to the version just installed, with the
406
- * hand-off switched off so no other copy takes the build over. The copy's bin
407
- * is read from its package.json again, now that the install has replaced it,
408
- * and runs under this Node. Under Yarn Plug'n'Play there is no bin on disk to
409
- * run that way, so Yarn runs the dependency's bin itself
410
- * (`yarn run --binaries-only sous build`), with its resolution hooks loaded.
411
- */
412
- async function buildProject(
413
- plan: UpdatePlan,
414
- buildDir: string | undefined,
415
- build: boolean,
416
- execute: InstallExecutor,
417
- env: NodeJS.ProcessEnv
418
- ): Promise<BuildOutcome> {
419
- if (!build) return { result: "skipped" };
420
- if (buildDir === undefined) return { result: "no-config" };
421
-
422
- section("Building the project");
423
- let command: PlannedCommand;
424
- if (plan.pnpRoot !== undefined) {
425
- command = { command: "yarn", args: ["run", "--binaries-only", "sous", "build"], cwd: buildDir };
426
- } else {
427
- const root = realpathOr(plan.location);
428
- const entry = binEntryOf(readPackageJson(root));
429
- if (entry === undefined) {
430
- displayError(
431
- `The update stands, but the new copy at ${root} names no bin sous can run, so the ` +
432
- "project was not built. Run 'sous build' in the project.",
433
- (line) => console.error(line)
434
- );
435
- return { result: "no-bin", directory: buildDir };
436
- }
437
- command = { command: process.execPath, args: [path.resolve(root, entry), "build"], cwd: buildDir };
438
- }
439
- const code = await runCommandAnnounced(execute, command, { ...env, SOUS_NO_DELEGATE: "1" });
440
- if (code !== 0) {
441
- displayError(
442
- `The update stands, but the build that followed it exited with code ${code}. ` +
443
- "Fix what the build reported above and run 'sous build' again." +
444
- (plan.pnpRoot === undefined ? "" : PNP_BUILD_HINT),
445
- (line) => console.error(line)
446
- );
447
- return { result: "failed", directory: buildDir, code };
448
- }
449
- return { result: "built", directory: buildDir };
450
- }
451
-
452
- /**
453
- * What a failed build under Yarn Plug'n'Play adds: sous cannot run from inside
454
- * Yarn's zip cache, only from a copy Yarn has unpacked.
455
- */
456
- const PNP_BUILD_HINT =
457
- " Under Yarn Plug'n'Play, sous runs only from an unpacked copy: if the error above is " +
458
- "'EBADF: bad file descriptor', add \"dependenciesMeta\": { \"@sous-io/sous\": { \"unplugged\": true } } " +
459
- "to package.json, then run 'yarn install'.";
460
-
461
- /** Prints one notice: its sentence, then where the copy is and the command to run by hand. */
462
- function printNotice(notice: UpdateNotice): void {
463
- note(notice.message, { indent: 2 });
464
- const facts: VariableEntry[] = [];
465
- if (notice.location !== undefined) facts.push({ label: "Location", value: notice.location });
466
- if (notice.manualCommand !== undefined) facts.push(commandFact("Run by hand", notice.manualCommand));
467
- showVariables(facts);
468
- blankLine();
469
- }
470
-
471
- /** The summary: one line per plan, and one for the build when it applied. */
472
- function printSummary(outcome: SelfUpdateOutcome, globalCount: number): void {
473
- section("Summary");
474
- const rows: VariableEntry[] = outcome.outcomes.map(({ plan, result, code }) => {
475
- const label = planTitle(plan, globalCount);
476
- switch (result) {
477
- case "updated":
478
- return { label, value: `updated to ${plan.target}`, detail: `from ${plan.installed}` };
479
- case "failed":
480
- return { label, value: `failed`, detail: `${plan.manager} exited with code ${code}` };
481
- case "declined":
482
- return { label, value: `skipped`, detail: `left at ${plan.installed}` };
483
- default:
484
- return { label, value: `up to date at ${plan.installed}` };
485
- }
486
- });
487
- const build = outcome.build;
488
- if (build !== undefined) {
489
- const label = "Project build";
490
- if (build.result === "built") rows.push({ label, value: "succeeded", detail: `in ${build.directory}` });
491
- if (build.result === "failed") {
492
- rows.push({ label, value: "failed", detail: `exited with code ${build.code}` });
493
- }
494
- if (build.result === "no-bin") rows.push({ label, value: "failed", detail: "the new copy names no bin" });
495
- if (build.result === "skipped") rows.push({ label, value: "skipped", detail: "--no-build was passed" });
496
- if (build.result === "no-config") {
497
- rows.push({ label, value: "not run", detail: "the project has no sous config" });
498
- }
499
- }
500
- showVariables(rows);
501
- }
502
-
503
- /** The registry URL an update reads, for the command's opening block. */
504
- export function registryUrl(env: NodeJS.ProcessEnv = process.env): string {
505
- return packageMetadataUrl(registryBaseUrl(env));
506
- }