@sous-io/sous 0.1.0 → 0.2.0
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 +121 -35
- package/bin/run.js +10 -1
- package/docs/markdown/README.md +27 -0
- package/docs/markdown/_sidebar.md +18 -0
- package/docs/markdown/commands.md +308 -0
- package/docs/markdown/config-discovery.md +74 -0
- package/docs/markdown/config-inspection.md +69 -0
- package/docs/markdown/config-layers.md +92 -0
- package/docs/markdown/config-variables.md +79 -0
- package/docs/markdown/configuration.md +71 -0
- package/docs/markdown/design-principles.md +59 -0
- package/docs/markdown/repositories-authoring.md +408 -0
- package/docs/markdown/repositories-consuming.md +580 -0
- package/docs/markdown/repositories-file-formats.md +1084 -0
- package/docs/markdown/repositories-variables.md +387 -0
- package/docs/markdown/repositories.md +303 -0
- package/docs/markdown/skill-categories.md +58 -0
- package/package.json +73 -9
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
- package/sous.config.schema.json +337 -0
- package/src/base-command.ts +220 -67
- package/src/commands/build.ts +150 -73
- package/src/commands/clear.ts +23 -15
- package/src/commands/compile.ts +74 -16
- package/src/commands/config/get.ts +110 -0
- package/src/commands/config/show.ts +32 -0
- package/src/commands/config/validate.ts +53 -0
- package/src/commands/help.ts +46 -0
- package/src/commands/launch.ts +36 -14
- package/src/commands/lock/rebuild.ts +241 -0
- package/src/commands/lock/show.ts +115 -0
- package/src/commands/namespace/list.ts +117 -0
- package/src/commands/namespace/show.ts +110 -0
- package/src/commands/prune.ts +3 -11
- package/src/commands/recipe/list.ts +95 -0
- package/src/commands/recipe/show.ts +301 -0
- package/src/commands/repo/add.ts +145 -0
- package/src/commands/repo/gc.ts +172 -0
- package/src/commands/repo/init.ts +136 -0
- package/src/commands/repo/link.ts +500 -0
- package/src/commands/repo/list.ts +179 -0
- package/src/commands/repo/release.ts +619 -0
- package/src/commands/repo/remove.ts +193 -0
- package/src/commands/repo/search.ts +189 -0
- package/src/commands/repo/submit.ts +133 -0
- package/src/commands/repo/unlink.ts +147 -0
- package/src/commands/subscription/add.ts +285 -0
- package/src/commands/subscription/list.ts +129 -0
- package/src/commands/subscription/remove.ts +181 -0
- package/src/commands/vars/ask.ts +374 -0
- package/src/commands/vars/index.ts +79 -0
- package/src/commands/vars/list.ts +67 -0
- package/src/commands/vars/show.ts +77 -0
- package/src/config-command.ts +30 -0
- package/src/lib/build-service.ts +206 -54
- package/src/lib/config-discovery.ts +220 -27
- package/src/lib/config-inspect.ts +145 -0
- package/src/lib/config-kernel.mjs +377 -0
- package/src/lib/config-schema.ts +361 -0
- package/src/lib/env-file.ts +328 -0
- package/src/lib/env-local.ts +18 -1
- package/src/lib/errors.ts +32 -0
- package/src/lib/include-resolver.ts +108 -15
- package/src/lib/interactive.ts +165 -0
- package/src/lib/markdown-compiler.ts +118 -37
- package/src/lib/package-info.ts +25 -0
- package/src/lib/pid-service.ts +32 -21
- package/src/lib/refs/find.ts +589 -0
- package/src/lib/refs/index.ts +12 -0
- package/src/lib/refs/pick.ts +147 -0
- package/src/lib/refs/scopes.ts +61 -0
- package/src/lib/repos/catalog-display.ts +116 -0
- package/src/lib/repos/catalog-inputs.ts +160 -0
- package/src/lib/repos/catalog.ts +722 -0
- package/src/lib/repos/core-recipe.ts +105 -0
- package/src/lib/repos/defaults.ts +175 -0
- package/src/lib/repos/formats/common.ts +389 -0
- package/src/lib/repos/formats/index-file.ts +215 -0
- package/src/lib/repos/formats/links-map.ts +96 -0
- package/src/lib/repos/formats/lockfile.ts +167 -0
- package/src/lib/repos/formats/patterns.ts +57 -0
- package/src/lib/repos/formats/recipe-manifest.ts +395 -0
- package/src/lib/repos/formats/repo-manifest.ts +88 -0
- package/src/lib/repos/formats/store-entry.ts +84 -0
- package/src/lib/repos/freshness.ts +208 -0
- package/src/lib/repos/git-clone.ts +312 -0
- package/src/lib/repos/identity.ts +89 -0
- package/src/lib/repos/index.ts +58 -0
- package/src/lib/repos/links.ts +353 -0
- package/src/lib/repos/load-manifest.ts +236 -0
- package/src/lib/repos/lock-service.ts +453 -0
- package/src/lib/repos/locked-namespace-resolver.ts +90 -0
- package/src/lib/repos/locked-recipes.ts +254 -0
- package/src/lib/repos/managed-layer.ts +422 -0
- package/src/lib/repos/namespace-resolver.ts +370 -0
- package/src/lib/repos/providers/base.ts +206 -0
- package/src/lib/repos/providers/git.ts +233 -0
- package/src/lib/repos/providers/github.ts +294 -0
- package/src/lib/repos/providers/gitlab.ts +263 -0
- package/src/lib/repos/providers/http.ts +102 -0
- package/src/lib/repos/providers/index-cache.ts +382 -0
- package/src/lib/repos/providers/index.ts +106 -0
- package/src/lib/repos/providers/local.ts +391 -0
- package/src/lib/repos/providers/provider.ts +401 -0
- package/src/lib/repos/recipe-config-layers.ts +287 -0
- package/src/lib/repos/recipe-targets.ts +223 -0
- package/src/lib/repos/ref-search.ts +46 -0
- package/src/lib/repos/ref.ts +513 -0
- package/src/lib/repos/reference-report.ts +122 -0
- package/src/lib/repos/release/bump.ts +161 -0
- package/src/lib/repos/release/git-state.ts +305 -0
- package/src/lib/repos/release/index-builder.ts +635 -0
- package/src/lib/repos/release/index.ts +16 -0
- package/src/lib/repos/release/plan.ts +512 -0
- package/src/lib/repos/release/submit-service.ts +496 -0
- package/src/lib/repos/release/tags.ts +243 -0
- package/src/lib/repos/release/validate.ts +463 -0
- package/src/lib/repos/resolver.ts +789 -0
- package/src/lib/repos/scaffold/index.ts +238 -0
- package/src/lib/repos/scaffold/templates.ts +413 -0
- package/src/lib/repos/seed.ts +414 -0
- package/src/lib/repos/store/contract.ts +64 -0
- package/src/lib/repos/store/hash.ts +114 -0
- package/src/lib/repos/store/recipe-store.ts +599 -0
- package/src/lib/repos/store/settings.ts +58 -0
- package/src/lib/repos/subscription-service.ts +2678 -0
- package/src/lib/repos/trust.ts +447 -0
- package/src/lib/settings.ts +546 -189
- package/src/lib/sous-home.ts +104 -0
- package/src/lib/state.ts +52 -20
- package/src/lib/vars/ask.ts +1152 -0
- package/src/lib/vars/definition-source.ts +252 -0
- package/src/lib/vars/display.ts +233 -0
- package/src/lib/vars/index.ts +18 -0
- package/src/lib/vars/ladder.ts +282 -0
- package/src/lib/vars/mappings.ts +265 -0
- package/src/lib/vars/names.ts +94 -0
- package/src/lib/vars/preanswers.ts +395 -0
- package/src/lib/vars/question-plan.ts +218 -0
- package/src/lib/vars/report.ts +228 -0
- package/src/lib/vars/safe-regex.ts +235 -0
- package/src/lib/vars/validate.ts +312 -0
- package/src/lib/watch-loop.ts +148 -0
- package/src/templating/init-liquid-engine.ts +58 -16
- package/src/utils/choice-prompt.ts +143 -0
- package/src/utils/command-errors.ts +186 -0
- package/src/utils/command-help.ts +45 -0
- package/src/utils/confirm-prompt.ts +110 -0
- package/src/utils/flags.ts +153 -0
- package/src/utils/formatting.ts +540 -55
- package/src/utils/prompts.ts +35 -1
- package/src/utils/sous-directory.ts +245 -0
- package/src/utils/table.ts +603 -0
- package/src/utils/value-prompt.ts +119 -0
- package/bin/xcv +0 -5
- package/shared-prompts/_partials/resume-task.md +0 -51
- package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
- package/shared-prompts/_partials/update-task-file.md +0 -52
- package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
- package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
- package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
- package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
- package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
- package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
- package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
- package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
- package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
- package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
- package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
- package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
- package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolution ladder: how sous decides whether a variable already has an
|
|
3
|
+
* answer, and which environment variable supplied it.
|
|
4
|
+
*
|
|
5
|
+
* Five rungs, most specific first:
|
|
6
|
+
*
|
|
7
|
+
* 1. mapping a record binding an arbitrary name to this exact variable
|
|
8
|
+
* 2. recipe SOUS_VAR_<NAMESPACE>_<RECIPE>_<VARIABLE>
|
|
9
|
+
* 3. namespace SOUS_VAR_<NAMESPACE>_<VARIABLE>
|
|
10
|
+
* 4. shared SOUS_VAR_<VARIABLE>
|
|
11
|
+
* 5. bare the definition's own `env` name, or the shared form
|
|
12
|
+
*
|
|
13
|
+
* Within a rung the real shell environment wins, then `.sous/.env.local`, then
|
|
14
|
+
* `.sous/.env`; the same order the env file loader uses, so what the ladder
|
|
15
|
+
* reports is what a build actually sees.
|
|
16
|
+
*
|
|
17
|
+
* The three sources are read separately rather than off `process.env`, because
|
|
18
|
+
* by the time a command runs the env files have already been injected into the
|
|
19
|
+
* process environment and the distinction would be lost.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { ENV_DEFAULTS_NAME, ENV_LOCAL_NAME } from "../config-discovery.js";
|
|
24
|
+
import { readEnvFileMap } from "../env-local.js";
|
|
25
|
+
import type { Settings } from "../settings.js";
|
|
26
|
+
import type { DefinedVariable } from "./definition-source.js";
|
|
27
|
+
import { mappedNamesFor } from "./mappings.js";
|
|
28
|
+
import {
|
|
29
|
+
bareName,
|
|
30
|
+
namespaceScopedName,
|
|
31
|
+
recipeScopedName,
|
|
32
|
+
sharedName,
|
|
33
|
+
} from "./names.js";
|
|
34
|
+
|
|
35
|
+
/** The rungs of the ladder, most specific first. */
|
|
36
|
+
export const LADDER_RUNGS = ["mapping", "recipe", "namespace", "shared", "bare"] as const;
|
|
37
|
+
|
|
38
|
+
/** One rung of the ladder. */
|
|
39
|
+
export type LadderRung = (typeof LADDER_RUNGS)[number];
|
|
40
|
+
|
|
41
|
+
/** Plain-language names for each rung, used in output. */
|
|
42
|
+
export const RUNG_LABELS: Record<LadderRung, string> = {
|
|
43
|
+
mapping: "mapping record",
|
|
44
|
+
recipe: "recipe scope",
|
|
45
|
+
namespace: "namespace scope",
|
|
46
|
+
shared: "shared scope",
|
|
47
|
+
bare: "declared name",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Where a value was found: the real environment, or one of the two env files. */
|
|
51
|
+
export type EnvSourceFile = "shell" | typeof ENV_LOCAL_NAME | typeof ENV_DEFAULTS_NAME;
|
|
52
|
+
|
|
53
|
+
/** Plain-language names for each place a value can come from. */
|
|
54
|
+
export const SOURCE_LABELS: Record<string, string> = {
|
|
55
|
+
shell: "the shell environment",
|
|
56
|
+
[ENV_LOCAL_NAME]: `the ${ENV_LOCAL_NAME} file`,
|
|
57
|
+
[ENV_DEFAULTS_NAME]: `the ${ENV_DEFAULTS_NAME} file`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** One environment variable name the ladder will look up, and why. */
|
|
61
|
+
export interface LadderCandidate {
|
|
62
|
+
/** Which rung generated (or recorded) the name. */
|
|
63
|
+
rung: LadderRung;
|
|
64
|
+
/** The environment variable name. */
|
|
65
|
+
envName: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Where a resolved value came from. */
|
|
69
|
+
export interface VariableSource extends LadderCandidate {
|
|
70
|
+
/** Which of the three layers held the value. */
|
|
71
|
+
file: EnvSourceFile;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A resolved answer: the value, and exactly where it came from. */
|
|
75
|
+
export interface ResolvedVariable {
|
|
76
|
+
/** The value as stored, before validation or coercion. */
|
|
77
|
+
value: string;
|
|
78
|
+
/** Which name, on which rung, in which layer, supplied it. */
|
|
79
|
+
source: VariableSource;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The three separately parsed environment layers plus the mapping records, all
|
|
84
|
+
* the ladder needs to answer a lookup.
|
|
85
|
+
*/
|
|
86
|
+
export interface LadderContext {
|
|
87
|
+
/** The real shell environment, captured before the env files were injected. */
|
|
88
|
+
shellEnv: Record<string, string>;
|
|
89
|
+
/** The parsed `.sous/.env.local` file. */
|
|
90
|
+
localEnv: Record<string, string>;
|
|
91
|
+
/** The parsed `.sous/.env` file. */
|
|
92
|
+
sharedEnv: Record<string, string>;
|
|
93
|
+
/** The merged `varMappings` block: environment variable name to target. */
|
|
94
|
+
mappings: Record<string, string>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Drops undefined entries from a process environment, keeping the strings. */
|
|
98
|
+
function sanitizeEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
|
99
|
+
const out: Record<string, string> = {};
|
|
100
|
+
for (const [key, value] of Object.entries(env)) {
|
|
101
|
+
if (typeof value === "string") out[key] = value;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What `loadLadderContext` needs in order to read the layers. */
|
|
107
|
+
export interface LadderContextOptions {
|
|
108
|
+
/** The project's `.sous/` directory, which holds both env files. */
|
|
109
|
+
sousDir: string;
|
|
110
|
+
/** The merged config, read for its `varMappings` block. */
|
|
111
|
+
settings?: Settings;
|
|
112
|
+
/**
|
|
113
|
+
* The real shell environment. Commands pass the snapshot BaseCommand takes
|
|
114
|
+
* before it injects the env files; without it the file-supplied values would
|
|
115
|
+
* be reported as though the shell had set them.
|
|
116
|
+
*/
|
|
117
|
+
shellEnv?: NodeJS.ProcessEnv;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reads the two env files and the mapping records into a ladder context.
|
|
122
|
+
*
|
|
123
|
+
* @param options - Where to read from; see LadderContextOptions.
|
|
124
|
+
*/
|
|
125
|
+
export function loadLadderContext(options: LadderContextOptions): LadderContext {
|
|
126
|
+
const { sousDir, settings, shellEnv = process.env } = options;
|
|
127
|
+
return {
|
|
128
|
+
shellEnv: sanitizeEnv(shellEnv),
|
|
129
|
+
localEnv: readEnvFileMap(path.join(sousDir, ENV_LOCAL_NAME)),
|
|
130
|
+
sharedEnv: readEnvFileMap(path.join(sousDir, ENV_DEFAULTS_NAME)),
|
|
131
|
+
mappings: settings?.varMappings ?? {},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every environment variable name that could answer this variable, most
|
|
137
|
+
* specific rung first. Duplicate names are dropped, keeping the most specific
|
|
138
|
+
* occurrence, so a definition whose `env` field repeats a generated name is
|
|
139
|
+
* reported once.
|
|
140
|
+
*
|
|
141
|
+
* @param defined - The definition and the recipe that published it.
|
|
142
|
+
* @param context - The environment layers and mapping records.
|
|
143
|
+
*/
|
|
144
|
+
export function variableCandidates(
|
|
145
|
+
defined: DefinedVariable,
|
|
146
|
+
context: LadderContext
|
|
147
|
+
): LadderCandidate[] {
|
|
148
|
+
const { namespace, name: recipe } = defined.recipe;
|
|
149
|
+
const variable = defined.definition.name;
|
|
150
|
+
|
|
151
|
+
const ordered: LadderCandidate[] = [
|
|
152
|
+
...mappedNamesFor(context.mappings, defined).map((envName) => ({
|
|
153
|
+
rung: "mapping" as const,
|
|
154
|
+
envName,
|
|
155
|
+
})),
|
|
156
|
+
{ rung: "recipe", envName: recipeScopedName(namespace, recipe, variable) },
|
|
157
|
+
{ rung: "namespace", envName: namespaceScopedName(namespace, variable) },
|
|
158
|
+
{ rung: "shared", envName: sharedName(variable) },
|
|
159
|
+
{ rung: "bare", envName: bareName(defined.definition) },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const seen = new Set<string>();
|
|
163
|
+
return ordered.filter((candidate) => {
|
|
164
|
+
if (seen.has(candidate.envName)) return false;
|
|
165
|
+
seen.add(candidate.envName);
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Looks one environment variable name up across the three layers, in
|
|
172
|
+
* precedence order.
|
|
173
|
+
*
|
|
174
|
+
* @param envName - The name to look up.
|
|
175
|
+
* @param context - The environment layers.
|
|
176
|
+
* @returns The value and the layer that held it, or undefined when nothing did.
|
|
177
|
+
*/
|
|
178
|
+
export function lookupEnvName(
|
|
179
|
+
envName: string,
|
|
180
|
+
context: LadderContext
|
|
181
|
+
): { value: string; file: EnvSourceFile } | undefined {
|
|
182
|
+
const shell = context.shellEnv[envName];
|
|
183
|
+
if (shell !== undefined) return { value: shell, file: "shell" };
|
|
184
|
+
|
|
185
|
+
const local = context.localEnv[envName];
|
|
186
|
+
if (local !== undefined) return { value: local, file: ENV_LOCAL_NAME };
|
|
187
|
+
|
|
188
|
+
const shared = context.sharedEnv[envName];
|
|
189
|
+
if (shared !== undefined) return { value: shared, file: ENV_DEFAULTS_NAME };
|
|
190
|
+
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Walks the ladder for one variable and returns the first answer it finds.
|
|
196
|
+
*
|
|
197
|
+
* @param defined - The definition and the recipe that published it.
|
|
198
|
+
* @param context - The environment layers and mapping records.
|
|
199
|
+
* @returns The value and its source, or undefined when no rung answered.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveVariable(
|
|
202
|
+
defined: DefinedVariable,
|
|
203
|
+
context: LadderContext
|
|
204
|
+
): ResolvedVariable | undefined {
|
|
205
|
+
for (const candidate of variableCandidates(defined, context)) {
|
|
206
|
+
const hit = lookupEnvName(candidate.envName, context);
|
|
207
|
+
if (hit !== undefined) {
|
|
208
|
+
return {
|
|
209
|
+
value: hit.value,
|
|
210
|
+
source: { rung: candidate.rung, envName: candidate.envName, file: hit.file },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A resolution plus the full candidate list, for diagnostics and `sous vars`. */
|
|
218
|
+
export interface VariableDiagnosis {
|
|
219
|
+
/** The winning answer, when a rung produced one. */
|
|
220
|
+
resolved?: ResolvedVariable;
|
|
221
|
+
/** Every name the ladder tried, most specific first. */
|
|
222
|
+
candidates: LadderCandidate[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resolves a variable and reports every candidate name it considered, which is
|
|
227
|
+
* what `sous vars <name>` prints and what a non-interactive failure message
|
|
228
|
+
* lists.
|
|
229
|
+
*
|
|
230
|
+
* @param defined - The definition and the recipe that published it.
|
|
231
|
+
* @param context - The environment layers and mapping records.
|
|
232
|
+
*/
|
|
233
|
+
export function diagnoseVariable(
|
|
234
|
+
defined: DefinedVariable,
|
|
235
|
+
context: LadderContext
|
|
236
|
+
): VariableDiagnosis {
|
|
237
|
+
const candidates = variableCandidates(defined, context);
|
|
238
|
+
for (const candidate of candidates) {
|
|
239
|
+
const hit = lookupEnvName(candidate.envName, context);
|
|
240
|
+
if (hit !== undefined) {
|
|
241
|
+
return {
|
|
242
|
+
resolved: {
|
|
243
|
+
value: hit.value,
|
|
244
|
+
source: { rung: candidate.rung, envName: candidate.envName, file: hit.file },
|
|
245
|
+
},
|
|
246
|
+
candidates,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { candidates };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Records a value in the context so later lookups in the same run see it, as
|
|
255
|
+
* they would on the next run once the file is on disk.
|
|
256
|
+
*
|
|
257
|
+
* @param context - The context to update.
|
|
258
|
+
* @param file - Which layer the value was written to.
|
|
259
|
+
* @param envName - The environment variable name.
|
|
260
|
+
* @param value - The value that was stored.
|
|
261
|
+
*/
|
|
262
|
+
export function recordAnswerInContext(
|
|
263
|
+
context: LadderContext,
|
|
264
|
+
file: EnvSourceFile,
|
|
265
|
+
envName: string,
|
|
266
|
+
value: string
|
|
267
|
+
): void {
|
|
268
|
+
if (file === "shell") context.shellEnv[envName] = value;
|
|
269
|
+
else if (file === ENV_LOCAL_NAME) context.localEnv[envName] = value;
|
|
270
|
+
else context.sharedEnv[envName] = value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A one-line, plain-language description of where a value came from, such as
|
|
275
|
+
* "the shared scope name SOUS_VAR_API_URL, from the .env file".
|
|
276
|
+
*
|
|
277
|
+
* @param source - The resolved source.
|
|
278
|
+
*/
|
|
279
|
+
export function describeSource(source: VariableSource): string {
|
|
280
|
+
const where = SOURCE_LABELS[source.file] ?? source.file;
|
|
281
|
+
return `the ${RUNG_LABELS[source.rung]} name ${source.envName}, from ${where}`;
|
|
282
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping records: the universal conflict resolver for variable answers.
|
|
3
|
+
*
|
|
4
|
+
* A mapping record binds one environment variable, of any name at all, to one
|
|
5
|
+
* fully qualified variable:
|
|
6
|
+
*
|
|
7
|
+
* SOME_VAR -> sous-recipes:misc/stuff/apiUrl
|
|
8
|
+
*
|
|
9
|
+
* It is the top rung of the resolution ladder, and it exists because generated
|
|
10
|
+
* names can collide: two recipes may both declare `apiUrl`, or an author may
|
|
11
|
+
* bind an existing name such as `GITHUB_TOKEN` that something else already
|
|
12
|
+
* uses. Rather than inventing a name grammar that sous would have to parse back
|
|
13
|
+
* into scopes, a record simply states the binding.
|
|
14
|
+
*
|
|
15
|
+
* Records live under the top-level `varMappings` config key. sous writes the
|
|
16
|
+
* ones it creates into the machine-written `conf.d/520-var-mappings.jsonc`
|
|
17
|
+
* layer; a user may also hand-write `varMappings` in the primary config, and
|
|
18
|
+
* the layers merge like anything else.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
24
|
+
import { ConfigError } from "../errors.js";
|
|
25
|
+
import {
|
|
26
|
+
managedLayerHeader,
|
|
27
|
+
updateManagedLayer,
|
|
28
|
+
} from "../repos/managed-layer.js";
|
|
29
|
+
import {
|
|
30
|
+
NAMESPACE_NAME_PATTERN,
|
|
31
|
+
RECIPE_NAME_PATTERN,
|
|
32
|
+
REPO_NAME_PATTERN,
|
|
33
|
+
VARIABLE_NAME_PATTERN,
|
|
34
|
+
} from "../repos/formats/patterns.js";
|
|
35
|
+
import type { DefinedVariable } from "./definition-source.js";
|
|
36
|
+
|
|
37
|
+
/** File name of the machine-written mapping record layer. */
|
|
38
|
+
export const VAR_MAPPINGS_LAYER_FILENAME = "520-var-mappings.jsonc";
|
|
39
|
+
|
|
40
|
+
/** What the mapping record layer holds, written into its header comment. */
|
|
41
|
+
export const VAR_MAPPINGS_DESCRIPTION = [
|
|
42
|
+
"Each entry under 'varMappings' binds an environment variable to one recipe",
|
|
43
|
+
"variable, so an answer can be stored under a name of your choosing when the",
|
|
44
|
+
"usual names are taken. The 'sous vars ask' command writes them; run",
|
|
45
|
+
"'sous vars' to see which name answered what.",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** The header comment sous writes at the top of the mapping record layer. */
|
|
49
|
+
export const VAR_MAPPINGS_COMMENT = managedLayerHeader(
|
|
50
|
+
VAR_MAPPINGS_LAYER_FILENAME,
|
|
51
|
+
VAR_MAPPINGS_DESCRIPTION
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/** A mapping record's target: one variable, named in full. */
|
|
55
|
+
export interface MappingTarget {
|
|
56
|
+
/** The repository's short name, when the record names one. */
|
|
57
|
+
repo?: string;
|
|
58
|
+
/** The recipe's namespace. */
|
|
59
|
+
namespace: string;
|
|
60
|
+
/** The recipe's name. */
|
|
61
|
+
recipe: string;
|
|
62
|
+
/** The variable's camelCase name. */
|
|
63
|
+
variable: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The one-line reminder appended to every mapping error. */
|
|
67
|
+
const TARGET_HELP =
|
|
68
|
+
"A mapping target is written as 'namespace/recipe/variableName', optionally " +
|
|
69
|
+
"qualified with a repository as 'repo:namespace/recipe/variableName'.";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parses a mapping target string into its parts, throwing a ConfigError that
|
|
73
|
+
* quotes the input and shows the grammar when it does not fit.
|
|
74
|
+
*
|
|
75
|
+
* @param input - The target as written in the config.
|
|
76
|
+
*/
|
|
77
|
+
export function parseMappingTarget(input: string): MappingTarget {
|
|
78
|
+
const fail = (problem: string): never => {
|
|
79
|
+
throw new ConfigError(
|
|
80
|
+
`Invalid variable mapping target '${input}': ${problem}\n ${TARGET_HELP}`
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
85
|
+
if (trimmed.length === 0) fail("a target must not be empty.");
|
|
86
|
+
|
|
87
|
+
let body = trimmed;
|
|
88
|
+
let repo: string | undefined;
|
|
89
|
+
|
|
90
|
+
const colon = body.indexOf(":");
|
|
91
|
+
if (colon !== -1) {
|
|
92
|
+
repo = body.slice(0, colon);
|
|
93
|
+
body = body.slice(colon + 1);
|
|
94
|
+
if (!REPO_NAME_PATTERN.test(repo)) {
|
|
95
|
+
fail(
|
|
96
|
+
`the repository qualifier '${repo}' must be lowercase kebab-case: a letter, ` +
|
|
97
|
+
"then letters, digits or hyphens."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const segments = body.split("/");
|
|
103
|
+
if (segments.length !== 3) {
|
|
104
|
+
fail("a target names a namespace, a recipe and a variable, joined by slashes.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const [namespace, recipe, variable] = segments as [string, string, string];
|
|
108
|
+
if (!NAMESPACE_NAME_PATTERN.test(namespace)) {
|
|
109
|
+
fail(`the namespace '${namespace}' must be lowercase kebab-case.`);
|
|
110
|
+
}
|
|
111
|
+
if (!RECIPE_NAME_PATTERN.test(recipe)) {
|
|
112
|
+
fail(`the recipe name '${recipe}' must be lowercase kebab-case.`);
|
|
113
|
+
}
|
|
114
|
+
if (!VARIABLE_NAME_PATTERN.test(variable)) {
|
|
115
|
+
fail(
|
|
116
|
+
`the variable name '${variable}' must be camelCase: a lowercase letter, then ` +
|
|
117
|
+
"letters or digits."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parsed: MappingTarget = { namespace, recipe, variable };
|
|
122
|
+
if (repo !== undefined) parsed.repo = repo;
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renders a mapping target back into its written form, which round-trips with
|
|
128
|
+
* `parseMappingTarget`.
|
|
129
|
+
*
|
|
130
|
+
* @param target - The target parts.
|
|
131
|
+
*/
|
|
132
|
+
export function formatMappingTarget(target: MappingTarget): string {
|
|
133
|
+
const qualifier = target.repo === undefined ? "" : `${target.repo}:`;
|
|
134
|
+
return `${qualifier}${target.namespace}/${target.recipe}/${target.variable}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The fully qualified target for one defined variable, repository qualifier
|
|
139
|
+
* included, which is what a new record is written with.
|
|
140
|
+
*
|
|
141
|
+
* @param defined - The definition and the recipe that published it.
|
|
142
|
+
*/
|
|
143
|
+
export function mappingTargetFor(defined: DefinedVariable): MappingTarget {
|
|
144
|
+
return {
|
|
145
|
+
repo: defined.recipe.repo,
|
|
146
|
+
namespace: defined.recipe.namespace,
|
|
147
|
+
recipe: defined.recipe.name,
|
|
148
|
+
variable: defined.definition.name,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* True when a mapping record's target names this variable. A record without a
|
|
154
|
+
* repository qualifier matches the variable in any repository; one with a
|
|
155
|
+
* qualifier must name the same repository.
|
|
156
|
+
*
|
|
157
|
+
* @param target - The record's parsed target.
|
|
158
|
+
* @param defined - The definition and the recipe that published it.
|
|
159
|
+
*/
|
|
160
|
+
export function mappingMatches(target: MappingTarget, defined: DefinedVariable): boolean {
|
|
161
|
+
if (target.repo !== undefined && target.repo !== defined.recipe.repo) return false;
|
|
162
|
+
return (
|
|
163
|
+
target.namespace === defined.recipe.namespace &&
|
|
164
|
+
target.recipe === defined.recipe.name &&
|
|
165
|
+
target.variable === defined.definition.name
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Every environment variable name bound to this variable by a mapping record,
|
|
171
|
+
* sorted so the order never depends on config layer order.
|
|
172
|
+
*
|
|
173
|
+
* @param mappings - The merged `varMappings` block.
|
|
174
|
+
* @param defined - The definition and the recipe that published it.
|
|
175
|
+
*/
|
|
176
|
+
export function mappedNamesFor(
|
|
177
|
+
mappings: Record<string, string>,
|
|
178
|
+
defined: DefinedVariable
|
|
179
|
+
): string[] {
|
|
180
|
+
const names: string[] = [];
|
|
181
|
+
for (const [envName, rawTarget] of Object.entries(mappings)) {
|
|
182
|
+
if (mappingMatches(parseMappingTarget(rawTarget), defined)) names.push(envName);
|
|
183
|
+
}
|
|
184
|
+
return names.sort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Records a mapping in the machine-written `conf.d/520-var-mappings.jsonc`
|
|
189
|
+
* layer, editing only that one entry's bytes so any comments, key order and
|
|
190
|
+
* formatting already in the file survive.
|
|
191
|
+
*
|
|
192
|
+
* The edit stages to a temporary name and renames over the layer, like every
|
|
193
|
+
* other file sous writes for a machine. A layer truncated by an interrupt would
|
|
194
|
+
* fail to parse and break every later command until someone deleted it by hand;
|
|
195
|
+
* a rename either happens or does not, so the previous layer survives.
|
|
196
|
+
*
|
|
197
|
+
* @param confDir - The project's `conf.d/` directory (`configContext.confDir`).
|
|
198
|
+
* @param envName - The environment variable the answer is stored under.
|
|
199
|
+
* @param target - The variable the name is bound to.
|
|
200
|
+
* @returns The path of the layer file that was written.
|
|
201
|
+
*/
|
|
202
|
+
export function writeMappingRecord(
|
|
203
|
+
confDir: string,
|
|
204
|
+
envName: string,
|
|
205
|
+
target: MappingTarget | string
|
|
206
|
+
): string {
|
|
207
|
+
const rendered = typeof target === "string" ? target : formatMappingTarget(target);
|
|
208
|
+
// Parse before writing, so a bad target is refused rather than persisted.
|
|
209
|
+
parseMappingTarget(rendered);
|
|
210
|
+
|
|
211
|
+
return updateManagedLayer(
|
|
212
|
+
path.dirname(confDir),
|
|
213
|
+
VAR_MAPPINGS_LAYER_FILENAME,
|
|
214
|
+
[{ path: ["varMappings", envName], value: rendered }],
|
|
215
|
+
{ confDir, header: VAR_MAPPINGS_COMMENT }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reads the records already in the mapping layer file, returning an empty
|
|
221
|
+
* object when the file is missing. A layer still under the old
|
|
222
|
+
* `520-var-mappings.json` name is read as a fallback; the next write migrates
|
|
223
|
+
* it.
|
|
224
|
+
*
|
|
225
|
+
* @param filePath - Path to the mapping record layer file.
|
|
226
|
+
*/
|
|
227
|
+
export function readMappingRecords(filePath: string): Record<string, string> {
|
|
228
|
+
let readPath = filePath;
|
|
229
|
+
if (!fs.existsSync(readPath)) {
|
|
230
|
+
const legacy = filePath.endsWith(".jsonc") ? filePath.slice(0, -1) : undefined;
|
|
231
|
+
if (legacy === undefined || !fs.existsSync(legacy)) return {};
|
|
232
|
+
readPath = legacy;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const errors: ParseError[] = [];
|
|
236
|
+
const parsed = parseJsonc(fs.readFileSync(readPath, "utf8"), errors, {
|
|
237
|
+
allowTrailingComma: true,
|
|
238
|
+
disallowComments: false,
|
|
239
|
+
});
|
|
240
|
+
if (errors.length > 0) {
|
|
241
|
+
throw new ConfigError(
|
|
242
|
+
`Could not read the variable mapping records at ${readPath}:\n` +
|
|
243
|
+
` The file is not valid JSON with comments.\n` +
|
|
244
|
+
` This file is written by sous; deleting it removes every mapping record.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const block =
|
|
249
|
+
parsed !== null && typeof parsed === "object"
|
|
250
|
+
? (parsed as { varMappings?: unknown }).varMappings
|
|
251
|
+
: undefined;
|
|
252
|
+
if (block === undefined) return {};
|
|
253
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) {
|
|
254
|
+
throw new ConfigError(
|
|
255
|
+
`The variable mapping records at ${readPath} are malformed: 'varMappings' must ` +
|
|
256
|
+
`be an object of environment variable names to targets.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const records: Record<string, string> = {};
|
|
261
|
+
for (const [key, value] of Object.entries(block as Record<string, unknown>)) {
|
|
262
|
+
if (typeof value === "string") records[key] = value;
|
|
263
|
+
}
|
|
264
|
+
return records;
|
|
265
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable names for recipe variable answers.
|
|
3
|
+
*
|
|
4
|
+
* Every name here is GENERATED and then LOOKED UP. Nothing in sous ever parses
|
|
5
|
+
* a name back into the scope that produced it: `_` is both the delimiter and a
|
|
6
|
+
* legal identifier character, so `SOUS_VAR_MISC_STUFF_API_URL` could be split
|
|
7
|
+
* in several places and no parse would be trustworthy. When a generated name
|
|
8
|
+
* collides with something else, a mapping record (see `mappings.ts`) binds an
|
|
9
|
+
* arbitrary name to one fully qualified variable instead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
|
|
13
|
+
|
|
14
|
+
/** The prefix every generated answer name carries. */
|
|
15
|
+
export const ENV_PREFIX = "SOUS_VAR_";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Converts a camelCase or kebab-case identifier to upper snake case.
|
|
19
|
+
*
|
|
20
|
+
* @param name - The identifier, such as `apiBaseUrl` or `task-files`.
|
|
21
|
+
* @returns The upper snake case form, such as `API_BASE_URL` or `TASK_FILES`.
|
|
22
|
+
*/
|
|
23
|
+
export function toUpperSnake(name: string): string {
|
|
24
|
+
return name
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
26
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
27
|
+
.replace(/[-\s.]+/g, "_")
|
|
28
|
+
.replace(/_+/g, "_")
|
|
29
|
+
.replace(/^_|_$/g, "")
|
|
30
|
+
.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The default environment variable name for a variable, derived from its
|
|
35
|
+
* camelCase name: `apiUrl` becomes `SOUS_VAR_API_URL`. This is the same string
|
|
36
|
+
* as the shared rung of the resolution ladder.
|
|
37
|
+
*
|
|
38
|
+
* @param variableName - The variable's camelCase name.
|
|
39
|
+
*/
|
|
40
|
+
export function deriveEnvName(variableName: string): string {
|
|
41
|
+
return `${ENV_PREFIX}${toUpperSnake(variableName)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The recipe-scoped name: the most specific generated rung, naming both the
|
|
46
|
+
* namespace and the recipe. `misc` plus `stuff` plus `apiUrl` becomes
|
|
47
|
+
* `SOUS_VAR_MISC_STUFF_API_URL`.
|
|
48
|
+
*
|
|
49
|
+
* @param namespace - The recipe's namespace.
|
|
50
|
+
* @param recipe - The recipe's name.
|
|
51
|
+
* @param variableName - The variable's camelCase name.
|
|
52
|
+
*/
|
|
53
|
+
export function recipeScopedName(
|
|
54
|
+
namespace: string,
|
|
55
|
+
recipe: string,
|
|
56
|
+
variableName: string
|
|
57
|
+
): string {
|
|
58
|
+
return `${ENV_PREFIX}${toUpperSnake(namespace)}_${toUpperSnake(recipe)}_${toUpperSnake(
|
|
59
|
+
variableName
|
|
60
|
+
)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The namespace-scoped name, which answers the same variable for every recipe
|
|
65
|
+
* in one namespace. `misc` plus `apiUrl` becomes `SOUS_VAR_MISC_API_URL`.
|
|
66
|
+
*
|
|
67
|
+
* @param namespace - The recipe's namespace.
|
|
68
|
+
* @param variableName - The variable's camelCase name.
|
|
69
|
+
*/
|
|
70
|
+
export function namespaceScopedName(namespace: string, variableName: string): string {
|
|
71
|
+
return `${ENV_PREFIX}${toUpperSnake(namespace)}_${toUpperSnake(variableName)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The shared name, which answers a variable of this name for every recipe that
|
|
76
|
+
* declares one. `apiUrl` becomes `SOUS_VAR_API_URL`.
|
|
77
|
+
*
|
|
78
|
+
* @param variableName - The variable's camelCase name.
|
|
79
|
+
*/
|
|
80
|
+
export function sharedName(variableName: string): string {
|
|
81
|
+
return deriveEnvName(variableName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The bare declared name: the definition's own `env` field when its author set
|
|
86
|
+
* one (so a recipe can bind an existing variable such as `GITHUB_TOKEN`), and
|
|
87
|
+
* the shared form otherwise. This is also the name a new answer is written
|
|
88
|
+
* under.
|
|
89
|
+
*
|
|
90
|
+
* @param definition - The variable definition.
|
|
91
|
+
*/
|
|
92
|
+
export function bareName(definition: VariableDefinition): string {
|
|
93
|
+
return definition.env ?? sharedName(definition.name);
|
|
94
|
+
}
|