@sous-io/sous 0.2.36 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -237
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -391
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -412
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1105
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
package/src/lib/package-info.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where this installation of sous lives, and what version it is.
|
|
3
|
-
*
|
|
4
|
-
* These two facts are needed by modules at every level, including ones that
|
|
5
|
-
* `settings.ts` itself imports, so they live in their own dependency-free module
|
|
6
|
-
* rather than in `settings.ts`. `settings.ts` re-exports both under their
|
|
7
|
-
* long-standing names, so nothing else has to care where they moved to.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
|
|
16
|
-
/** Resolved path to the package root (two levels up from `src/lib/`). */
|
|
17
|
-
export const CLI_ROOT = path.resolve(path.dirname(__filename), "../..");
|
|
18
|
-
|
|
19
|
-
/** Version string read from the package's own package.json at module load time. */
|
|
20
|
-
const packageJson = JSON.parse(
|
|
21
|
-
fs.readFileSync(path.join(CLI_ROOT, "package.json"), "utf8")
|
|
22
|
-
) as { version: string };
|
|
23
|
-
|
|
24
|
-
/** The running sous version. */
|
|
25
|
-
export const SOUS_VERSION: string = packageJson.version;
|
package/src/lib/pid-service.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { VarScope } from "./settings.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Manages PID files for the Sous watcher, enforcing a single instance per config.
|
|
7
|
-
* The PID file lives at <sousDir>/sous.pid by default.
|
|
8
|
-
*/
|
|
9
|
-
export class PidService {
|
|
10
|
-
/**
|
|
11
|
-
* Returns the PID file path for a config.
|
|
12
|
-
*
|
|
13
|
-
* Precedence (mirrors StateService.getFilePath):
|
|
14
|
-
* 1. A `pidFilePath` variable in the settings scope (explicit override).
|
|
15
|
-
* 2. `<sousDir>/sous.pid`.
|
|
16
|
-
* 3. `<cwd>/sous.pid` as a last resort.
|
|
17
|
-
*
|
|
18
|
-
* @param vars - The resolved settings-scope variables.
|
|
19
|
-
*/
|
|
20
|
-
getFilePath(vars?: VarScope): string {
|
|
21
|
-
if (vars?.pidFilePath) {
|
|
22
|
-
return vars.pidFilePath;
|
|
23
|
-
}
|
|
24
|
-
if (vars?.sousDir) {
|
|
25
|
-
return path.join(vars.sousDir, "sous.pid");
|
|
26
|
-
}
|
|
27
|
-
return path.join(process.cwd(), "sous.pid");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Checks if a watcher is already running for this config.
|
|
32
|
-
* - If a PID file exists and the process is alive, throws an Error.
|
|
33
|
-
* - If a PID file exists but the process is dead (stale), overwrites it.
|
|
34
|
-
* - If no PID file exists, creates one with the current process.pid.
|
|
35
|
-
*
|
|
36
|
-
* @param pidFilePath - Path from getFilePath().
|
|
37
|
-
* @param label - Human-readable project label used in the "already running"
|
|
38
|
-
* error (e.g. `BaseCommand.projectLabel`). Falls back to a label derived
|
|
39
|
-
* from the PID file path.
|
|
40
|
-
*/
|
|
41
|
-
async acquire(pidFilePath: string, label?: string): Promise<void> {
|
|
42
|
-
if (fs.existsSync(pidFilePath)) {
|
|
43
|
-
const raw = fs.readFileSync(pidFilePath, "utf8").trim();
|
|
44
|
-
const existingPid = parseInt(raw, 10);
|
|
45
|
-
|
|
46
|
-
if (!isNaN(existingPid)) {
|
|
47
|
-
const alive = this._isProcessAlive(existingPid);
|
|
48
|
-
if (alive) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`A watcher is already running for project '${label ?? deriveLabel(pidFilePath)}' (PID ${existingPid}). Stop it first or delete ${pidFilePath}.`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
// Stale PID file — fall through and overwrite
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const dir = path.dirname(pidFilePath);
|
|
58
|
-
if (!fs.existsSync(dir)) {
|
|
59
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
fs.writeFileSync(pidFilePath, String(process.pid), "utf8");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Removes the PID file. Called on clean exit.
|
|
66
|
-
*/
|
|
67
|
-
async release(pidFilePath: string): Promise<void> {
|
|
68
|
-
if (fs.existsSync(pidFilePath)) {
|
|
69
|
-
fs.unlinkSync(pidFilePath);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Returns true if the given PID corresponds to a running process.
|
|
75
|
-
* Uses signal 0 to probe without sending a real signal.
|
|
76
|
-
*/
|
|
77
|
-
_isProcessAlive(pid: number): boolean {
|
|
78
|
-
try {
|
|
79
|
-
process.kill(pid, 0);
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Derives a project label from a PID file path when the caller supplied none.
|
|
89
|
-
* The default PID file lives at `<project>/.sous/sous.pid`, so a plain
|
|
90
|
-
* `basename(dirname(...))` would always say ".sous"; skip past that directory
|
|
91
|
-
* to the project directory itself.
|
|
92
|
-
*/
|
|
93
|
-
function deriveLabel(pidFilePath: string): string {
|
|
94
|
-
let dir = path.dirname(pidFilePath);
|
|
95
|
-
if (path.basename(dir) === ".sous") {
|
|
96
|
-
dir = path.dirname(dir);
|
|
97
|
-
}
|
|
98
|
-
return path.basename(dir);
|
|
99
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
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 readPackageJson(dir: string): Record<string, unknown> | undefined;
|
|
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;
|
|
26
|
-
export function formatHandoffNotice(input: {
|
|
27
|
-
install: Extract<ProjectInstall, { same: false }>;
|
|
28
|
-
ownVersion: string;
|
|
29
|
-
ownRoot: string;
|
|
30
|
-
verbose: boolean;
|
|
31
|
-
}): string[];
|
|
32
|
-
export function planHandoff(input: {
|
|
33
|
-
cwd: string;
|
|
34
|
-
ownRoot: string;
|
|
35
|
-
env: Record<string, string | undefined>;
|
|
36
|
-
argv?: readonly string[];
|
|
37
|
-
}): HandoffPlan;
|
|
38
|
-
export function handOffToProjectInstall(input: {
|
|
39
|
-
ownRoot: string;
|
|
40
|
-
cwd?: string;
|
|
41
|
-
env?: Record<string, string | undefined>;
|
|
42
|
-
argv?: readonly string[];
|
|
43
|
-
stderr?: { write(chunk: string): unknown };
|
|
44
|
-
}): Promise<boolean>;
|
|
@@ -1,234 +0,0 @@
|
|
|
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
|
-
* - `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.
|
|
34
|
-
* - The notice goes to stderr, so piped stdout stays clean. It prints when
|
|
35
|
-
* the two versions differ, and on every hand-off under `SOUS_DEBUG` or
|
|
36
|
-
* with `--verbose` anywhere on the command line. Plain, it is one line
|
|
37
|
-
* naming the version handed off to; verbose (`--verbose` or `SOUS_DEBUG`)
|
|
38
|
-
* it adds where both installs are and how to keep the invoked one running.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import fs from "node:fs";
|
|
42
|
-
import path from "node:path";
|
|
43
|
-
import { pathToFileURL } from "node:url";
|
|
44
|
-
|
|
45
|
-
/** The npm package name a project copy is looked up under. */
|
|
46
|
-
export const PACKAGE_NAME = "@sous-io/sous";
|
|
47
|
-
|
|
48
|
-
/** The environment variable that keeps the invoked copy running. */
|
|
49
|
-
export const NO_DELEGATE_ENV = "SOUS_NO_DELEGATE";
|
|
50
|
-
|
|
51
|
-
/** The environment variable that makes every hand-off announce itself. */
|
|
52
|
-
export const DEBUG_ENV = "SOUS_DEBUG";
|
|
53
|
-
|
|
54
|
-
/** The flag that makes the notice say where both installs are. */
|
|
55
|
-
export const VERBOSE_FLAG = "--verbose";
|
|
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
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Whether an on/off environment variable is on: set to anything but an empty
|
|
73
|
-
* string, `0`, `false`, `no` or `off` (case-insensitive, whitespace trimmed).
|
|
74
|
-
* The same reading `SOUS_DEBUG` has always had.
|
|
75
|
-
*/
|
|
76
|
-
export function isEnvFlagOn(value) {
|
|
77
|
-
return !["", "0", "false", "no", "off"].includes((value ?? "").trim().toLowerCase());
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Reads the package.json in `dir`, returning the parsed object or undefined
|
|
82
|
-
* for a file that is missing, unreadable or not JSON.
|
|
83
|
-
*/
|
|
84
|
-
export function readPackageJson(dir) {
|
|
85
|
-
try {
|
|
86
|
-
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
87
|
-
} catch {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Works out which file a package's `bin` field names for the `sous` command,
|
|
94
|
-
* relative to the package root, or undefined when it cannot be told: no field,
|
|
95
|
-
* an object naming neither `sous` nor `xcv` with more than one entry, or a
|
|
96
|
-
* value that is not a string.
|
|
97
|
-
*/
|
|
98
|
-
export function binEntryOf(pkg) {
|
|
99
|
-
const bin = pkg?.bin;
|
|
100
|
-
if (typeof bin === "string") return bin;
|
|
101
|
-
if (!bin || typeof bin !== "object") return undefined;
|
|
102
|
-
const named = bin.sous ?? bin.xcv;
|
|
103
|
-
if (typeof named === "string") return named;
|
|
104
|
-
const entries = Object.values(bin);
|
|
105
|
-
if (entries.length === 1 && typeof entries[0] === "string") return entries[0];
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Looks up from `startDir` for a project install of the package.
|
|
111
|
-
*
|
|
112
|
-
* Returns undefined when no ancestor holds `node_modules/@sous-io/sous`, or
|
|
113
|
-
* when the first one found is not usable (its package.json does not name the
|
|
114
|
-
* package, or its bin cannot be determined or does not exist). Returns
|
|
115
|
-
* `{ same: true, root }` when the first copy found IS the invoked install
|
|
116
|
-
* (`ownRoot`), compared by real path, and otherwise `{ same: false, root,
|
|
117
|
-
* version, bin }` with `bin` as an absolute path.
|
|
118
|
-
*
|
|
119
|
-
* The walk stops at the first copy, usable or not: a broken copy nearer the
|
|
120
|
-
* working directory is what `npx` would run too, and skipping past it to an
|
|
121
|
-
* older one further up would be a guess.
|
|
122
|
-
*/
|
|
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) {
|
|
135
|
-
let dir = path.resolve(startDir);
|
|
136
|
-
for (;;) {
|
|
137
|
-
const candidate = path.join(dir, "node_modules", ...PACKAGE_NAME.split("/"));
|
|
138
|
-
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
139
|
-
const parent = path.dirname(dir);
|
|
140
|
-
if (parent === dir) return undefined;
|
|
141
|
-
dir = parent;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
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) {
|
|
150
|
-
try {
|
|
151
|
-
return fs.realpathSync(p);
|
|
152
|
-
} catch {
|
|
153
|
-
return path.resolve(p);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
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) {
|
|
164
|
-
const root = realpathOr(candidate);
|
|
165
|
-
if (root === realpathOr(ownRoot)) return { same: true, root };
|
|
166
|
-
const pkg = readPackageJson(root);
|
|
167
|
-
if (!pkg || pkg.name !== PACKAGE_NAME) return undefined;
|
|
168
|
-
const entry = binEntryOf(pkg);
|
|
169
|
-
if (!entry) return undefined;
|
|
170
|
-
const bin = path.resolve(root, entry);
|
|
171
|
-
if (!fs.existsSync(bin)) return undefined;
|
|
172
|
-
return { same: false, root, version: typeof pkg.version === "string" ? pkg.version : "unknown", bin };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The lines the notice is made of: one line naming the version handed off to,
|
|
177
|
-
* and, when verbose, where both installs are and how to keep the invoked one
|
|
178
|
-
* running. Plain text, because this prints before tsx exists; the block
|
|
179
|
-
* mirrors the shape `showVariables` gives a key and value list.
|
|
180
|
-
*/
|
|
181
|
-
export function formatHandoffNotice({ install, ownVersion, ownRoot, verbose }) {
|
|
182
|
-
const lines = [`Handing off to the project-level Sous install: v${install.version}`];
|
|
183
|
-
if (verbose) {
|
|
184
|
-
lines.push(
|
|
185
|
-
` Project install: ${install.root}`,
|
|
186
|
-
` Invoked install: v${ownVersion} at ${ownRoot}`,
|
|
187
|
-
`Set ${NO_DELEGATE_ENV}=1 to run the invoked install instead.`
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
return lines;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Decides what the invoked install should do, without doing it.
|
|
195
|
-
*
|
|
196
|
-
* Returns `{ kind: "run-self" }` when the invoked copy runs the command, or
|
|
197
|
-
* `{ kind: "hand-off", install, notice }` naming the project copy to import
|
|
198
|
-
* and the lines to print on stderr first (an empty list when nothing is said).
|
|
199
|
-
*/
|
|
200
|
-
export function planHandoff({ cwd, ownRoot, env, argv = [] }) {
|
|
201
|
-
if (isEnvFlagOn(env[NO_DELEGATE_ENV])) return { kind: "run-self" };
|
|
202
|
-
if (commandWordOf(argv) === SELF_UPDATE_COMMAND) return { kind: "run-self" };
|
|
203
|
-
const install = findProjectInstall(cwd, ownRoot);
|
|
204
|
-
if (!install || install.same) return { kind: "run-self" };
|
|
205
|
-
|
|
206
|
-
const ownPkg = readPackageJson(ownRoot);
|
|
207
|
-
const ownVersion = typeof ownPkg?.version === "string" ? ownPkg.version : "unknown";
|
|
208
|
-
const verbose = isEnvFlagOn(env[DEBUG_ENV]) || argv.includes(VERBOSE_FLAG);
|
|
209
|
-
const announce = ownVersion !== install.version || verbose;
|
|
210
|
-
const notice = announce
|
|
211
|
-
? formatHandoffNotice({ install, ownVersion, ownRoot: realpathOr(ownRoot), verbose })
|
|
212
|
-
: [];
|
|
213
|
-
return { kind: "hand-off", install, notice };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Hands the current invocation to the project's copy when there is one to hand
|
|
218
|
-
* it to. Resolves true after that copy's bin has run (it reads the same
|
|
219
|
-
* process.argv and sets the same exit code), and false, having loaded nothing,
|
|
220
|
-
* when the invoked copy should run the command itself.
|
|
221
|
-
*/
|
|
222
|
-
export async function handOffToProjectInstall({
|
|
223
|
-
ownRoot,
|
|
224
|
-
cwd = process.cwd(),
|
|
225
|
-
env = process.env,
|
|
226
|
-
argv = process.argv.slice(2),
|
|
227
|
-
stderr = process.stderr,
|
|
228
|
-
}) {
|
|
229
|
-
const plan = planHandoff({ cwd, ownRoot, env, argv });
|
|
230
|
-
if (plan.kind !== "hand-off") return false;
|
|
231
|
-
for (const line of plan.notice) stderr.write(`${line}\n`);
|
|
232
|
-
await import(pathToFileURL(plan.install.bin).href);
|
|
233
|
-
return true;
|
|
234
|
-
}
|