@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.
Files changed (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -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 -237
  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 -391
  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 -412
  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 -1105
  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,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;
@@ -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
- }