@sous-io/sous 0.1.1 → 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 +115 -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 +72 -8
- 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/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,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where variable definitions come from.
|
|
3
|
+
*
|
|
4
|
+
* A definition is a published specification, not a value: it says what a
|
|
5
|
+
* variable is called, what shape an answer takes, and what question to ask.
|
|
6
|
+
* Definitions ship inside recipe manifests, so the real source is the set of
|
|
7
|
+
* recipes a project's lockfile pins. Every consumer goes through ONE seam,
|
|
8
|
+
* `loadProjectDefinitions`; nothing else in the variables layer knows where
|
|
9
|
+
* definitions came from, which is what lets `sous vars ask --file` read a
|
|
10
|
+
* standalone file through the same machinery.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { parseFormat } from "../repos/formats/common.js";
|
|
16
|
+
import {
|
|
17
|
+
variableDefinitionSchema,
|
|
18
|
+
type VariableDefinition,
|
|
19
|
+
} from "../repos/formats/recipe-manifest.js";
|
|
20
|
+
import { loadManifestFile } from "../repos/load-manifest.js";
|
|
21
|
+
import {
|
|
22
|
+
listLockedRecipes,
|
|
23
|
+
readProjectLockfile,
|
|
24
|
+
readRecipeManifestIn,
|
|
25
|
+
} from "../repos/locked-recipes.js";
|
|
26
|
+
import type { Settings } from "../settings.js";
|
|
27
|
+
|
|
28
|
+
/** Which recipe published a definition, spelled out for display and for naming. */
|
|
29
|
+
export interface DefiningRecipe {
|
|
30
|
+
/** The short name of the repository the recipe came from. */
|
|
31
|
+
repo: string;
|
|
32
|
+
/** The recipe's namespace. */
|
|
33
|
+
namespace: string;
|
|
34
|
+
/** The recipe's name. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** The exact version of the recipe in play. */
|
|
37
|
+
version: string;
|
|
38
|
+
/**
|
|
39
|
+
* Where the repository lives: a URL for a hosted repository, or an absolute
|
|
40
|
+
* path for one read through the `local` provider. Used to show a recipe as a
|
|
41
|
+
* link rather than as a bare name.
|
|
42
|
+
*/
|
|
43
|
+
url?: string;
|
|
44
|
+
/** The recipe's folder inside the repository, when it is known. */
|
|
45
|
+
path?: string;
|
|
46
|
+
/** The recipe's directory on this machine, when it is present. */
|
|
47
|
+
dir?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One variable definition together with the recipe that published it. */
|
|
51
|
+
export interface DefinedVariable {
|
|
52
|
+
/** The published specification. */
|
|
53
|
+
definition: VariableDefinition;
|
|
54
|
+
/** The recipe the definition came from. */
|
|
55
|
+
recipe: DefiningRecipe;
|
|
56
|
+
/**
|
|
57
|
+
* How this variable came to be in play: the subscribed recipe first, then
|
|
58
|
+
* each recipe it depends on, ending with the recipe that declares the
|
|
59
|
+
* definition. A direct subscription has one entry; an indirect one shows the
|
|
60
|
+
* whole chain. Absent when nothing recorded it, in which case the defining
|
|
61
|
+
* recipe stands for itself.
|
|
62
|
+
*/
|
|
63
|
+
requiredBy?: DefiningRecipe[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Anything that can produce the variable definitions in play for a project. */
|
|
67
|
+
export interface VariableDefinitionSource {
|
|
68
|
+
/** Loads every definition this source knows about. */
|
|
69
|
+
load(): Promise<DefinedVariable[]>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A source backed by a fixed array. Used by tests and by any caller that has
|
|
74
|
+
* already assembled its definitions.
|
|
75
|
+
*/
|
|
76
|
+
export class StaticDefinitionSource implements VariableDefinitionSource {
|
|
77
|
+
/**
|
|
78
|
+
* @param defined - The definitions this source returns.
|
|
79
|
+
*/
|
|
80
|
+
constructor(private readonly defined: DefinedVariable[]) {}
|
|
81
|
+
|
|
82
|
+
/** Returns the definitions handed to the constructor. */
|
|
83
|
+
async load(): Promise<DefinedVariable[]> {
|
|
84
|
+
return this.defined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The project's own definitions: every variable declared by every recipe the
|
|
90
|
+
* lockfile pins, whether the project subscribed to it directly or a dependency
|
|
91
|
+
* pulled it in.
|
|
92
|
+
*
|
|
93
|
+
* Both kinds count, deliberately. A recipe may `depends` on another purely to
|
|
94
|
+
* reuse its published variable definitions, and a build that cannot answer
|
|
95
|
+
* those variables is just as broken as one that cannot answer a subscription's.
|
|
96
|
+
*
|
|
97
|
+
* A recipe the store does not hold yet contributes nothing rather than failing:
|
|
98
|
+
* a fresh clone lists what it can until `sous build` restores the rest.
|
|
99
|
+
*/
|
|
100
|
+
export class ProjectDefinitionSource implements VariableDefinitionSource {
|
|
101
|
+
/**
|
|
102
|
+
* @param settings - The merged project config, which holds the subscriptions.
|
|
103
|
+
* @param sousDir - The project's `.sous/` directory, where the lockfile lives.
|
|
104
|
+
* @param env - The environment to read; decides where the store is.
|
|
105
|
+
*/
|
|
106
|
+
constructor(
|
|
107
|
+
private readonly settings: Settings,
|
|
108
|
+
private readonly sousDir: string,
|
|
109
|
+
private readonly env: NodeJS.ProcessEnv = process.env
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
/** Every variable published by every recipe this project's lockfile pins. */
|
|
113
|
+
async load(): Promise<DefinedVariable[]> {
|
|
114
|
+
void this.settings;
|
|
115
|
+
|
|
116
|
+
const defined: DefinedVariable[] = [];
|
|
117
|
+
const lock = readProjectLockfile(this.sousDir);
|
|
118
|
+
|
|
119
|
+
for (const located of listLockedRecipes({ sousDir: this.sousDir, env: this.env })) {
|
|
120
|
+
if (!located.present) continue;
|
|
121
|
+
|
|
122
|
+
const manifest = readRecipeManifestIn(located.dir);
|
|
123
|
+
if (manifest === undefined) continue;
|
|
124
|
+
|
|
125
|
+
const url = lock.repos[located.repo]?.url;
|
|
126
|
+
const recipe: DefiningRecipe = {
|
|
127
|
+
repo: located.repo,
|
|
128
|
+
namespace: located.namespace,
|
|
129
|
+
name: located.name,
|
|
130
|
+
version: located.version,
|
|
131
|
+
dir: located.dir,
|
|
132
|
+
...(url === undefined ? {} : { url }),
|
|
133
|
+
};
|
|
134
|
+
for (const definition of manifest.variables ?? []) {
|
|
135
|
+
defined.push({ definition, recipe });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return defined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds the definition source for a project. This is the single injection
|
|
145
|
+
* point every `sous vars` command uses.
|
|
146
|
+
*
|
|
147
|
+
* @param settings - The merged project config.
|
|
148
|
+
* @param sousDir - The project's `.sous/` directory.
|
|
149
|
+
* @param env - The environment to read; decides where the store is.
|
|
150
|
+
*/
|
|
151
|
+
export function loadProjectDefinitions(
|
|
152
|
+
settings: Settings,
|
|
153
|
+
sousDir: string,
|
|
154
|
+
env: NodeJS.ProcessEnv = process.env
|
|
155
|
+
): VariableDefinitionSource {
|
|
156
|
+
return new ProjectDefinitionSource(settings, sousDir, env);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Standalone definition files ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A standalone definitions document: the same `variables:` array a recipe
|
|
163
|
+
* manifest carries, in a file of its own. `sous vars ask --file` reads one of
|
|
164
|
+
* these, which is how a project asks questions that are not published by any
|
|
165
|
+
* recipe yet.
|
|
166
|
+
*
|
|
167
|
+
* It is the same schema, so the same rules apply: every definition must carry a
|
|
168
|
+
* description and an example, and both are shown when the question is asked.
|
|
169
|
+
*/
|
|
170
|
+
export const definitionsFileSchema = z.object({
|
|
171
|
+
/** The definitions to ask, in the recipe manifest's own shape. */
|
|
172
|
+
variables: z.array(variableDefinitionSchema).min(1, "must list at least one variable"),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/** A validated standalone definitions document. */
|
|
176
|
+
export type DefinitionsFile = z.infer<typeof definitionsFileSchema>;
|
|
177
|
+
|
|
178
|
+
/** The repository name recorded for definitions that came from a local file. */
|
|
179
|
+
export const LOCAL_FILE_REPO = "local";
|
|
180
|
+
|
|
181
|
+
/** The namespace recorded for definitions that came from a local file. */
|
|
182
|
+
export const LOCAL_FILE_NAMESPACE = "local";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Turns a file name into a kebab-case pseudo-recipe name, so definitions read
|
|
186
|
+
* from a file still have a recipe to be attributed to and still generate the
|
|
187
|
+
* usual scoped environment variable names.
|
|
188
|
+
*
|
|
189
|
+
* @param filePath - The definitions file's path.
|
|
190
|
+
*/
|
|
191
|
+
export function pseudoRecipeName(filePath: string): string {
|
|
192
|
+
const base = path.basename(filePath).replace(/\.[^.]+$/, "");
|
|
193
|
+
const slug = base
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
196
|
+
.replace(/^-+|-+$/g, "");
|
|
197
|
+
return slug.length === 0 || !/^[a-z]/.test(slug) ? `file-${slug}` : slug;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Loads a standalone definitions file (YAML or JSON, with the same permissive
|
|
202
|
+
* JSON dialect manifests use) and attributes every definition in it to a
|
|
203
|
+
* pseudo-recipe named after the file.
|
|
204
|
+
*
|
|
205
|
+
* @param filePath - Absolute path to the definitions file.
|
|
206
|
+
*/
|
|
207
|
+
export function loadDefinitionsFile(filePath: string): DefinedVariable[] {
|
|
208
|
+
const raw = loadManifestFile(filePath);
|
|
209
|
+
const parsed = parseFormat(
|
|
210
|
+
definitionsFileSchema,
|
|
211
|
+
raw,
|
|
212
|
+
filePath,
|
|
213
|
+
"variable definitions file"
|
|
214
|
+
);
|
|
215
|
+
const recipe: DefiningRecipe = {
|
|
216
|
+
repo: LOCAL_FILE_REPO,
|
|
217
|
+
namespace: LOCAL_FILE_NAMESPACE,
|
|
218
|
+
name: pseudoRecipeName(filePath),
|
|
219
|
+
version: "0.0.0",
|
|
220
|
+
dir: filePath,
|
|
221
|
+
};
|
|
222
|
+
return parsed.variables.map((definition) => ({ definition, recipe }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A source backed by a standalone definitions file. */
|
|
226
|
+
export class FileDefinitionSource implements VariableDefinitionSource {
|
|
227
|
+
/**
|
|
228
|
+
* @param filePath - Absolute path to the definitions file.
|
|
229
|
+
*/
|
|
230
|
+
constructor(private readonly filePath: string) {}
|
|
231
|
+
|
|
232
|
+
/** Reads and validates the file, throwing a ConfigError when it does not fit. */
|
|
233
|
+
async load(): Promise<DefinedVariable[]> {
|
|
234
|
+
return loadDefinitionsFile(this.filePath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The display key for a defined variable: `namespace/recipe.variableName`.
|
|
240
|
+
* Sorting and lookups use it, so one variable never appears twice under two
|
|
241
|
+
* spellings.
|
|
242
|
+
*
|
|
243
|
+
* @param defined - The definition and the recipe that published it.
|
|
244
|
+
*/
|
|
245
|
+
export function definedVariableKey(defined: DefinedVariable): string {
|
|
246
|
+
return `${defined.recipe.namespace}/${defined.recipe.name}.${defined.definition.name}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The recipe's key (`namespace/recipe`), as shown in listings. */
|
|
250
|
+
export function definingRecipeKey(recipe: DefiningRecipe): string {
|
|
251
|
+
return `${recipe.namespace}/${recipe.name}`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display helpers for the `sous vars` commands: masking secrets, and the
|
|
3
|
+
* labeled facts block both the listing and the ask report use. The tables those
|
|
4
|
+
* commands print are laid out by the shared renderer in `src/utils/table.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
|
|
9
|
+
import {
|
|
10
|
+
BULLET,
|
|
11
|
+
formatVariable,
|
|
12
|
+
VARIABLE_INDENT,
|
|
13
|
+
wrapColumns,
|
|
14
|
+
type VariableEntry,
|
|
15
|
+
} from "../../utils/formatting.js";
|
|
16
|
+
import {
|
|
17
|
+
definingRecipeKey,
|
|
18
|
+
type DefinedVariable,
|
|
19
|
+
type DefiningRecipe,
|
|
20
|
+
} from "./definition-source.js";
|
|
21
|
+
import { constraintBullets } from "./validate.js";
|
|
22
|
+
|
|
23
|
+
/** What the value column shows for a secret whose answer is known. */
|
|
24
|
+
export const HIDDEN_VALUE = "(hidden)";
|
|
25
|
+
|
|
26
|
+
/** What the value column shows for a variable nothing has answered. */
|
|
27
|
+
export const UNANSWERED_VALUE = "(unanswered)";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The display form of a value: a secret never prints, so that a terminal
|
|
31
|
+
* recording, a screen share or a scrollback buffer cannot leak one.
|
|
32
|
+
*
|
|
33
|
+
* @param value - The stored value, or undefined when there is no answer.
|
|
34
|
+
* @param secret - Whether the definition declared the variable a secret.
|
|
35
|
+
*/
|
|
36
|
+
export function displayValue(value: string | undefined, secret: boolean): string {
|
|
37
|
+
if (value === undefined) return UNANSWERED_VALUE;
|
|
38
|
+
if (secret) return HIDDEN_VALUE;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The two documentation rows every command shows for a variable: the
|
|
44
|
+
* publisher's description, and the sample answer that makes the one-line
|
|
45
|
+
* question concrete. A published definition must carry both, so every caller
|
|
46
|
+
* can show them without checking first.
|
|
47
|
+
*
|
|
48
|
+
* @param definition - The variable definition to document.
|
|
49
|
+
* @returns Label-to-text rows, ready for `showVariables` or an aligned label block.
|
|
50
|
+
*/
|
|
51
|
+
export function documentationRows(
|
|
52
|
+
definition: VariableDefinition
|
|
53
|
+
): Record<string, string> {
|
|
54
|
+
return {
|
|
55
|
+
About: definition.description,
|
|
56
|
+
"For example": String(definition.example),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- The labeled facts about one variable --------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One line of a fact: the text, and any secondary detail shown after it in
|
|
64
|
+
* muted grey (a repository location, for instance) rather than in parentheses.
|
|
65
|
+
*/
|
|
66
|
+
export interface FactLine {
|
|
67
|
+
/** The text shown beside the label. */
|
|
68
|
+
text: string;
|
|
69
|
+
/** The secondary detail that follows it, muted. */
|
|
70
|
+
detail?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One labeled fact: the label, and the lines shown beside it. */
|
|
74
|
+
export interface LabeledFact {
|
|
75
|
+
/**
|
|
76
|
+
* The label, in the plain-word form every key and value display uses
|
|
77
|
+
* (`default`, `required-by`); the `@` the recipe manifests write is not part
|
|
78
|
+
* of it.
|
|
79
|
+
*/
|
|
80
|
+
label: string;
|
|
81
|
+
/** The text shown beside the label, one entry per line. */
|
|
82
|
+
lines: Array<string | FactLine>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** True when a repository location is a URL rather than a path on this machine. */
|
|
86
|
+
function isHostedUrl(location: string): boolean {
|
|
87
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(location) || location.startsWith("git@");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Where a recipe's source lives: the repository URL with the recipe's folder
|
|
92
|
+
* appended for a hosted repository, the filesystem path for one read from this
|
|
93
|
+
* machine, and nothing at all when neither was recorded.
|
|
94
|
+
*
|
|
95
|
+
* @param recipe - The recipe to locate.
|
|
96
|
+
*/
|
|
97
|
+
export function recipeLocation(recipe: DefiningRecipe): string | undefined {
|
|
98
|
+
if (recipe.url !== undefined && isHostedUrl(recipe.url)) {
|
|
99
|
+
const base = recipe.url.replace(/\/+$/, "");
|
|
100
|
+
return recipe.path === undefined ? base : `${base}/${recipe.path}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return recipe.url !== undefined
|
|
104
|
+
? recipe.path === undefined
|
|
105
|
+
? recipe.url
|
|
106
|
+
: path.join(recipe.url, recipe.path)
|
|
107
|
+
: recipe.dir;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A recipe written as a fact line: its key, with its location following it in
|
|
112
|
+
* muted grey. The location is a trailing detail rather than a parenthetical, so
|
|
113
|
+
* the recipe key stays the thing the eye lands on.
|
|
114
|
+
*
|
|
115
|
+
* @param recipe - The recipe to link to.
|
|
116
|
+
*/
|
|
117
|
+
export function recipeLink(recipe: DefiningRecipe): FactLine {
|
|
118
|
+
const key = definingRecipeKey(recipe);
|
|
119
|
+
const location = recipeLocation(recipe);
|
|
120
|
+
return location === undefined ? { text: key } : { text: key, detail: location };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Everything the facts renderer needs that the definition itself does not carry. */
|
|
124
|
+
export interface VariableFactsInput {
|
|
125
|
+
/** The variable and the recipe that published it. */
|
|
126
|
+
defined: DefinedVariable;
|
|
127
|
+
/** Absolute path of the env file the answer is stored in. */
|
|
128
|
+
storagePath: string;
|
|
129
|
+
/** The environment variable name the answer is stored under. */
|
|
130
|
+
storedAs: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The labeled facts about one variable, in the order both the advanced view and
|
|
135
|
+
* `sous vars show` print them. One function builds them so the two never drift
|
|
136
|
+
* apart in wording or in order.
|
|
137
|
+
*
|
|
138
|
+
* @param input - The variable, where its answer is stored, and under what name.
|
|
139
|
+
* @returns The facts, ready for `renderFacts`.
|
|
140
|
+
*/
|
|
141
|
+
export function variableFacts(input: VariableFactsInput): LabeledFact[] {
|
|
142
|
+
const { defined, storagePath, storedAs } = input;
|
|
143
|
+
const { definition } = defined;
|
|
144
|
+
const facts: LabeledFact[] = [];
|
|
145
|
+
|
|
146
|
+
if (definition.default !== undefined) {
|
|
147
|
+
facts.push({ label: "default", lines: [String(definition.default)] });
|
|
148
|
+
}
|
|
149
|
+
facts.push({ label: "example", lines: [String(definition.example)] });
|
|
150
|
+
|
|
151
|
+
const chain = defined.requiredBy ?? [defined.recipe];
|
|
152
|
+
const requiredBy: Array<string | FactLine> = [recipeLink(chain[0] ?? defined.recipe)];
|
|
153
|
+
if (chain.length > 1) {
|
|
154
|
+
requiredBy.push(
|
|
155
|
+
`pulled in through ${chain.map((recipe) => definingRecipeKey(recipe)).join(" then ")}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
facts.push({ label: "required-by", lines: requiredBy });
|
|
159
|
+
facts.push({ label: "defined-by", lines: [recipeLink(defined.recipe)] });
|
|
160
|
+
facts.push({ label: "storage-path", lines: [storagePath] });
|
|
161
|
+
facts.push({ label: "stored-as", lines: [storedAs] });
|
|
162
|
+
facts.push({
|
|
163
|
+
label: "constraints",
|
|
164
|
+
lines: constraintBullets(definition).map((bullet) => `${BULLET} ${bullet}`),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return facts;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The facts the basic view of a question shows, in the order it shows them. It
|
|
172
|
+
* is a subset of the same list the advanced view prints, selected by label, so
|
|
173
|
+
* the two views can never word a fact differently or lay it out differently.
|
|
174
|
+
*/
|
|
175
|
+
export const BASIC_FACT_LABELS = [
|
|
176
|
+
"default",
|
|
177
|
+
"example",
|
|
178
|
+
"stored-as",
|
|
179
|
+
"storage-path",
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Picks the named facts out of a fact list, in the order the labels were given
|
|
184
|
+
* and skipping any the variable does not have (a variable with no default has
|
|
185
|
+
* no `default` fact).
|
|
186
|
+
*
|
|
187
|
+
* @param facts - Every fact about the variable.
|
|
188
|
+
* @param labels - The labels to keep, in the order they should be shown.
|
|
189
|
+
*/
|
|
190
|
+
export function selectFacts(facts: LabeledFact[], labels: string[]): LabeledFact[] {
|
|
191
|
+
const byLabel = new Map(facts.map((fact) => [fact.label, fact]));
|
|
192
|
+
return labels
|
|
193
|
+
.map((label) => byLabel.get(label))
|
|
194
|
+
.filter((fact): fact is LabeledFact => fact !== undefined);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How far a rendered facts block is indented under the text above it. It is the
|
|
199
|
+
* same depth as every other key and value block, so a facts block never reads
|
|
200
|
+
* as a different kind of list.
|
|
201
|
+
*/
|
|
202
|
+
export const FACTS_INDENT = VARIABLE_INDENT;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Lays the labeled facts out through the one key and value renderer, so a fact
|
|
206
|
+
* about a variable looks exactly like every other key and value sous prints:
|
|
207
|
+
* labels aligned, colons lined up, values in the value color, and a location
|
|
208
|
+
* trailing in muted grey.
|
|
209
|
+
*
|
|
210
|
+
* @param facts - The facts to render.
|
|
211
|
+
* @param width - The column to wrap at, indentation included.
|
|
212
|
+
* @returns The rendered lines, colored for a terminal, indented by `FACTS_INDENT`.
|
|
213
|
+
*/
|
|
214
|
+
export function renderFacts(facts: LabeledFact[], width = wrapColumns()): string[] {
|
|
215
|
+
const labelWidth = Math.max(...facts.map((fact) => fact.label.length));
|
|
216
|
+
const lines: string[] = [];
|
|
217
|
+
|
|
218
|
+
for (const fact of facts) {
|
|
219
|
+
fact.lines.forEach((raw, index) => {
|
|
220
|
+
const line: FactLine = typeof raw === "string" ? { text: raw } : raw;
|
|
221
|
+
const entry: VariableEntry = {
|
|
222
|
+
// A fact needing more than one line labels only the first of them; the
|
|
223
|
+
// rest continue underneath it.
|
|
224
|
+
label: index === 0 ? fact.label : "",
|
|
225
|
+
value: line.text,
|
|
226
|
+
...(line.detail === undefined ? {} : { detail: line.detail }),
|
|
227
|
+
};
|
|
228
|
+
lines.push(...formatVariable(entry, { indent: FACTS_INDENT, labelWidth, width }));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return lines;
|
|
233
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The variables layer: where variable definitions come from, how a stored
|
|
3
|
+
* answer is found, how an answer is validated, and how a question is asked.
|
|
4
|
+
*
|
|
5
|
+
* Import from here rather than from the individual modules, so the commands and
|
|
6
|
+
* later phases have one place to look for what this layer offers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "./definition-source.js";
|
|
10
|
+
export * from "./display.js";
|
|
11
|
+
export * from "./ladder.js";
|
|
12
|
+
export * from "./mappings.js";
|
|
13
|
+
export * from "./names.js";
|
|
14
|
+
export * from "./validate.js";
|
|
15
|
+
export * from "./ask.js";
|
|
16
|
+
export * from "./report.js";
|
|
17
|
+
export * from "./preanswers.js";
|
|
18
|
+
export * from "./question-plan.js";
|