@sous-io/sous 0.2.37 → 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 -245
- 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 -410
- 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 -413
- 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 -1107
- 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
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ConfigError } from "./errors.js";
|
|
4
|
-
import {
|
|
5
|
-
listRecipeConfigLayers,
|
|
6
|
-
type RecipeConfigLayer,
|
|
7
|
-
} from "./repos/recipe-config-layers.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The directory name sous looks for when walking up from the working directory.
|
|
11
|
-
*/
|
|
12
|
-
export const SOUS_DIR_NAME = ".sous";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Primary config file names searched inside a `.sous/` directory. Exactly ONE of
|
|
16
|
-
* these may exist per `.sous/`; more than one is a hard error (no silent
|
|
17
|
-
* first-match-wins).
|
|
18
|
-
*/
|
|
19
|
-
export const CONFIG_FILE_NAMES = [
|
|
20
|
-
"sous.config.js",
|
|
21
|
-
"sous.config.mjs",
|
|
22
|
-
"sous.config.json",
|
|
23
|
-
"sous.config.jsonc",
|
|
24
|
-
"sous.config.yaml",
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The drop-in config layer directory inside `.sous/`. Every
|
|
29
|
-
* `conf.d/*.{js,mjs,json,jsonc,yaml}` file (non-recursive) is loaded after the
|
|
30
|
-
* primary config and deep-merged in bytewise filename order.
|
|
31
|
-
*/
|
|
32
|
-
export const CONFD_DIR_NAME = "conf.d";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* File extensions recognised as config layers inside `conf.d/`. A `.jsonc`
|
|
36
|
-
* layer is JSON with comments: line comments, block comments and trailing
|
|
37
|
-
* commas are all allowed in it, which is why the layers sous manages for a
|
|
38
|
-
* project are written that way.
|
|
39
|
-
*/
|
|
40
|
-
export const LAYER_EXTENSIONS = [".js", ".mjs", ".json", ".jsonc", ".yaml"] as const;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The name of the optional shared-defaults env file inside `.sous/`. This file
|
|
44
|
-
* is meant to be committed: it holds values a whole team shares, never secrets.
|
|
45
|
-
*/
|
|
46
|
-
export const ENV_DEFAULTS_NAME = ".env";
|
|
47
|
-
|
|
48
|
-
/** The name of the optional machine-specific env file inside `.sous/`. */
|
|
49
|
-
export const ENV_LOCAL_NAME = ".env.local";
|
|
50
|
-
|
|
51
|
-
/** A located sous configuration. */
|
|
52
|
-
export type DiscoveredConfig = {
|
|
53
|
-
/** Absolute path to the primary config file itself. */
|
|
54
|
-
configPath: string;
|
|
55
|
-
/**
|
|
56
|
-
* Absolute path to the `.sous/` directory holding the config, when the config
|
|
57
|
-
* was found by walking up. For an explicit `--config` path this is the config
|
|
58
|
-
* file's parent directory, whatever it is called.
|
|
59
|
-
*/
|
|
60
|
-
sousDir: string;
|
|
61
|
-
/** Absolute path to the `conf.d/` drop-in directory (may not exist). */
|
|
62
|
-
confDir: string;
|
|
63
|
-
/**
|
|
64
|
-
* Ordered absolute paths of every config layer: the primary config first,
|
|
65
|
-
* then any config layers subscribed recipes contribute, then the `conf.d/`
|
|
66
|
-
* layers in bytewise filename order. Recipes sit in the middle so a recipe can
|
|
67
|
-
* supply defaults and the project always wins over them.
|
|
68
|
-
*/
|
|
69
|
-
layerPaths: string[];
|
|
70
|
-
/** The subset of `layerPaths` that came from subscribed recipes. */
|
|
71
|
-
recipeLayerPaths: string[];
|
|
72
|
-
/**
|
|
73
|
-
* The recipe layers themselves, already read and already filtered down to the
|
|
74
|
-
* keys a recipe is allowed to set. Sous reads these instead of handing their
|
|
75
|
-
* paths to the config kernel, so a recipe cannot set a key that would change
|
|
76
|
-
* what sous trusts or what sous runs. See `repos/recipe-config-layers.ts`.
|
|
77
|
-
*/
|
|
78
|
-
recipeLayers: RecipeConfigLayer[];
|
|
79
|
-
/**
|
|
80
|
-
* Complete, plain-language sentences about anything a recipe contributed that
|
|
81
|
-
* sous declined to load. Printed by the command, since discovery runs before
|
|
82
|
-
* there is anywhere good to print.
|
|
83
|
-
*/
|
|
84
|
-
recipeLayerWarnings: string[];
|
|
85
|
-
/** How the config was located. */
|
|
86
|
-
source: "flag" | "walk-up";
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Returns the list of directories to check, starting at `startDir` and walking
|
|
91
|
-
* up to the filesystem root. Used by discovery and by the not-found error message.
|
|
92
|
-
*/
|
|
93
|
-
export function candidateDirs(startDir: string): string[] {
|
|
94
|
-
const dirs: string[] = [];
|
|
95
|
-
let current = path.resolve(startDir);
|
|
96
|
-
|
|
97
|
-
for (;;) {
|
|
98
|
-
dirs.push(current);
|
|
99
|
-
const parent = path.dirname(current);
|
|
100
|
-
if (parent === current) break;
|
|
101
|
-
current = parent;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return dirs;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Returns the primary config file inside `sousDir`, or null when none exists.
|
|
109
|
-
*
|
|
110
|
-
* @throws ConfigError when MORE THAN ONE primary candidate exists — sous never
|
|
111
|
-
* silently picks one of several `sous.config.*` files.
|
|
112
|
-
*/
|
|
113
|
-
export function findConfigInSousDir(sousDir: string): string | null {
|
|
114
|
-
const found: string[] = [];
|
|
115
|
-
for (const name of CONFIG_FILE_NAMES) {
|
|
116
|
-
const candidate = path.join(sousDir, name);
|
|
117
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
118
|
-
found.push(candidate);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (found.length > 1) {
|
|
123
|
-
throw new ConfigError(
|
|
124
|
-
`Multiple primary sous config files found in ${sousDir}:\n` +
|
|
125
|
-
found.map((f) => ` ${f}`).join("\n") +
|
|
126
|
-
`\n A .sous/ directory may hold exactly one of: ${CONFIG_FILE_NAMES.join(", ")}.\n` +
|
|
127
|
-
` Keep one primary config and move the rest into ${CONFD_DIR_NAME}/ (with unique names) or delete them.`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return found[0] ?? null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Compares two strings bytewise (plain `<` on the string), locale-independent
|
|
136
|
-
* so layer order is identical on every machine. Note this is NOT numeric:
|
|
137
|
-
* `10-` sorts before `2-` (zero-pad layer prefixes if that matters).
|
|
138
|
-
*/
|
|
139
|
-
function bytewiseCompare(a: string, b: string): number {
|
|
140
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Lists the config layer files inside a `conf.d/` directory: every
|
|
145
|
-
* `*.{js,mjs,json,jsonc,yaml}` file directly inside it (non-recursive), sorted
|
|
146
|
-
* bytewise by filename. A missing directory yields an empty list.
|
|
147
|
-
*/
|
|
148
|
-
export function listConfDirLayers(confDir: string): string[] {
|
|
149
|
-
if (!fs.existsSync(confDir) || !fs.statSync(confDir).isDirectory()) return [];
|
|
150
|
-
|
|
151
|
-
return fs
|
|
152
|
-
.readdirSync(confDir)
|
|
153
|
-
.filter((name) => {
|
|
154
|
-
const ext = path.extname(name).toLowerCase();
|
|
155
|
-
if (!(LAYER_EXTENSIONS as readonly string[]).includes(ext)) return false;
|
|
156
|
-
const full = path.join(confDir, name);
|
|
157
|
-
return fs.statSync(full).isFile();
|
|
158
|
-
})
|
|
159
|
-
.sort(bytewiseCompare)
|
|
160
|
-
.map((name) => path.join(confDir, name));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Asserts that every loaded config file (primary + conf.d layers) has a unique
|
|
165
|
-
* baseName; the filename minus its FINAL extension. Two layers named
|
|
166
|
-
* `500-repos.json` and `500-repos.jsonc` would otherwise merge in an order that
|
|
167
|
-
* depends on their extensions, which is never what the author meant. It is also
|
|
168
|
-
* what stops a managed layer from existing under both its old `.json` name and
|
|
169
|
-
* its `.jsonc` one.
|
|
170
|
-
*
|
|
171
|
-
* @throws ConfigError naming both conflicting files.
|
|
172
|
-
*/
|
|
173
|
-
export function assertUniqueLayerBaseNames(layerPaths: string[]): void {
|
|
174
|
-
const seen = new Map<string, string>();
|
|
175
|
-
for (const layerPath of layerPaths) {
|
|
176
|
-
const base = path.basename(layerPath, path.extname(layerPath));
|
|
177
|
-
const existing = seen.get(base);
|
|
178
|
-
if (existing !== undefined) {
|
|
179
|
-
throw new ConfigError(
|
|
180
|
-
`Duplicate config layer baseName '${base}':\n` +
|
|
181
|
-
` ${existing}\n` +
|
|
182
|
-
` ${layerPath}\n` +
|
|
183
|
-
` Every loaded config file (the primary config and all ${CONFD_DIR_NAME}/ layers) must have a\n` +
|
|
184
|
-
` unique filename once its final extension is removed. Rename one of them.`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
seen.set(base, layerPath);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Builds a full DiscoveredConfig from a located primary config: computes the
|
|
193
|
-
* conf.d directory, enumerates its layers and the layers subscribed recipes
|
|
194
|
-
* contribute, and runs the duplicate-baseName check.
|
|
195
|
-
*
|
|
196
|
-
* Recipe layers load after the primary config and before the `conf.d/` layers,
|
|
197
|
-
* so a recipe supplies defaults and the project always wins over them. They are
|
|
198
|
-
* left out of the duplicate-baseName check deliberately: that check exists so a
|
|
199
|
-
* person never has to guess which of two files they wrote merges last, and a
|
|
200
|
-
* recipe's file names are not theirs to rename.
|
|
201
|
-
*
|
|
202
|
-
* @param confDirOverride - Absolute path to use as the conf.d directory instead
|
|
203
|
-
* of `<sousDir>/conf.d`. Set from the `--sous-confd` flag or `SOUS_CONFD` env
|
|
204
|
-
* var; discovery then builds its layers from the overridden directory.
|
|
205
|
-
*/
|
|
206
|
-
function buildDiscoveredConfig(
|
|
207
|
-
configPath: string,
|
|
208
|
-
sousDir: string,
|
|
209
|
-
source: DiscoveredConfig["source"],
|
|
210
|
-
confDirOverride?: string
|
|
211
|
-
): DiscoveredConfig {
|
|
212
|
-
const confDir = confDirOverride ?? path.join(sousDir, CONFD_DIR_NAME);
|
|
213
|
-
const projectLayers = [configPath, ...listConfDirLayers(confDir)];
|
|
214
|
-
assertUniqueLayerBaseNames(projectLayers);
|
|
215
|
-
|
|
216
|
-
const recipes = listRecipeConfigLayers(sousDir);
|
|
217
|
-
const layerPaths = [
|
|
218
|
-
configPath,
|
|
219
|
-
...recipes.paths,
|
|
220
|
-
...projectLayers.slice(1),
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
configPath,
|
|
225
|
-
sousDir,
|
|
226
|
-
confDir,
|
|
227
|
-
layerPaths,
|
|
228
|
-
recipeLayerPaths: recipes.paths,
|
|
229
|
-
recipeLayers: recipes.layers,
|
|
230
|
-
recipeLayerWarnings: recipes.warnings,
|
|
231
|
-
source,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Re-runs the conf.d enumeration, the recipe layer enumeration and the
|
|
237
|
-
* duplicate-baseName check for an existing discovery.
|
|
238
|
-
*
|
|
239
|
-
* Two callers need it. Watch mode calls it because layer files can appear or
|
|
240
|
-
* disappear while watching. `BaseCommand.init()` calls it once after the
|
|
241
|
-
* `.sous/` env files are loaded, because `SOUS_HOME` is file-settable and it
|
|
242
|
-
* decides where the store holding the recipe layers is.
|
|
243
|
-
*
|
|
244
|
-
* The existing `confDir` is preserved (not recomputed from `sousDir`), so a
|
|
245
|
-
* `SOUS_CONFD` / `--sous-confd` override survives across watch reloads.
|
|
246
|
-
*/
|
|
247
|
-
export function refreshDiscoveredConfig(discovered: DiscoveredConfig): DiscoveredConfig {
|
|
248
|
-
return buildDiscoveredConfig(
|
|
249
|
-
discovered.configPath,
|
|
250
|
-
discovered.sousDir,
|
|
251
|
-
discovered.source,
|
|
252
|
-
discovered.confDir
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Walks up from `startDir` looking for a `.sous/` directory that contains one of
|
|
258
|
-
* CONFIG_FILE_NAMES. The first directory with a match wins; a `.sous/` directory
|
|
259
|
-
* without a config file does not stop the walk.
|
|
260
|
-
*
|
|
261
|
-
* @param startDir - Directory to start from (normally `process.cwd()`).
|
|
262
|
-
* @param confDirOverride - Absolute conf.d directory to use instead of
|
|
263
|
-
* `<sousDir>/conf.d` (from `--sous-confd` / `SOUS_CONFD`).
|
|
264
|
-
* @returns The discovered config, or null when nothing was found.
|
|
265
|
-
* @throws ConfigError when a `.sous/` holds several primary configs, or when
|
|
266
|
-
* loaded layer baseNames collide.
|
|
267
|
-
*/
|
|
268
|
-
export function discoverConfig(
|
|
269
|
-
startDir: string = process.cwd(),
|
|
270
|
-
confDirOverride?: string
|
|
271
|
-
): DiscoveredConfig | null {
|
|
272
|
-
for (const dir of candidateDirs(startDir)) {
|
|
273
|
-
const sousDir = path.join(dir, SOUS_DIR_NAME);
|
|
274
|
-
if (!fs.existsSync(sousDir) || !fs.statSync(sousDir).isDirectory()) continue;
|
|
275
|
-
|
|
276
|
-
const configPath = findConfigInSousDir(sousDir);
|
|
277
|
-
if (configPath) return buildDiscoveredConfig(configPath, sousDir, "walk-up", confDirOverride);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Resolves an explicit `--config` value into a DiscoveredConfig.
|
|
285
|
-
*
|
|
286
|
-
* The value may point at a config file or at a directory. A directory is searched
|
|
287
|
-
* for CONFIG_FILE_NAMES, and if the directory itself is not named `.sous`, its
|
|
288
|
-
* `.sous/` child is searched too — so `--config .` works inside a project root.
|
|
289
|
-
*
|
|
290
|
-
* @param configFlag - The raw `--config` value (relative paths resolve against cwd).
|
|
291
|
-
* @param cwd - Base directory for relative paths.
|
|
292
|
-
* @param confDirOverride - Absolute conf.d directory to use instead of
|
|
293
|
-
* `<sousDir>/conf.d` (from `--sous-confd` / `SOUS_CONFD`).
|
|
294
|
-
* @param sourceLabel - How the caller supplied the value (`--config`,
|
|
295
|
-
* `--sous-config`, `SOUS_CONFIG`, `--sous-dir`, `SOUS_DIR`). Used only in error
|
|
296
|
-
* messages so a user who set `SOUS_DIR` is not told to fix `--config`.
|
|
297
|
-
* @returns The resolved config.
|
|
298
|
-
* @throws When the path does not exist or holds no recognised config file.
|
|
299
|
-
*/
|
|
300
|
-
export function resolveConfigFlag(
|
|
301
|
-
configFlag: string,
|
|
302
|
-
cwd: string = process.cwd(),
|
|
303
|
-
confDirOverride?: string,
|
|
304
|
-
sourceLabel = "--config"
|
|
305
|
-
): DiscoveredConfig {
|
|
306
|
-
const resolved = path.resolve(cwd, expandHome(configFlag));
|
|
307
|
-
|
|
308
|
-
if (!fs.existsSync(resolved)) {
|
|
309
|
-
throw new Error(`${sourceLabel} path not found: ${resolved}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (fs.statSync(resolved).isDirectory()) {
|
|
313
|
-
const direct = findConfigInSousDir(resolved);
|
|
314
|
-
if (direct) {
|
|
315
|
-
return buildDiscoveredConfig(direct, resolved, "flag", confDirOverride);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const nested = path.join(resolved, SOUS_DIR_NAME);
|
|
319
|
-
if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
|
|
320
|
-
const nestedConfig = findConfigInSousDir(nested);
|
|
321
|
-
if (nestedConfig) {
|
|
322
|
-
return buildDiscoveredConfig(nestedConfig, nested, "flag", confDirOverride);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
throw new Error(
|
|
327
|
-
`${sourceLabel} points at a directory with no sous config file: ${resolved}\n` +
|
|
328
|
-
`Looked for: ${CONFIG_FILE_NAMES.join(", ")}`
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return buildDiscoveredConfig(resolved, path.dirname(resolved), "flag", confDirOverride);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Expands a leading `~` to the user's home directory. Leaves other paths alone.
|
|
337
|
-
*/
|
|
338
|
-
export function expandHome(inputPath: string): string {
|
|
339
|
-
if (inputPath === "~") return process.env.HOME ?? inputPath;
|
|
340
|
-
if (inputPath.startsWith("~/")) {
|
|
341
|
-
const home = process.env.HOME;
|
|
342
|
-
if (home) return path.join(home, inputPath.slice(2));
|
|
343
|
-
}
|
|
344
|
-
return inputPath;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Builds the message shown when no config could be found. Names every directory
|
|
349
|
-
* that was checked and shows how to fix it.
|
|
350
|
-
*
|
|
351
|
-
* @param startDir - The directory discovery started from.
|
|
352
|
-
*/
|
|
353
|
-
export function formatNotFoundMessage(startDir: string = process.cwd()): string {
|
|
354
|
-
const dirs = candidateDirs(startDir);
|
|
355
|
-
const shown = dirs.slice(0, 8);
|
|
356
|
-
const omitted = dirs.length - shown.length;
|
|
357
|
-
|
|
358
|
-
const checked = shown
|
|
359
|
-
.map((dir) => ` ${path.join(dir, SOUS_DIR_NAME)}/`)
|
|
360
|
-
.join("\n");
|
|
361
|
-
|
|
362
|
-
const more = omitted > 0 ? `\n ... and ${omitted} more parent director${omitted === 1 ? "y" : "ies"}` : "";
|
|
363
|
-
|
|
364
|
-
return [
|
|
365
|
-
"No sous config found.",
|
|
366
|
-
"",
|
|
367
|
-
`Starting at ${startDir}, sous walked up looking for a ${SOUS_DIR_NAME}/ directory`,
|
|
368
|
-
`containing one of: ${CONFIG_FILE_NAMES.join(", ")}`,
|
|
369
|
-
"",
|
|
370
|
-
" Checked:",
|
|
371
|
-
checked + more,
|
|
372
|
-
"",
|
|
373
|
-
" To fix this, either:",
|
|
374
|
-
` 1. Set the project up: run 'sous init' in its root directory, which writes`,
|
|
375
|
-
` ${SOUS_DIR_NAME}/${CONFIG_FILE_NAMES[0]} and everything else a first build needs, or`,
|
|
376
|
-
" 2. Pass the config explicitly: sous <command> --config <path>",
|
|
377
|
-
].join("\n");
|
|
378
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { styleText } from "node:util";
|
|
2
|
-
import { ConfigError } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared helpers for the `sous config *` inspection commands: dot-path lookup
|
|
6
|
-
* with `[n]` array indexing, colorized pretty-JSON rendering, and value
|
|
7
|
-
* truncation for the `--layers` provenance view.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Sentinel returned by lookupPath when a path segment does not exist. */
|
|
11
|
-
export const NOT_FOUND = Symbol("sous.config.path.not-found");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Splits a dot-path with optional `[n]` array indices into ordered segments.
|
|
15
|
-
* `compilation.targets[0].entryPoint` → `["compilation", "targets", 0, "entryPoint"]`.
|
|
16
|
-
* Numeric-looking bracket segments become numbers; everything else is a string key.
|
|
17
|
-
*
|
|
18
|
-
* @throws ConfigError on a malformed path (empty, unbalanced brackets, etc.).
|
|
19
|
-
*/
|
|
20
|
-
export function parsePath(path: string): (string | number)[] {
|
|
21
|
-
const trimmed = path.trim();
|
|
22
|
-
if (trimmed === "") {
|
|
23
|
-
throw new ConfigError("Empty config path. Provide a dot-path like 'compilation.targets[0].entryPoint'.");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const segments: (string | number)[] = [];
|
|
27
|
-
// Consume, in order: bare keys, `[n]` array indices, and `.` separators
|
|
28
|
-
// between them. Any character that fits none of these (e.g. `a..b`, `a[]`,
|
|
29
|
-
// `a[x]`) leaves a gap the scanner reports as malformed.
|
|
30
|
-
const tokenRe = /([^.[\]]+)|\[(\d+)\]|(\.)/g;
|
|
31
|
-
let lastIndex = 0;
|
|
32
|
-
let expectKey = false; // a `.` was just consumed, so a key must follow
|
|
33
|
-
let match: RegExpExecArray | null;
|
|
34
|
-
|
|
35
|
-
while ((match = tokenRe.exec(trimmed)) !== null) {
|
|
36
|
-
if (match.index !== lastIndex) {
|
|
37
|
-
throw new ConfigError(`Malformed config path near '${trimmed.slice(lastIndex)}' in '${path}'.`);
|
|
38
|
-
}
|
|
39
|
-
if (match[1] !== undefined) {
|
|
40
|
-
segments.push(match[1]);
|
|
41
|
-
expectKey = false;
|
|
42
|
-
} else if (match[2] !== undefined) {
|
|
43
|
-
if (expectKey) {
|
|
44
|
-
throw new ConfigError(`Malformed config path near '${trimmed.slice(match.index)}' in '${path}'.`);
|
|
45
|
-
}
|
|
46
|
-
segments.push(Number(match[2]));
|
|
47
|
-
} else {
|
|
48
|
-
// A `.` separator: a key must follow it, and one may not lead the path or
|
|
49
|
-
// directly follow another `.` (i.e. `.a`, `a..b` are malformed).
|
|
50
|
-
if (expectKey || segments.length === 0) {
|
|
51
|
-
throw new ConfigError(`Malformed config path near '${trimmed.slice(match.index)}' in '${path}'.`);
|
|
52
|
-
}
|
|
53
|
-
expectKey = true;
|
|
54
|
-
}
|
|
55
|
-
lastIndex = tokenRe.lastIndex;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (lastIndex !== trimmed.length || expectKey) {
|
|
59
|
-
throw new ConfigError(`Malformed config path near '${trimmed.slice(lastIndex) || "end of path"}' in '${path}'.`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return segments;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Walks `root` following `segments`, returning the value found or NOT_FOUND when
|
|
67
|
-
* any segment is missing (or the path descends into a non-indexable value).
|
|
68
|
-
*/
|
|
69
|
-
export function lookupPath(root: unknown, segments: (string | number)[]): unknown {
|
|
70
|
-
let current: unknown = root;
|
|
71
|
-
for (const segment of segments) {
|
|
72
|
-
if (current === null || current === undefined || typeof current !== "object") {
|
|
73
|
-
return NOT_FOUND;
|
|
74
|
-
}
|
|
75
|
-
if (typeof segment === "number") {
|
|
76
|
-
if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
|
|
77
|
-
return NOT_FOUND;
|
|
78
|
-
}
|
|
79
|
-
current = current[segment];
|
|
80
|
-
} else {
|
|
81
|
-
// Use hasOwnProperty, NOT the `in` operator: `in` walks the prototype
|
|
82
|
-
// chain, so inherited Object.prototype members (`constructor`, `toString`,
|
|
83
|
-
// `hasOwnProperty`, `__proto__`, …) would falsely "resolve" and print
|
|
84
|
-
// garbage instead of NOT_FOUND.
|
|
85
|
-
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
86
|
-
return NOT_FOUND;
|
|
87
|
-
}
|
|
88
|
-
current = (current as Record<string, unknown>)[segment];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return current;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** True for JSON scalar values (string, finite/any number, boolean, null). */
|
|
95
|
-
export function isScalar(value: unknown): value is string | number | boolean | null {
|
|
96
|
-
return (
|
|
97
|
-
value === null ||
|
|
98
|
-
typeof value === "string" ||
|
|
99
|
-
typeof value === "number" ||
|
|
100
|
-
typeof value === "boolean"
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Colorizes an already-serialized pretty-JSON string using node:util styleText:
|
|
106
|
-
* object keys cyan, string values green, numbers yellow, booleans/null magenta.
|
|
107
|
-
* Operates purely on JSON token syntax, so it never mangles structure.
|
|
108
|
-
*
|
|
109
|
-
* Callers should only invoke this when writing to a TTY; piped output must stay
|
|
110
|
-
* plain so it parses as JSON.
|
|
111
|
-
*/
|
|
112
|
-
export function colorizeJson(json: string): string {
|
|
113
|
-
const tokenRe = /"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g;
|
|
114
|
-
return json.replace(tokenRe, (match, offset: number, whole: string) => {
|
|
115
|
-
if (match[0] === '"') {
|
|
116
|
-
const rest = whole.slice(offset + match.length);
|
|
117
|
-
// A string immediately followed by a colon is an object key.
|
|
118
|
-
return /^\s*:/.test(rest)
|
|
119
|
-
? styleText("cyan", match)
|
|
120
|
-
: styleText("green", match);
|
|
121
|
-
}
|
|
122
|
-
if (match === "true" || match === "false" || match === "null") {
|
|
123
|
-
return styleText("magenta", match);
|
|
124
|
-
}
|
|
125
|
-
return styleText("yellow", match);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Renders a value as pretty JSON (2-space), colorized when `useColor` is true.
|
|
131
|
-
*/
|
|
132
|
-
export function renderJson(value: unknown, useColor: boolean): string {
|
|
133
|
-
const json = JSON.stringify(value, null, 2);
|
|
134
|
-
return useColor ? colorizeJson(json) : json;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* JSON-encodes a value on a single line and truncates it to `max` characters
|
|
139
|
-
* (with an ellipsis) for the compact `--layers` provenance lines.
|
|
140
|
-
*/
|
|
141
|
-
export function truncateJson(value: unknown, max = 80): string {
|
|
142
|
-
const encoded = JSON.stringify(value);
|
|
143
|
-
if (encoded === undefined) return "undefined";
|
|
144
|
-
return encoded.length > max ? `${encoded.slice(0, max - 1)}…` : encoded;
|
|
145
|
-
}
|