@sous-io/sous 0.2.29 → 0.2.31
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.
- package/README.md +12 -0
- package/docs/markdown/README.md +52 -0
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +31 -6
- package/docs/markdown/repositories-authoring.md +7 -3
- package/docs/markdown/repositories-consuming.md +26 -12
- package/docs/markdown/repositories-file-formats.md +35 -5
- package/docs/markdown/repositories-variables.md +4 -1
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/base-command.ts +75 -2
- package/src/commands/lock/rebuild.ts +5 -4
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/update.ts +178 -0
- package/src/lib/project-install.d.mts +4 -0
- package/src/lib/project-install.mjs +46 -8
- package/src/lib/repos/catalog.ts +43 -55
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +40 -1
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +6 -204
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/providers/http.ts +37 -16
- package/src/lib/repos/release/index-builder.ts +168 -32
- package/src/lib/repos/resolver.ts +53 -12
- package/src/lib/repos/subscription-service.ts +93 -11
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/self-update/index.ts +14 -0
- package/src/lib/self-update/installs.ts +412 -0
- package/src/lib/self-update/managers.ts +126 -0
- package/src/lib/self-update/plan.ts +315 -0
- package/src/lib/self-update/registry.ts +99 -0
- package/src/lib/self-update/run.ts +506 -0
- package/src/lib/self-update/versions.ts +243 -0
- package/src/lib/vars/question-plan.ts +44 -21
|
@@ -2,27 +2,41 @@
|
|
|
2
2
|
* `sous recipe show <ref>`.
|
|
3
3
|
*
|
|
4
4
|
* Shows one recipe in full: where it is published, every version it publishes,
|
|
5
|
-
* what the version this project would use depends on
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* what the version this project would use depends on and how each dependency is
|
|
6
|
+
* declared, everything subscribing to it would install, every question that
|
|
7
|
+
* would ask, and where its files land in this project. It reads only what sous
|
|
8
|
+
* already has on disk: the repository's cached index, the project's lockfile,
|
|
9
|
+
* and the recipe's own files when they are in the store or a linked working
|
|
10
|
+
* copy. The index describes each version's dependencies and questions, so a
|
|
11
|
+
* recipe nothing has installed is described as fully as one in the store, its
|
|
12
|
+
* files excepted. `--latest` reads the repository's index from upstream
|
|
13
|
+
* instead, saving nothing, and `--installed` looks the ref up among installed
|
|
14
|
+
* recipes only.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { Args } from "@oclif/core";
|
|
18
|
+
import semver from "semver";
|
|
14
19
|
import { BaseCommand } from "../../base-command.js";
|
|
15
20
|
import { resolveRootScope } from "../../lib/settings.js";
|
|
16
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
subscriptionServiceFor,
|
|
23
|
+
type SubscriptionPreview,
|
|
24
|
+
type SubscriptionService,
|
|
25
|
+
} from "../../lib/repos/subscription-service.js";
|
|
17
26
|
import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
|
|
18
27
|
import {
|
|
19
28
|
describeInstalled,
|
|
20
29
|
describeRecipe,
|
|
30
|
+
type CatalogInputs,
|
|
21
31
|
type RecipeContentListing,
|
|
22
32
|
type RecipeDependencyListing,
|
|
23
|
-
type
|
|
33
|
+
type RecipeDetail,
|
|
24
34
|
type RecipeVersionListing,
|
|
25
35
|
} from "../../lib/repos/catalog.js";
|
|
36
|
+
import { PROJECT_REQUESTER } from "../../lib/repos/resolver.js";
|
|
37
|
+
import { describeDeclaration, describeDependencyKind } from "../../lib/repos/declarations.js";
|
|
38
|
+
import { formatQuestionPlan } from "../../lib/vars/question-plan.js";
|
|
39
|
+
import { describeError } from "../../lib/repos/release/validate.js";
|
|
26
40
|
import {
|
|
27
41
|
INDENT,
|
|
28
42
|
describeIndexSource,
|
|
@@ -61,14 +75,19 @@ const DEPENDENCY_COLUMNS: TableColumn[] = [
|
|
|
61
75
|
{ key: "kind", header: "Kind", overflow: "wrap", priority: "medium", minWidth: 16 },
|
|
62
76
|
];
|
|
63
77
|
|
|
64
|
-
/** The columns the
|
|
65
|
-
const
|
|
66
|
-
{ key: "
|
|
67
|
-
{ key: "
|
|
68
|
-
{ key: "
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
/** The columns the listing of everything a subscription installs shows. */
|
|
79
|
+
const INSTALLS_COLUMNS: TableColumn[] = [
|
|
80
|
+
{ key: "key", header: "Recipe", kind: "path", overflow: "truncate", minWidth: 12 },
|
|
81
|
+
{ key: "version", header: "Version", overflow: "truncate", minWidth: 7 },
|
|
82
|
+
{ key: "kind", header: "Kind", overflow: "wrap", minWidth: 16 },
|
|
83
|
+
{
|
|
84
|
+
key: "neededBy",
|
|
85
|
+
header: "Needed by",
|
|
86
|
+
overflow: "wrap",
|
|
87
|
+
flex: 1,
|
|
88
|
+
priority: "medium",
|
|
89
|
+
minWidth: 12,
|
|
90
|
+
},
|
|
72
91
|
];
|
|
73
92
|
|
|
74
93
|
/** The columns the content listing shows. */
|
|
@@ -86,7 +105,8 @@ const CONTENT_COLUMNS: TableColumn[] = [
|
|
|
86
105
|
];
|
|
87
106
|
|
|
88
107
|
export default class RecipeShow extends BaseCommand {
|
|
89
|
-
static description =
|
|
108
|
+
static description =
|
|
109
|
+
"Show one recipe: its versions, dependencies, what subscribing installs and asks, and its files";
|
|
90
110
|
|
|
91
111
|
/**
|
|
92
112
|
* The other spelling of the topic. It lives under a hidden topic, so it is
|
|
@@ -165,24 +185,76 @@ export default class RecipeShow extends BaseCommand {
|
|
|
165
185
|
printBrowsingNotes({ notChecked: notChecked.filter((name) => name === detail.repo) });
|
|
166
186
|
|
|
167
187
|
this.printVersions(detail.versions);
|
|
168
|
-
this.printDependencies(
|
|
188
|
+
this.printDependencies(
|
|
189
|
+
detail.dependencies,
|
|
190
|
+
detail.manifestRead || detail.dependenciesRecorded
|
|
191
|
+
);
|
|
169
192
|
|
|
170
|
-
|
|
193
|
+
const preview = await this.preview(service, inputs, detail);
|
|
194
|
+
if (typeof preview === "string") {
|
|
195
|
+
blankLine();
|
|
196
|
+
paragraph(preview);
|
|
197
|
+
} else {
|
|
198
|
+
this.printInstalls(preview);
|
|
199
|
+
this.printQuestions(preview);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (detail.manifestRead) {
|
|
203
|
+
this.printContents(detail.contents);
|
|
204
|
+
} else {
|
|
205
|
+
blankLine();
|
|
206
|
+
subheading("Where its files land in this project");
|
|
171
207
|
blankLine();
|
|
172
208
|
paragraph(
|
|
173
|
-
"The recipe's own files are not on this machine, so the
|
|
174
|
-
"
|
|
209
|
+
"The recipe's own files are not on this machine, so the files it publishes are " +
|
|
210
|
+
"not known here. Subscribing to it fetches them."
|
|
175
211
|
);
|
|
176
|
-
footer();
|
|
177
|
-
return;
|
|
178
212
|
}
|
|
179
213
|
|
|
180
|
-
this.printVariables(detail.variables);
|
|
181
|
-
this.printContents(detail.contents);
|
|
182
|
-
|
|
183
214
|
footer();
|
|
184
215
|
}
|
|
185
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Works out what subscribing to the described version would install and ask,
|
|
219
|
+
* from the same indexes the rest of the page reads, fetching nothing. The
|
|
220
|
+
* answer is a sentence instead when it cannot be worked out.
|
|
221
|
+
*
|
|
222
|
+
* @param service - The project's subscription service.
|
|
223
|
+
* @param inputs - The catalog's inputs, whose indexes the preview resolves against.
|
|
224
|
+
* @param detail - The recipe being described.
|
|
225
|
+
*/
|
|
226
|
+
private async preview(
|
|
227
|
+
service: SubscriptionService,
|
|
228
|
+
inputs: CatalogInputs,
|
|
229
|
+
detail: RecipeDetail
|
|
230
|
+
): Promise<SubscriptionPreview | string> {
|
|
231
|
+
if (detail.describing === undefined) {
|
|
232
|
+
return (
|
|
233
|
+
"This recipe publishes no version a subscription would install, so nothing " +
|
|
234
|
+
"is described below."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
return await service.previewSubscription(
|
|
239
|
+
{
|
|
240
|
+
repo: detail.repo,
|
|
241
|
+
namespace: detail.namespace,
|
|
242
|
+
recipe: detail.name,
|
|
243
|
+
range: detail.describing,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
indexes: new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
|
|
247
|
+
...(semver.prerelease(detail.describing) === null ? {} : { prerelease: true }),
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return (
|
|
252
|
+
`Sous could not work out what subscribing to version ${detail.describing} ` +
|
|
253
|
+
`would install. ${describeError(error)}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
186
258
|
/**
|
|
187
259
|
* Every published version, newest first, with what each one is to this
|
|
188
260
|
* project.
|
|
@@ -207,24 +279,21 @@ export default class RecipeShow extends BaseCommand {
|
|
|
207
279
|
}
|
|
208
280
|
|
|
209
281
|
/**
|
|
210
|
-
* What the described version depends on
|
|
211
|
-
*
|
|
212
|
-
* was released.
|
|
282
|
+
* What the described version depends on: the manifest entry that brings each
|
|
283
|
+
* dependency in and whether it is a co-subscription or a build dependency,
|
|
284
|
+
* and what its repository's index resolved it to when it was released.
|
|
213
285
|
*
|
|
214
286
|
* @param dependencies - The dependencies the catalog listed.
|
|
215
|
-
* @param
|
|
287
|
+
* @param known - Whether the manifest or the index says what the version declares.
|
|
216
288
|
*/
|
|
217
|
-
private printDependencies(
|
|
218
|
-
dependencies: RecipeDependencyListing[],
|
|
219
|
-
manifestRead: boolean
|
|
220
|
-
): void {
|
|
289
|
+
private printDependencies(dependencies: RecipeDependencyListing[], known: boolean): void {
|
|
221
290
|
blankLine();
|
|
222
291
|
subheading("What it depends on");
|
|
223
292
|
blankLine();
|
|
224
293
|
|
|
225
294
|
if (dependencies.length === 0) {
|
|
226
295
|
paragraph(
|
|
227
|
-
|
|
296
|
+
known
|
|
228
297
|
? "This recipe depends on nothing else."
|
|
229
298
|
: "The repository's index records no dependencies for this version."
|
|
230
299
|
);
|
|
@@ -233,7 +302,10 @@ export default class RecipeShow extends BaseCommand {
|
|
|
233
302
|
|
|
234
303
|
const rows = dependencies.map((entry) => ({
|
|
235
304
|
key: entry.key,
|
|
236
|
-
declared:
|
|
305
|
+
declared:
|
|
306
|
+
entry.declared === undefined
|
|
307
|
+
? "not recorded in the index"
|
|
308
|
+
: describeDeclaration(entry.declared),
|
|
237
309
|
resolved:
|
|
238
310
|
entry.resolvedVersion ??
|
|
239
311
|
(entry.resolvedRange === undefined
|
|
@@ -249,32 +321,69 @@ export default class RecipeShow extends BaseCommand {
|
|
|
249
321
|
}
|
|
250
322
|
|
|
251
323
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
324
|
+
* Every recipe a subscription to the described version would install, each
|
|
325
|
+
* with whether its files land in the project and what brings it in, and the
|
|
326
|
+
* repositories it would need that the project does not trust yet.
|
|
254
327
|
*
|
|
255
|
-
* @param
|
|
328
|
+
* @param preview - What subscribing would install and ask.
|
|
256
329
|
*/
|
|
257
|
-
private
|
|
330
|
+
private printInstalls(preview: SubscriptionPreview): void {
|
|
258
331
|
blankLine();
|
|
259
|
-
subheading("What
|
|
332
|
+
subheading("What subscribing installs");
|
|
260
333
|
blankLine();
|
|
261
334
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
335
|
+
const rows = preview.resolved.map((recipe) => {
|
|
336
|
+
const subscribed = recipe.requestedBy.includes(PROJECT_REQUESTER);
|
|
337
|
+
const holders = recipe.requestedBy.filter((holder) => holder !== PROJECT_REQUESTER);
|
|
338
|
+
return {
|
|
339
|
+
key: recipe.key,
|
|
340
|
+
version: recipe.version,
|
|
341
|
+
kind: subscribed ? "the subscription itself" : describeDependencyKind(recipe.kind),
|
|
342
|
+
neededBy: holders.length === 0 ? "this subscription" : holders.join(", "),
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
for (const line of renderTable(INSTALLS_COLUMNS, rows, { indent: INDENT })) {
|
|
347
|
+
log(indent(line, INDENT));
|
|
265
348
|
}
|
|
266
349
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
350
|
+
if (preview.missingRepos.length > 0) {
|
|
351
|
+
blankLine();
|
|
352
|
+
paragraph(
|
|
353
|
+
`Subscribing also needs ${
|
|
354
|
+
preview.missingRepos.length === 1 ? "a repository" : "repositories"
|
|
355
|
+
} this project does not trust yet, so what ${
|
|
356
|
+
preview.missingRepos.length === 1 ? "it publishes" : "they publish"
|
|
357
|
+
} is not listed above: ` +
|
|
358
|
+
preview.missingRepos
|
|
359
|
+
.map(
|
|
360
|
+
(missing) =>
|
|
361
|
+
`${missing.name}${missing.url === undefined ? "" : ` (${missing.url})`}, ` +
|
|
362
|
+
`needed by ${missing.requiredBy.map((entry) => entry.requestedBy).join(", ")}`
|
|
363
|
+
)
|
|
364
|
+
.join("; ") +
|
|
365
|
+
"."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
275
369
|
|
|
276
|
-
|
|
277
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Every question a subscription to the described version would ask, laid out
|
|
372
|
+
* exactly as `sous subscription add --dry-run` lays them out, without the
|
|
373
|
+
* flags that answer them ahead of time.
|
|
374
|
+
*
|
|
375
|
+
* @param preview - What subscribing would install and ask.
|
|
376
|
+
*/
|
|
377
|
+
private printQuestions(preview: SubscriptionPreview): void {
|
|
378
|
+
blankLine();
|
|
379
|
+
subheading("What subscribing asks you");
|
|
380
|
+
blankLine();
|
|
381
|
+
|
|
382
|
+
for (const line of formatQuestionPlan(preview.questions, {
|
|
383
|
+
unreadable: preview.unreadable,
|
|
384
|
+
answerHints: false,
|
|
385
|
+
})) {
|
|
386
|
+
log(line === "" ? "" : indent(line));
|
|
278
387
|
}
|
|
279
388
|
}
|
|
280
389
|
|
|
@@ -311,13 +420,3 @@ export default class RecipeShow extends BaseCommand {
|
|
|
311
420
|
}
|
|
312
421
|
}
|
|
313
422
|
|
|
314
|
-
/**
|
|
315
|
-
* Plain-language wording for how a dependency was declared.
|
|
316
|
-
*
|
|
317
|
-
* @param kind - What the manifest declared it as, when the manifest was read.
|
|
318
|
-
*/
|
|
319
|
-
function describeDependencyKind(kind: "depends" | "subscribes" | undefined): string {
|
|
320
|
-
if (kind === "depends") return "build dependency";
|
|
321
|
-
if (kind === "subscribes") return "co-subscription";
|
|
322
|
-
return "unknown";
|
|
323
|
-
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sous update` moves sous itself to a newer (or a chosen) version, for the
|
|
3
|
+
* global install, the project's own install, or both, through whichever
|
|
4
|
+
* package manager owns each one (sous-io/sous#135).
|
|
5
|
+
*
|
|
6
|
+
* sous update
|
|
7
|
+
* sous update --global
|
|
8
|
+
* sous update --major
|
|
9
|
+
* sous update --version 0.2.18
|
|
10
|
+
* sous update --dry-run
|
|
11
|
+
*
|
|
12
|
+
* It runs outside a project as readily as inside one, so it opts out of the
|
|
13
|
+
* config requirement, as `sous init` does. `bin/run.js` never hands this
|
|
14
|
+
* command to a project's own copy: updating the copy that was invoked is the
|
|
15
|
+
* point. The flow itself lives in `lib/self-update/run.ts`; this class turns
|
|
16
|
+
* the flags into its options and decides the exit code.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { Flags } from "@oclif/core";
|
|
21
|
+
import { BaseCommand } from "../base-command.js";
|
|
22
|
+
import { SOUS_DIR_NAME, findConfigInSousDir } from "../lib/config-discovery.js";
|
|
23
|
+
import { ConfigError, isConfigError } from "../lib/errors.js";
|
|
24
|
+
import { CLI_ROOT } from "../lib/package-info.js";
|
|
25
|
+
import { registryUrl, runSelfUpdate } from "../lib/self-update/run.js";
|
|
26
|
+
import type { UpdateScope, VersionRequest } from "../lib/self-update/index.js";
|
|
27
|
+
import { confirmationFlag } from "../utils/flags.js";
|
|
28
|
+
import { footer, showCommandVars } from "../utils/formatting.js";
|
|
29
|
+
|
|
30
|
+
export default class Update extends BaseCommand {
|
|
31
|
+
static description =
|
|
32
|
+
"Update sous itself, globally and in this project, through the package manager that installed each copy";
|
|
33
|
+
|
|
34
|
+
/** This command updates sous wherever it is installed; a project config is optional. */
|
|
35
|
+
static override requiresConfig = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A config this copy cannot load (one written for a newer sous, say) is the
|
|
39
|
+
* very thing an update may fix, so it is a warning here rather than a stop.
|
|
40
|
+
*/
|
|
41
|
+
static override toleratesConfigErrors = true;
|
|
42
|
+
|
|
43
|
+
static examples = [
|
|
44
|
+
"<%= config.bin %> update",
|
|
45
|
+
"<%= config.bin %> update --global",
|
|
46
|
+
"<%= config.bin %> update --major",
|
|
47
|
+
"<%= config.bin %> update --version 0.2.18",
|
|
48
|
+
"<%= config.bin %> update --version next",
|
|
49
|
+
"<%= config.bin %> update --dry-run",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
static flags = {
|
|
53
|
+
...BaseCommand.baseFlags,
|
|
54
|
+
major: Flags.boolean({
|
|
55
|
+
description: "Install the newest published version, whatever its major version",
|
|
56
|
+
default: false,
|
|
57
|
+
exclusive: ["version"],
|
|
58
|
+
}),
|
|
59
|
+
version: Flags.string({
|
|
60
|
+
description: "Install this version, the newest version in this range, or the version this dist-tag names",
|
|
61
|
+
exclusive: ["major"],
|
|
62
|
+
}),
|
|
63
|
+
prerelease: Flags.boolean({
|
|
64
|
+
description:
|
|
65
|
+
"Count prerelease versions as candidates; on by default when the installed version is a prerelease",
|
|
66
|
+
allowNo: true,
|
|
67
|
+
aliases: ["pre"],
|
|
68
|
+
}),
|
|
69
|
+
global: Flags.boolean({
|
|
70
|
+
description: "Update only the global install",
|
|
71
|
+
default: false,
|
|
72
|
+
exclusive: ["project"],
|
|
73
|
+
}),
|
|
74
|
+
project: Flags.boolean({
|
|
75
|
+
description: "Update only this project's own install",
|
|
76
|
+
default: false,
|
|
77
|
+
exclusive: ["global"],
|
|
78
|
+
}),
|
|
79
|
+
yes: confirmationFlag(),
|
|
80
|
+
"dry-run": Flags.boolean({
|
|
81
|
+
description: "Print every install's plan without installing anything",
|
|
82
|
+
default: false,
|
|
83
|
+
}),
|
|
84
|
+
"no-build": Flags.boolean({
|
|
85
|
+
description: "Update the project install without building the project afterwards",
|
|
86
|
+
default: false,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
async run(): Promise<void> {
|
|
91
|
+
const { flags } = await this.parse(Update);
|
|
92
|
+
const dryRun = flags["dry-run"];
|
|
93
|
+
const request: VersionRequest =
|
|
94
|
+
flags.version !== undefined
|
|
95
|
+
? { kind: "spec", spec: flags.version }
|
|
96
|
+
: flags.major
|
|
97
|
+
? { kind: "major" }
|
|
98
|
+
: { kind: "default" };
|
|
99
|
+
const only: UpdateScope | undefined = flags.global
|
|
100
|
+
? "global"
|
|
101
|
+
: flags.project
|
|
102
|
+
? "project"
|
|
103
|
+
: undefined;
|
|
104
|
+
|
|
105
|
+
showCommandVars({
|
|
106
|
+
Directory: this.configLocator.cwd,
|
|
107
|
+
Installs: only === undefined ? "global and project" : `${only} only`,
|
|
108
|
+
Version: describeRequest(request),
|
|
109
|
+
Prereleases:
|
|
110
|
+
flags.prerelease === undefined
|
|
111
|
+
? "counted when the installed version is one"
|
|
112
|
+
: flags.prerelease
|
|
113
|
+
? "counted"
|
|
114
|
+
: "not counted",
|
|
115
|
+
Registry: registryUrl(),
|
|
116
|
+
"Dry Run": dryRun,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const outcome = await runSelfUpdate({
|
|
120
|
+
cwd: this.configLocator.cwd,
|
|
121
|
+
ownRoot: CLI_ROOT,
|
|
122
|
+
request,
|
|
123
|
+
...(flags.prerelease === undefined ? {} : { prerelease: flags.prerelease }),
|
|
124
|
+
...(only === undefined ? {} : { only }),
|
|
125
|
+
dryRun,
|
|
126
|
+
yes: flags.yes,
|
|
127
|
+
interactive: this.interactive,
|
|
128
|
+
build: !flags["no-build"],
|
|
129
|
+
buildDirFor: (projectRoot) => this.buildDirFor(projectRoot),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
footer();
|
|
133
|
+
|
|
134
|
+
if (outcome.failed) {
|
|
135
|
+
throw new ConfigError(
|
|
136
|
+
"Not every step of the update succeeded; the summary above lists what completed and what failed."
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where a build after a project update runs: the directory of the config
|
|
143
|
+
* discovery found, when it lies inside the project that was updated, and
|
|
144
|
+
* otherwise the project root when its own `.sous/` holds a primary config.
|
|
145
|
+
* A config this copy could not load counts as found: the new copy may
|
|
146
|
+
* understand it, and its build is where any real problem is reported.
|
|
147
|
+
* Undefined means the project has no sous config, so nothing is built.
|
|
148
|
+
*/
|
|
149
|
+
private buildDirFor(projectRoot: string): string | undefined {
|
|
150
|
+
const found = this.hasConfig ? this.discovered.sousDir : this.rejectedConfig?.sousDir;
|
|
151
|
+
if (found !== undefined) {
|
|
152
|
+
const configDir = path.dirname(found);
|
|
153
|
+
const relative = path.relative(projectRoot, configDir);
|
|
154
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) return configDir;
|
|
155
|
+
}
|
|
156
|
+
return holdsPrimaryConfig(path.join(projectRoot, SOUS_DIR_NAME)) ? projectRoot : undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether a `.sous/` directory holds a primary config. Holding several is
|
|
162
|
+
* still holding one: the build that follows reports that mistake itself.
|
|
163
|
+
*/
|
|
164
|
+
function holdsPrimaryConfig(sousDir: string): boolean {
|
|
165
|
+
try {
|
|
166
|
+
return findConfigInSousDir(sousDir) !== null;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (isConfigError(error)) return true;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The version request, as the opening block says it. */
|
|
174
|
+
function describeRequest(request: VersionRequest): string {
|
|
175
|
+
if (request.kind === "default") return "the newest in each install's major version";
|
|
176
|
+
if (request.kind === "major") return "the newest published, whatever its major version";
|
|
177
|
+
return request.spec;
|
|
178
|
+
}
|
|
@@ -18,7 +18,11 @@ export type HandoffPlan =
|
|
|
18
18
|
|
|
19
19
|
export function isEnvFlagOn(value: string | undefined): boolean;
|
|
20
20
|
export function binEntryOf(pkg: unknown): string | undefined;
|
|
21
|
+
export function readPackageJson(dir: string): Record<string, unknown> | undefined;
|
|
21
22
|
export function findProjectInstall(startDir: string, ownRoot: string): ProjectInstall | undefined;
|
|
23
|
+
export function findProjectCopy(startDir: string): string | undefined;
|
|
24
|
+
export function realpathOr(p: string): string;
|
|
25
|
+
export function describeInstall(candidate: string, ownRoot: string): ProjectInstall | undefined;
|
|
22
26
|
export function formatHandoffNotice(input: {
|
|
23
27
|
install: Extract<ProjectInstall, { same: false }>;
|
|
24
28
|
ownVersion: string;
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* - Anything unreadable or ambiguous means "run the copy that was invoked".
|
|
28
28
|
* A hand-off is a convenience; a refusal to run is not.
|
|
29
29
|
* - `SOUS_NO_DELEGATE` (anything but 0/false/no/off) runs the invoked copy.
|
|
30
|
+
* - `sous update` runs the invoked copy: updating the copy that was invoked
|
|
31
|
+
* is its job, and a project copy may be too old to have the command. The
|
|
32
|
+
* command word is the first entry on the command line that is not a flag,
|
|
33
|
+
* so `sous subscription update` still hands off.
|
|
30
34
|
* - The notice goes to stderr, so piped stdout stays clean. It prints when
|
|
31
35
|
* the two versions differ, and on every hand-off under `SOUS_DEBUG` or
|
|
32
36
|
* with `--verbose` anywhere on the command line. Plain, it is one line
|
|
@@ -50,6 +54,20 @@ export const DEBUG_ENV = "SOUS_DEBUG";
|
|
|
50
54
|
/** The flag that makes the notice say where both installs are. */
|
|
51
55
|
export const VERBOSE_FLAG = "--verbose";
|
|
52
56
|
|
|
57
|
+
/** The command that always runs in the copy that was invoked. */
|
|
58
|
+
export const SELF_UPDATE_COMMAND = "update";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The command word on a command line: its first entry that does not start
|
|
62
|
+
* with `-`, or undefined when every entry is a flag.
|
|
63
|
+
*
|
|
64
|
+
* commandWordOf(["--verbose", "update", "--yes"]) -> "update"
|
|
65
|
+
* commandWordOf(["subscription", "update"]) -> "subscription"
|
|
66
|
+
*/
|
|
67
|
+
export function commandWordOf(argv) {
|
|
68
|
+
return argv.find((arg) => !arg.startsWith("-"));
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
/**
|
|
54
72
|
* Whether an on/off environment variable is on: set to anything but an empty
|
|
55
73
|
* string, `0`, `false`, `no` or `off` (case-insensitive, whitespace trimmed).
|
|
@@ -60,10 +78,10 @@ export function isEnvFlagOn(value) {
|
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/**
|
|
63
|
-
* Reads
|
|
64
|
-
* that is missing, unreadable or not JSON.
|
|
81
|
+
* Reads the package.json in `dir`, returning the parsed object or undefined
|
|
82
|
+
* for a file that is missing, unreadable or not JSON.
|
|
65
83
|
*/
|
|
66
|
-
function readPackageJson(dir) {
|
|
84
|
+
export function readPackageJson(dir) {
|
|
67
85
|
try {
|
|
68
86
|
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
69
87
|
} catch {
|
|
@@ -103,19 +121,32 @@ export function binEntryOf(pkg) {
|
|
|
103
121
|
* older one further up would be a guess.
|
|
104
122
|
*/
|
|
105
123
|
export function findProjectInstall(startDir, ownRoot) {
|
|
124
|
+
const candidate = findProjectCopy(startDir);
|
|
125
|
+
return candidate === undefined ? undefined : describeInstall(candidate, ownRoot);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Looks up from `startDir` for the nearest `node_modules/@sous-io/sous` that
|
|
130
|
+
* holds a package.json, and returns that path as found, WITHOUT resolving
|
|
131
|
+
* symlinks (pnpm links it into its own store, so only the unresolved path says
|
|
132
|
+
* which project holds it). Undefined when no ancestor holds one.
|
|
133
|
+
*/
|
|
134
|
+
export function findProjectCopy(startDir) {
|
|
106
135
|
let dir = path.resolve(startDir);
|
|
107
136
|
for (;;) {
|
|
108
137
|
const candidate = path.join(dir, "node_modules", ...PACKAGE_NAME.split("/"));
|
|
109
|
-
if (fs.existsSync(path.join(candidate, "package.json")))
|
|
110
|
-
return describeInstall(candidate, ownRoot);
|
|
111
|
-
}
|
|
138
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
112
139
|
const parent = path.dirname(dir);
|
|
113
140
|
if (parent === dir) return undefined;
|
|
114
141
|
dir = parent;
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
|
|
118
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The real path of `p`, or `p` resolved when it cannot be read (it does not
|
|
147
|
+
* exist, or a link in it is broken).
|
|
148
|
+
*/
|
|
149
|
+
export function realpathOr(p) {
|
|
119
150
|
try {
|
|
120
151
|
return fs.realpathSync(p);
|
|
121
152
|
} catch {
|
|
@@ -123,7 +154,13 @@ function realpathOr(p) {
|
|
|
123
154
|
}
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Describes the copy of the package at `candidate` the way `findProjectInstall`
|
|
159
|
+
* does: `{ same: true, root }` when it is `ownRoot`, compared by real path,
|
|
160
|
+
* `{ same: false, root, version, bin }` for a usable other copy, and undefined
|
|
161
|
+
* for one that is not usable.
|
|
162
|
+
*/
|
|
163
|
+
export function describeInstall(candidate, ownRoot) {
|
|
127
164
|
const root = realpathOr(candidate);
|
|
128
165
|
if (root === realpathOr(ownRoot)) return { same: true, root };
|
|
129
166
|
const pkg = readPackageJson(root);
|
|
@@ -162,6 +199,7 @@ export function formatHandoffNotice({ install, ownVersion, ownRoot, verbose }) {
|
|
|
162
199
|
*/
|
|
163
200
|
export function planHandoff({ cwd, ownRoot, env, argv = [] }) {
|
|
164
201
|
if (isEnvFlagOn(env[NO_DELEGATE_ENV])) return { kind: "run-self" };
|
|
202
|
+
if (commandWordOf(argv) === SELF_UPDATE_COMMAND) return { kind: "run-self" };
|
|
165
203
|
const install = findProjectInstall(cwd, ownRoot);
|
|
166
204
|
if (!install || install.same) return { kind: "run-self" };
|
|
167
205
|
|