@sous-io/sous 0.2.4 → 0.2.7
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 +10 -2
- package/bin/run.js +45 -20
- package/docs/markdown/README.md +42 -0
- package/docs/markdown/_sidebar.md +1 -0
- package/docs/markdown/commands.md +8 -2
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/lib/project-install.d.mts +40 -0
- package/src/lib/project-install.mjs +201 -0
- package/src/lib/version-report.ts +90 -0
package/README.md
CHANGED
|
@@ -26,12 +26,20 @@ formats agents actually read: `.claude/` plus `CLAUDE.md` for Claude Code, `.cod
|
|
|
26
26
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
|
-
Install the CLI:
|
|
29
|
+
Install the CLI globally, or add it to a project, or both:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm install -g @sous-io/sous
|
|
32
|
+
npm install -g @sous-io/sous # one sous for every project on the machine
|
|
33
|
+
npm install -D @sous-io/sous # the version this project's templates were written against
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
A project install pins the sous version a project builds with, and it always does the
|
|
37
|
+
building: a global `sous` run anywhere inside a project that holds `node_modules/@sous-io/sous`
|
|
38
|
+
hands the command to that copy, so `sous`, `npx sous` and a package script all produce the same
|
|
39
|
+
output. When the two versions differ, one line on standard error names the version handed off
|
|
40
|
+
to (`--verbose` adds where both installs are). Set `SOUS_NO_DELEGATE=1` to run the copy you
|
|
41
|
+
invoked instead.
|
|
42
|
+
|
|
35
43
|
Or run it from a clone (useful when developing sous itself):
|
|
36
44
|
|
|
37
45
|
```bash
|
package/bin/run.js
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { handOffToProjectInstall, isEnvFlagOn } from "../src/lib/project-install.mjs";
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
6
|
+
// A project that installs @sous-io/sous itself has pinned the version its
|
|
7
|
+
// templates and lockfile were written against, so that copy does the work.
|
|
8
|
+
// This runs before anything else is loaded: when a project copy is found, its
|
|
9
|
+
// own bin is imported into this process and this one loads nothing further
|
|
10
|
+
// (see src/lib/project-install.mjs for the rules, and SOUS_NO_DELEGATE to
|
|
11
|
+
// keep the invoked copy running).
|
|
12
|
+
const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const handedOff = await handOffToProjectInstall({ ownRoot });
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
if (!handedOff) {
|
|
16
|
+
// The CLI runs from TypeScript source; register tsx before oclif dynamically
|
|
17
|
+
// imports any command module. Resolving "tsx" from this file (rather than a
|
|
18
|
+
// $PKG_ROOT/node_modules path) works in every install layout: repo clone,
|
|
19
|
+
// global install (nested deps), and local/npx installs (hoisted deps).
|
|
20
|
+
const { register } = await import("tsx/esm/api");
|
|
21
|
+
register();
|
|
11
22
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
15
|
-
|
|
23
|
+
// `sous --version` is answered here rather than by oclif, whose answer is
|
|
24
|
+
// its user agent string. Plain, it is the version alone; `--verbose` adds
|
|
25
|
+
// the package, where it is installed, the platform and the Node build.
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
const { isVersionRequest, printVersion } = await import("../src/lib/version-report.ts");
|
|
28
|
+
if (isVersionRequest(argv)) {
|
|
29
|
+
printVersion(argv, ownRoot);
|
|
30
|
+
} else {
|
|
31
|
+
await runCommand();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// src/utils/command-errors.ts), so development mode is switched on only when
|
|
20
|
-
// SOUS_DEBUG asks for the traces; anything that gets past a command's own
|
|
21
|
-
// reporting then prints its stack too.
|
|
22
|
-
const debugRequested = !["", "0", "false", "no", "off"].includes(
|
|
23
|
-
(process.env.SOUS_DEBUG ?? "").trim().toLowerCase()
|
|
24
|
-
);
|
|
35
|
+
async function runCommand() {
|
|
36
|
+
const { execute, settings } = await import("@oclif/core");
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
// tsx (above) already makes .ts imports work, so oclif's own auto-transpile
|
|
39
|
+
// machinery is redundant; leaving it on makes every downstream run warn that
|
|
40
|
+
// the (unshipped) typescript devDependency is missing.
|
|
41
|
+
settings.enableAutoTranspile = false;
|
|
42
|
+
|
|
43
|
+
// oclif's development mode turns on its debug setting, which makes every error
|
|
44
|
+
// it prints a raw stack trace. Sous reports its own errors as sentences (see
|
|
45
|
+
// src/utils/command-errors.ts), so development mode is switched on only when
|
|
46
|
+
// SOUS_DEBUG asks for the traces; anything that gets past a command's own
|
|
47
|
+
// reporting then prints its stack too.
|
|
48
|
+
const debugRequested = isEnvFlagOn(process.env.SOUS_DEBUG);
|
|
49
|
+
|
|
50
|
+
await execute({ development: debugRequested, dir: import.meta.url });
|
|
51
|
+
}
|
package/docs/markdown/README.md
CHANGED
|
@@ -17,6 +17,48 @@ compiled 4 targets, pruned 1 stale file
|
|
|
17
17
|
?> These docs are young. **Configuration** and **Repositories** are the reference material so
|
|
18
18
|
far; more will follow.
|
|
19
19
|
|
|
20
|
+
## Installing
|
|
21
|
+
|
|
22
|
+
Sous installs three ways, and they work together:
|
|
23
|
+
|
|
24
|
+
- **Globally** (`npm install -g @sous-io/sous`): one `sous` on the path for every project on the
|
|
25
|
+
machine, and the one to reach for first.
|
|
26
|
+
- **In a project** (`npm install -D @sous-io/sous`): the project pins the version its templates
|
|
27
|
+
and its lockfile were written against, and `npx sous` or a package script runs it. This is the
|
|
28
|
+
right choice for a team, because the implicit `core` subscription asks for exactly the running
|
|
29
|
+
version and a project built by two versions in turn rewrites its committed lockfile back and
|
|
30
|
+
forth.
|
|
31
|
+
- **Both**: a global `sous` run anywhere inside a project that holds `node_modules/@sous-io/sous`
|
|
32
|
+
hands the whole command to that copy before loading any of its own code, so the project's
|
|
33
|
+
version always does the building however it was invoked. The lookup walks up from the working
|
|
34
|
+
directory the way Node resolves a package, so a copy hoisted to a monorepo root is found from
|
|
35
|
+
any package inside it.
|
|
36
|
+
|
|
37
|
+
When the two versions differ, one line on standard error names the version handed off to;
|
|
38
|
+
standard output is untouched, so a piped command prints exactly what it always did. Add
|
|
39
|
+
`--verbose` to any command and the notice also says where both installs are and how to keep
|
|
40
|
+
the invoked one running; set `SOUS_DEBUG` and the notice prints on every hand-off. Set
|
|
41
|
+
`SOUS_NO_DELEGATE` to anything but `0`, `false`, `no` or `off` to run the copy you invoked
|
|
42
|
+
instead, for debugging a broken project install or for deliberately using the global one:
|
|
43
|
+
|
|
44
|
+
```term
|
|
45
|
+
$ sous --version
|
|
46
|
+
Handing off to the project-level Sous install: v0.2.4
|
|
47
|
+
v0.2.4
|
|
48
|
+
$ sous --version --verbose
|
|
49
|
+
Handing off to the project-level Sous install: v0.2.4
|
|
50
|
+
Project install: /work/app/node_modules/@sous-io/sous
|
|
51
|
+
Invoked install: v0.3.0 at /usr/lib/node_modules/@sous-io/sous
|
|
52
|
+
Set SOUS_NO_DELEGATE=1 to run the invoked install instead.
|
|
53
|
+
v0.2.4
|
|
54
|
+
Package : @sous-io/sous
|
|
55
|
+
Install : /work/app/node_modules/@sous-io/sous
|
|
56
|
+
Platform: linux-x64
|
|
57
|
+
Node : v22.21.0
|
|
58
|
+
$ SOUS_NO_DELEGATE=1 sous --version
|
|
59
|
+
v0.3.0
|
|
60
|
+
```
|
|
61
|
+
|
|
20
62
|
## Where to look
|
|
21
63
|
|
|
22
64
|
- Watch the [animated introduction](../) for the full pitch
|
|
@@ -21,3 +21,4 @@
|
|
|
21
21
|
- [0001: Repositories](adrs/0001-repositories.md)
|
|
22
22
|
- [0002: Recipe answers in the template scope](adrs/0002-recipe-answers-in-templates.md)
|
|
23
23
|
- [0003: Project setup with sous init](adrs/0003-project-init.md)
|
|
24
|
+
- [0004: A global sous defers to the project's install](adrs/0004-project-install-handoff.md)
|
|
@@ -38,7 +38,8 @@ under [When sous cannot ask](repositories-consuming.md#when-sous-cannot-ask).
|
|
|
38
38
|
|
|
39
39
|
Help has four spellings. `sous --help` prints the root screen; `sous repo add --help`, `sous repo add -h` and
|
|
40
40
|
`sous help repo add` all print that one command's. `sous help` alone lists the topics and commands, and
|
|
41
|
-
`sous --version` prints the version,
|
|
41
|
+
`sous --version` prints the version alone, as `v1.2.3`; `sous --version --verbose` adds the package name, where
|
|
42
|
+
it is installed, the platform and the Node build under it.
|
|
42
43
|
|
|
43
44
|
Every topic answers to both spellings of its name: `repo` and `repos`, `subscription` and `subscriptions`,
|
|
44
45
|
`namespace` and `namespaces`, `recipe` and `recipes`, `lock` and `locks`, `vars` and `var`, `config` and
|
|
@@ -338,7 +339,12 @@ $ sous subscription add workflow --non-interactive
|
|
|
338
339
|
No expected failure prints a stack trace. A failure sous did not expect prints the message and one more sentence
|
|
339
340
|
asking you to set `SOUS_DEBUG=1` and run the command again. Set `SOUS_DEBUG` to anything but `0`, `false`, `no`
|
|
340
341
|
or `off` and every reported failure prints its stack to standard error underneath the message:
|
|
341
|
-
`SOUS_DEBUG=1 sous build`.
|
|
342
|
+
`SOUS_DEBUG=1 sous build`. The one other thing it changes is that every hand-off to a project's own install
|
|
343
|
+
announces itself, not only one between different versions.
|
|
344
|
+
|
|
345
|
+
`SOUS_NO_DELEGATE` is the other environment variable every command reads. Set it the same way and the copy of
|
|
346
|
+
sous you invoked runs the command, even inside a project that installs its own `@sous-io/sous`; see
|
|
347
|
+
[Installing](README.md#installing) for the hand-off it switches off.
|
|
342
348
|
|
|
343
349
|
Every listing fits itself to the terminal it runs in: columns shrink, descriptions wrap, and a path or URL is
|
|
344
350
|
cut in the middle so the host and the last segment both survive. On a terminal too narrow, the least important
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for project-install.mjs, which is plain JavaScript because it runs
|
|
3
|
+
* before tsx is registered. Keep the two in step by hand.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PACKAGE_NAME: "@sous-io/sous";
|
|
7
|
+
export const NO_DELEGATE_ENV: "SOUS_NO_DELEGATE";
|
|
8
|
+
export const DEBUG_ENV: "SOUS_DEBUG";
|
|
9
|
+
export const VERBOSE_FLAG: "--verbose";
|
|
10
|
+
|
|
11
|
+
export type ProjectInstall =
|
|
12
|
+
| { same: true; root: string }
|
|
13
|
+
| { same: false; root: string; version: string; bin: string };
|
|
14
|
+
|
|
15
|
+
export type HandoffPlan =
|
|
16
|
+
| { kind: "run-self" }
|
|
17
|
+
| { kind: "hand-off"; install: Extract<ProjectInstall, { same: false }>; notice: string[] };
|
|
18
|
+
|
|
19
|
+
export function isEnvFlagOn(value: string | undefined): boolean;
|
|
20
|
+
export function binEntryOf(pkg: unknown): string | undefined;
|
|
21
|
+
export function findProjectInstall(startDir: string, ownRoot: string): ProjectInstall | undefined;
|
|
22
|
+
export function formatHandoffNotice(input: {
|
|
23
|
+
install: Extract<ProjectInstall, { same: false }>;
|
|
24
|
+
ownVersion: string;
|
|
25
|
+
ownRoot: string;
|
|
26
|
+
verbose: boolean;
|
|
27
|
+
}): string[];
|
|
28
|
+
export function planHandoff(input: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
ownRoot: string;
|
|
31
|
+
env: Record<string, string | undefined>;
|
|
32
|
+
argv?: readonly string[];
|
|
33
|
+
}): HandoffPlan;
|
|
34
|
+
export function handOffToProjectInstall(input: {
|
|
35
|
+
ownRoot: string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
env?: Record<string, string | undefined>;
|
|
38
|
+
argv?: readonly string[];
|
|
39
|
+
stderr?: { write(chunk: string): unknown };
|
|
40
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hand-off from the sous that was invoked to the sous a project installs.
|
|
3
|
+
*
|
|
4
|
+
* A global install is a convenience launcher; a project's own `@sous-io/sous`
|
|
5
|
+
* dependency is the version its templates and its lockfile were written
|
|
6
|
+
* against, and it is the one that has to do the work (the implicit `core`
|
|
7
|
+
* subscription asks for exactly the running version, so two versions taking
|
|
8
|
+
* turns in one project rewrite the committed lockfile back and forth). The
|
|
9
|
+
* published bin (bin/run.js) calls `handOffToProjectInstall` before it loads
|
|
10
|
+
* anything else, and when a project copy is found, imports that copy's own bin
|
|
11
|
+
* in this same process and lets it run the command.
|
|
12
|
+
*
|
|
13
|
+
* Plain JavaScript ESM, no TypeScript syntax: this runs BEFORE tsx is
|
|
14
|
+
* registered, under bare Node, because the whole point is to load none of the
|
|
15
|
+
* invoked install's code when another copy should run. It ships to npm via the
|
|
16
|
+
* package.json "files": "src" allowlist, like the config kernel next to it.
|
|
17
|
+
*
|
|
18
|
+
* The rules, all of them here and nowhere else:
|
|
19
|
+
* - The lookup walks up from the working directory looking for
|
|
20
|
+
* `node_modules/@sous-io/sous`, the way Node resolves a package, so a copy
|
|
21
|
+
* hoisted to a monorepo root is found from any package inside it.
|
|
22
|
+
* - A copy whose real path is the invoked install's own root is "self", and
|
|
23
|
+
* self never hands off; that is what stops the project's copy from
|
|
24
|
+
* handing off to itself after the global copy handed off to it.
|
|
25
|
+
* - The copy's bin is read from its package.json `bin` field, never assumed,
|
|
26
|
+
* so an older layout (the bin was once called `xcv`) still works.
|
|
27
|
+
* - Anything unreadable or ambiguous means "run the copy that was invoked".
|
|
28
|
+
* A hand-off is a convenience; a refusal to run is not.
|
|
29
|
+
* - `SOUS_NO_DELEGATE` (anything but 0/false/no/off) runs the invoked copy.
|
|
30
|
+
* - The notice goes to stderr, so piped stdout stays clean, and only when
|
|
31
|
+
* the two versions differ; `SOUS_DEBUG` prints it on every hand-off. It is
|
|
32
|
+
* one line naming the version handed off to; `--verbose` anywhere on the
|
|
33
|
+
* command line (or `SOUS_DEBUG`) adds where both installs are and how to
|
|
34
|
+
* keep the invoked one running.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import { pathToFileURL } from "node:url";
|
|
40
|
+
|
|
41
|
+
/** The npm package name a project copy is looked up under. */
|
|
42
|
+
export const PACKAGE_NAME = "@sous-io/sous";
|
|
43
|
+
|
|
44
|
+
/** The environment variable that keeps the invoked copy running. */
|
|
45
|
+
export const NO_DELEGATE_ENV = "SOUS_NO_DELEGATE";
|
|
46
|
+
|
|
47
|
+
/** The environment variable that makes every hand-off announce itself. */
|
|
48
|
+
export const DEBUG_ENV = "SOUS_DEBUG";
|
|
49
|
+
|
|
50
|
+
/** The flag that makes the notice say where both installs are. */
|
|
51
|
+
export const VERBOSE_FLAG = "--verbose";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether an on/off environment variable is on: set to anything but an empty
|
|
55
|
+
* string, `0`, `false`, `no` or `off` (case-insensitive, whitespace trimmed).
|
|
56
|
+
* The same reading `SOUS_DEBUG` has always had.
|
|
57
|
+
*/
|
|
58
|
+
export function isEnvFlagOn(value) {
|
|
59
|
+
return !["", "0", "false", "no", "off"].includes((value ?? "").trim().toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads a package.json, returning the parsed object or undefined for a file
|
|
64
|
+
* that is missing, unreadable or not JSON.
|
|
65
|
+
*/
|
|
66
|
+
function readPackageJson(dir) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Works out which file a package's `bin` field names for the `sous` command,
|
|
76
|
+
* relative to the package root, or undefined when it cannot be told: no field,
|
|
77
|
+
* an object naming neither `sous` nor `xcv` with more than one entry, or a
|
|
78
|
+
* value that is not a string.
|
|
79
|
+
*/
|
|
80
|
+
export function binEntryOf(pkg) {
|
|
81
|
+
const bin = pkg?.bin;
|
|
82
|
+
if (typeof bin === "string") return bin;
|
|
83
|
+
if (!bin || typeof bin !== "object") return undefined;
|
|
84
|
+
const named = bin.sous ?? bin.xcv;
|
|
85
|
+
if (typeof named === "string") return named;
|
|
86
|
+
const entries = Object.values(bin);
|
|
87
|
+
if (entries.length === 1 && typeof entries[0] === "string") return entries[0];
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Looks up from `startDir` for a project install of the package.
|
|
93
|
+
*
|
|
94
|
+
* Returns undefined when no ancestor holds `node_modules/@sous-io/sous`, or
|
|
95
|
+
* when the first one found is not usable (its package.json does not name the
|
|
96
|
+
* package, or its bin cannot be determined or does not exist). Returns
|
|
97
|
+
* `{ same: true, root }` when the first copy found IS the invoked install
|
|
98
|
+
* (`ownRoot`), compared by real path, and otherwise `{ same: false, root,
|
|
99
|
+
* version, bin }` with `bin` as an absolute path.
|
|
100
|
+
*
|
|
101
|
+
* The walk stops at the first copy, usable or not: a broken copy nearer the
|
|
102
|
+
* working directory is what `npx` would run too, and skipping past it to an
|
|
103
|
+
* older one further up would be a guess.
|
|
104
|
+
*/
|
|
105
|
+
export function findProjectInstall(startDir, ownRoot) {
|
|
106
|
+
let dir = path.resolve(startDir);
|
|
107
|
+
for (;;) {
|
|
108
|
+
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
|
+
}
|
|
112
|
+
const parent = path.dirname(dir);
|
|
113
|
+
if (parent === dir) return undefined;
|
|
114
|
+
dir = parent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function realpathOr(p) {
|
|
119
|
+
try {
|
|
120
|
+
return fs.realpathSync(p);
|
|
121
|
+
} catch {
|
|
122
|
+
return path.resolve(p);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function describeInstall(candidate, ownRoot) {
|
|
127
|
+
const root = realpathOr(candidate);
|
|
128
|
+
if (root === realpathOr(ownRoot)) return { same: true, root };
|
|
129
|
+
const pkg = readPackageJson(root);
|
|
130
|
+
if (!pkg || pkg.name !== PACKAGE_NAME) return undefined;
|
|
131
|
+
const entry = binEntryOf(pkg);
|
|
132
|
+
if (!entry) return undefined;
|
|
133
|
+
const bin = path.resolve(root, entry);
|
|
134
|
+
if (!fs.existsSync(bin)) return undefined;
|
|
135
|
+
return { same: false, root, version: typeof pkg.version === "string" ? pkg.version : "unknown", bin };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The lines the notice is made of: one line naming the version handed off to,
|
|
140
|
+
* and, when verbose, where both installs are and how to keep the invoked one
|
|
141
|
+
* running. Plain text, because this prints before tsx exists; the block
|
|
142
|
+
* mirrors the shape `showVariables` gives a key and value list.
|
|
143
|
+
*/
|
|
144
|
+
export function formatHandoffNotice({ install, ownVersion, ownRoot, verbose }) {
|
|
145
|
+
const lines = [`Handing off to the project-level Sous install: v${install.version}`];
|
|
146
|
+
if (verbose) {
|
|
147
|
+
lines.push(
|
|
148
|
+
` Project install: ${install.root}`,
|
|
149
|
+
` Invoked install: v${ownVersion} at ${ownRoot}`,
|
|
150
|
+
`Set ${NO_DELEGATE_ENV}=1 to run the invoked install instead.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Decides what the invoked install should do, without doing it.
|
|
158
|
+
*
|
|
159
|
+
* Returns `{ kind: "run-self" }` when the invoked copy runs the command, or
|
|
160
|
+
* `{ kind: "hand-off", install, notice }` naming the project copy to import
|
|
161
|
+
* and the lines to print on stderr first (an empty list when nothing is said).
|
|
162
|
+
*/
|
|
163
|
+
export function planHandoff({ cwd, ownRoot, env, argv = [] }) {
|
|
164
|
+
if (isEnvFlagOn(env[NO_DELEGATE_ENV])) return { kind: "run-self" };
|
|
165
|
+
const install = findProjectInstall(cwd, ownRoot);
|
|
166
|
+
if (!install || install.same) return { kind: "run-self" };
|
|
167
|
+
|
|
168
|
+
const ownPkg = readPackageJson(ownRoot);
|
|
169
|
+
const ownVersion = typeof ownPkg?.version === "string" ? ownPkg.version : "unknown";
|
|
170
|
+
const debug = isEnvFlagOn(env[DEBUG_ENV]);
|
|
171
|
+
const announce = ownVersion !== install.version || debug;
|
|
172
|
+
const notice = announce
|
|
173
|
+
? formatHandoffNotice({
|
|
174
|
+
install,
|
|
175
|
+
ownVersion,
|
|
176
|
+
ownRoot: realpathOr(ownRoot),
|
|
177
|
+
verbose: debug || argv.includes(VERBOSE_FLAG),
|
|
178
|
+
})
|
|
179
|
+
: [];
|
|
180
|
+
return { kind: "hand-off", install, notice };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hands the current invocation to the project's copy when there is one to hand
|
|
185
|
+
* it to. Resolves true after that copy's bin has run (it reads the same
|
|
186
|
+
* process.argv and sets the same exit code), and false, having loaded nothing,
|
|
187
|
+
* when the invoked copy should run the command itself.
|
|
188
|
+
*/
|
|
189
|
+
export async function handOffToProjectInstall({
|
|
190
|
+
ownRoot,
|
|
191
|
+
cwd = process.cwd(),
|
|
192
|
+
env = process.env,
|
|
193
|
+
argv = process.argv.slice(2),
|
|
194
|
+
stderr = process.stderr,
|
|
195
|
+
}) {
|
|
196
|
+
const plan = planHandoff({ cwd, ownRoot, env, argv });
|
|
197
|
+
if (plan.kind !== "hand-off") return false;
|
|
198
|
+
for (const line of plan.notice) stderr.write(`${line}\n`);
|
|
199
|
+
await import(pathToFileURL(plan.install.bin).href);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `sous --version` prints.
|
|
3
|
+
*
|
|
4
|
+
* oclif answers `--version` with its user agent string (package, version,
|
|
5
|
+
* platform and Node build on one line). Sous answers it itself, in bin/run.js,
|
|
6
|
+
* before oclif is loaded: the version alone, or with `--verbose` the same facts
|
|
7
|
+
* as a key and value list, so a person asking "which sous is this?" gets one
|
|
8
|
+
* short line and a person asking "where is it?" gets the rest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import { log, showVariables } from "../utils/formatting.js";
|
|
14
|
+
|
|
15
|
+
/** The one flag oclif treats as a version request, and so does sous. */
|
|
16
|
+
export const VERSION_FLAG = "--version";
|
|
17
|
+
|
|
18
|
+
/** The flag that adds where the install is, the platform and the Node build. */
|
|
19
|
+
export const VERBOSE_FLAG = "--verbose";
|
|
20
|
+
|
|
21
|
+
/** The facts a version report is made of. */
|
|
22
|
+
export type VersionFacts = {
|
|
23
|
+
/** The npm package name. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** The package version. */
|
|
26
|
+
version: string;
|
|
27
|
+
/** The absolute path of the install answering. */
|
|
28
|
+
root: string;
|
|
29
|
+
/** The platform and architecture, the way oclif spells them. */
|
|
30
|
+
platform: string;
|
|
31
|
+
/** The Node build, with its leading `v`. */
|
|
32
|
+
node: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Whether the command line is a version request: `--version` as its first word. */
|
|
36
|
+
export function isVersionRequest(argv: readonly string[]): boolean {
|
|
37
|
+
return argv[0] === VERSION_FLAG;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Reads the facts about the install rooted at `packageRoot`. */
|
|
41
|
+
export function readVersionFacts(packageRoot: string): VersionFacts {
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(`${packageRoot}/package.json`, "utf8")) as {
|
|
43
|
+
name?: unknown;
|
|
44
|
+
version?: unknown;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
name: typeof pkg.name === "string" ? pkg.name : "unknown",
|
|
48
|
+
version: typeof pkg.version === "string" ? pkg.version : "unknown",
|
|
49
|
+
root: packageRoot,
|
|
50
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
51
|
+
node: process.version,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Prints the report: the version on its own line, and with `verbose` the
|
|
57
|
+
* facts under it as a key and value list.
|
|
58
|
+
*
|
|
59
|
+
* @param facts - What to report.
|
|
60
|
+
* @param verbose - Whether to print the facts under the version.
|
|
61
|
+
* @param write - Where each line goes; standard output by default.
|
|
62
|
+
*/
|
|
63
|
+
export function printVersionReport(
|
|
64
|
+
facts: VersionFacts,
|
|
65
|
+
verbose: boolean,
|
|
66
|
+
write: (line: string) => void = log
|
|
67
|
+
): void {
|
|
68
|
+
write(`v${facts.version}`);
|
|
69
|
+
if (!verbose) return;
|
|
70
|
+
showVariables(
|
|
71
|
+
{
|
|
72
|
+
Package: facts.name,
|
|
73
|
+
Install: facts.root,
|
|
74
|
+
Platform: facts.platform,
|
|
75
|
+
Node: facts.node,
|
|
76
|
+
},
|
|
77
|
+
{ write }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Answers a version request from the command line: `sous --version`, or
|
|
83
|
+
* `sous --version --verbose`.
|
|
84
|
+
*
|
|
85
|
+
* @param argv - The command line after the program name.
|
|
86
|
+
* @param packageRoot - The root of the install answering.
|
|
87
|
+
*/
|
|
88
|
+
export function printVersion(argv: readonly string[], packageRoot: string): void {
|
|
89
|
+
printVersionReport(readVersionFacts(packageRoot), argv.includes(VERBOSE_FLAG));
|
|
90
|
+
}
|