@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,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recipe manifest: `sous.recipe.yaml` (or `.yml`, or `.json`) in each
|
|
3
|
+
* recipe folder. It is HAND-WRITTEN and is the complete, executable-free
|
|
4
|
+
* description of one publishable unit: what it is called, what version it is,
|
|
5
|
+
* what it depends on, what files it contributes, and what variables it needs
|
|
6
|
+
* answered.
|
|
7
|
+
*
|
|
8
|
+
* Two dependency lists exist, and they mean different things:
|
|
9
|
+
*
|
|
10
|
+
* - `depends` is a build dependency. The target is fetched, pinned and trust
|
|
11
|
+
* gated, and is addressable from this recipe's own files, but its files do
|
|
12
|
+
* NOT enter the subscribing project's output.
|
|
13
|
+
* - `subscribes` is a co-subscription. Subscribing to this recipe subscribes
|
|
14
|
+
* the project to the target too, with full semantics: its questions run and
|
|
15
|
+
* its files DO enter the output. A curated bundle is simply a recipe made
|
|
16
|
+
* mostly of `subscribes` entries.
|
|
17
|
+
*
|
|
18
|
+
* Both lists name their targets BY LOCATION, in one of two spellings:
|
|
19
|
+
*
|
|
20
|
+
* workflow/sat a sibling in this repository
|
|
21
|
+
* github://sous-io/sous-recipes/workflow/sat a recipe in another one
|
|
22
|
+
*
|
|
23
|
+
* A short name such as `sous-recipes:` is a consuming project's own label, so
|
|
24
|
+
* it never appears in a published manifest; see `parseDependencyRef`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import {
|
|
29
|
+
envVarNameSchema,
|
|
30
|
+
extensibleObject,
|
|
31
|
+
formatVersionSchema,
|
|
32
|
+
namespaceNameSchema,
|
|
33
|
+
parseFormat,
|
|
34
|
+
recipeNameSchema,
|
|
35
|
+
relativePathSchema,
|
|
36
|
+
semverVersionSchema,
|
|
37
|
+
variableNameSchema,
|
|
38
|
+
} from "./common.js";
|
|
39
|
+
import { parseDependencyRef } from "../ref.js";
|
|
40
|
+
|
|
41
|
+
// --- Dependency refs ----------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A dependency in `depends` or `subscribes`, in either of the two spellings a
|
|
45
|
+
* manifest may use: a bare ref naming a recipe in this same repository, or a
|
|
46
|
+
* locator URL naming one in another repository. Parsed with the real parser so
|
|
47
|
+
* a manifest and the rest of sous never disagree about what a dependency means;
|
|
48
|
+
* the parser's message is carried through as the zod issue message.
|
|
49
|
+
*/
|
|
50
|
+
const dependencyRefSchema = z.string().superRefine((value, ctx) => {
|
|
51
|
+
try {
|
|
52
|
+
parseDependencyRef(value);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
ctx.addIssue({ code: "custom", message: (error as Error).message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Builds a list-of-refs schema that also rejects the same target listed twice. */
|
|
59
|
+
function refListSchema(label: string) {
|
|
60
|
+
return z.array(dependencyRefSchema).superRefine((refs, ctx) => {
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
refs.forEach((entry, index) => {
|
|
63
|
+
const trimmed = entry.trim();
|
|
64
|
+
if (seen.has(trimmed)) {
|
|
65
|
+
ctx.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: [index],
|
|
68
|
+
message: `'${trimmed}' is listed more than once in ${label}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
seen.add(trimmed);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Contents -----------------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What a content entry contributes. `skills`, `memories` and `prompts` are
|
|
80
|
+
* compiled or copied into the subscribing project's agent directories; `config`
|
|
81
|
+
* entries name config layer files that are merged into the subscriber's config.
|
|
82
|
+
*/
|
|
83
|
+
export const CONTENT_KINDS = ["skills", "memories", "prompts", "config"] as const;
|
|
84
|
+
|
|
85
|
+
/** One group of files this recipe contributes, all of the same kind. */
|
|
86
|
+
export const recipeContentSchema = extensibleObject({
|
|
87
|
+
/** What the files are, which decides where they land in a subscribing project. */
|
|
88
|
+
kind: z.enum(CONTENT_KINDS),
|
|
89
|
+
/** Glob patterns, relative to the recipe folder, naming the files to contribute. */
|
|
90
|
+
include: z
|
|
91
|
+
.array(relativePathSchema("an include pattern", true))
|
|
92
|
+
.min(1, "must list at least one include pattern"),
|
|
93
|
+
/** Glob patterns, relative to the recipe folder, removed from the include set. */
|
|
94
|
+
exclude: z.array(relativePathSchema("an exclude pattern", true)).optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// --- Variable definitions -----------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** The value types a recipe may declare for a variable. */
|
|
100
|
+
export const VARIABLE_TYPES = [
|
|
101
|
+
"string",
|
|
102
|
+
"number",
|
|
103
|
+
"boolean",
|
|
104
|
+
"enum",
|
|
105
|
+
"path",
|
|
106
|
+
"url",
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
/** Which env file an answer is written to. */
|
|
110
|
+
export const VARIABLE_SCOPES = ["shared", "local"] as const;
|
|
111
|
+
|
|
112
|
+
/** Constraints checked against an answer before it is accepted or stored. */
|
|
113
|
+
export const variableValidationSchema = extensibleObject({
|
|
114
|
+
/** A regular expression the answer must match, as a string. */
|
|
115
|
+
pattern: z
|
|
116
|
+
.string()
|
|
117
|
+
.refine(
|
|
118
|
+
(value) => {
|
|
119
|
+
try {
|
|
120
|
+
new RegExp(value);
|
|
121
|
+
return true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{ message: "must be a valid regular expression" }
|
|
127
|
+
)
|
|
128
|
+
.optional(),
|
|
129
|
+
/** Minimum length of a string answer. */
|
|
130
|
+
minLength: z.number().int().min(0).optional(),
|
|
131
|
+
/** Maximum length of a string answer. */
|
|
132
|
+
maxLength: z.number().int().min(0).optional(),
|
|
133
|
+
/** Minimum value of a numeric answer. */
|
|
134
|
+
min: z.number().optional(),
|
|
135
|
+
/** Maximum value of a numeric answer. */
|
|
136
|
+
max: z.number().optional(),
|
|
137
|
+
/** The allowed answers. Required when the variable's type is 'enum'. */
|
|
138
|
+
enum: z.array(z.string()).min(1, "must list at least one option").optional(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A published variable definition: a specification, not a value. Definitions
|
|
143
|
+
* are inert; a question is asked only when a subscribed recipe needs the
|
|
144
|
+
* variable and no valid answer is already in scope.
|
|
145
|
+
*/
|
|
146
|
+
export const variableDefinitionSchema = extensibleObject({
|
|
147
|
+
/** The variable's camelCase name, as templates refer to it. */
|
|
148
|
+
name: variableNameSchema,
|
|
149
|
+
/**
|
|
150
|
+
* The environment variable an answer binds to. Authors may name an existing
|
|
151
|
+
* variable such as GITHUB_TOKEN to reuse a value already in the environment.
|
|
152
|
+
* When omitted, `sous repo release` derives an upper snake case default from
|
|
153
|
+
* the name; the runtime never derives one.
|
|
154
|
+
*/
|
|
155
|
+
env: envVarNameSchema.optional(),
|
|
156
|
+
/** The answer's type, which decides how it is validated and prompted for. */
|
|
157
|
+
type: z.enum(VARIABLE_TYPES),
|
|
158
|
+
/** The one-line question shown when the variable is asked. */
|
|
159
|
+
prompt: z.string().min(1, "must not be empty"),
|
|
160
|
+
/**
|
|
161
|
+
* The paragraph that explains the variable: what it is for, what a good
|
|
162
|
+
* answer looks like, and what changes when it is set. Shown above the
|
|
163
|
+
* question when sous asks, and by `sous vars <name>`. Required, because a
|
|
164
|
+
* consumer reading the question has no other way to learn what a publisher
|
|
165
|
+
* meant by it.
|
|
166
|
+
*/
|
|
167
|
+
description: z
|
|
168
|
+
.string({
|
|
169
|
+
error:
|
|
170
|
+
"is required: every published variable must explain itself in a sentence or " +
|
|
171
|
+
"two. The description is shown above the question when sous asks, and by " +
|
|
172
|
+
"'sous vars <name>'",
|
|
173
|
+
})
|
|
174
|
+
.min(1, "must not be empty"),
|
|
175
|
+
/**
|
|
176
|
+
* A realistic sample answer. It is documentation and prompt copy only; sous
|
|
177
|
+
* never stores it, never offers it as the answer, and never falls back to it.
|
|
178
|
+
* Use `default` for a value a project should actually start with.
|
|
179
|
+
*/
|
|
180
|
+
example: z.union([z.string(), z.number(), z.boolean()], {
|
|
181
|
+
error:
|
|
182
|
+
"is required: every published variable must show what a real answer looks like. " +
|
|
183
|
+
"The example is shown with the question and by 'sous vars <name>'; it is " +
|
|
184
|
+
"documentation only and is never stored as the answer (use 'default' for that)",
|
|
185
|
+
}),
|
|
186
|
+
/** The value offered when the question is asked with nothing else in scope. */
|
|
187
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
188
|
+
/** Whether an answer is needed for a build to run. Defaults to true. */
|
|
189
|
+
required: z.boolean().default(true),
|
|
190
|
+
/**
|
|
191
|
+
* Whether the answer is a secret. A secret is always written to the
|
|
192
|
+
* gitignored `.sous/.env.local`, never to the committed `.sous/.env`.
|
|
193
|
+
*/
|
|
194
|
+
secret: z.boolean().default(false),
|
|
195
|
+
/**
|
|
196
|
+
* Which env file the answer is written to: 'shared' is `.sous/.env`, which is
|
|
197
|
+
* committed and shared with the team; 'local' is `.sous/.env.local`, which is
|
|
198
|
+
* gitignored and machine-specific. Defaults to 'shared'.
|
|
199
|
+
*/
|
|
200
|
+
scope: z.enum(VARIABLE_SCOPES).default("shared"),
|
|
201
|
+
/** Constraints checked against an answer. */
|
|
202
|
+
validate: variableValidationSchema.optional(),
|
|
203
|
+
})
|
|
204
|
+
.superRefine((definition, ctx) => {
|
|
205
|
+
const { type, validate } = definition;
|
|
206
|
+
|
|
207
|
+
if (type === "enum" && (validate?.enum === undefined || validate.enum.length === 0)) {
|
|
208
|
+
ctx.addIssue({
|
|
209
|
+
code: "custom",
|
|
210
|
+
path: ["validate", "enum"],
|
|
211
|
+
message:
|
|
212
|
+
"a variable of type 'enum' must list its options under 'validate.enum'",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (validate !== undefined) {
|
|
217
|
+
const { minLength, maxLength, min, max } = validate;
|
|
218
|
+
if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
|
|
219
|
+
ctx.addIssue({
|
|
220
|
+
code: "custom",
|
|
221
|
+
path: ["validate", "maxLength"],
|
|
222
|
+
message: "must not be smaller than 'validate.minLength'",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
226
|
+
ctx.addIssue({
|
|
227
|
+
code: "custom",
|
|
228
|
+
path: ["validate", "max"],
|
|
229
|
+
message: "must not be smaller than 'validate.min'",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// A secret written to the committed env file would leak, so the two fields
|
|
235
|
+
// are not allowed to contradict each other.
|
|
236
|
+
if (definition.secret && definition.scope === "shared") {
|
|
237
|
+
ctx.addIssue({
|
|
238
|
+
code: "custom",
|
|
239
|
+
path: ["scope"],
|
|
240
|
+
message:
|
|
241
|
+
"a secret variable is stored in the gitignored '.sous/.env.local', so its " +
|
|
242
|
+
"scope must be 'local' (or left unset)",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// `default` and `example` are both literal values written by the publisher,
|
|
247
|
+
// so both have to fit the variable they describe; an example that could
|
|
248
|
+
// never be a valid answer is worse than no example at all.
|
|
249
|
+
checkDeclaredValue(definition.default, "default");
|
|
250
|
+
checkDeclaredValue(definition.example, "example");
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Checks one publisher-written literal against the declared type and the
|
|
254
|
+
* 'validate.enum' options.
|
|
255
|
+
*
|
|
256
|
+
* @param value - The literal to check, or undefined when it was omitted.
|
|
257
|
+
* @param field - The field name, used as the issue path.
|
|
258
|
+
*/
|
|
259
|
+
function checkDeclaredValue(
|
|
260
|
+
value: string | number | boolean | undefined,
|
|
261
|
+
field: "default" | "example"
|
|
262
|
+
): void {
|
|
263
|
+
if (value === undefined) return;
|
|
264
|
+
|
|
265
|
+
const expected =
|
|
266
|
+
type === "boolean" ? "boolean" : type === "number" ? "number" : "string";
|
|
267
|
+
if (typeof value !== expected) {
|
|
268
|
+
ctx.addIssue({
|
|
269
|
+
code: "custom",
|
|
270
|
+
path: [field],
|
|
271
|
+
message: `must be a ${expected}, to match the declared type '${type}'`,
|
|
272
|
+
});
|
|
273
|
+
} else if (
|
|
274
|
+
type === "enum" &&
|
|
275
|
+
validate?.enum !== undefined &&
|
|
276
|
+
!validate.enum.includes(String(value))
|
|
277
|
+
) {
|
|
278
|
+
ctx.addIssue({
|
|
279
|
+
code: "custom",
|
|
280
|
+
path: [field],
|
|
281
|
+
message: "must be one of the options listed under 'validate.enum'",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// --- The manifest -------------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/** The recipe manifest schema. */
|
|
290
|
+
export const recipeManifestSchema = extensibleObject({
|
|
291
|
+
formatVersion: formatVersionSchema,
|
|
292
|
+
/** The namespace this recipe belongs to. Must be declared by the repo manifest. */
|
|
293
|
+
namespace: namespaceNameSchema,
|
|
294
|
+
/** The recipe's name, unique within its namespace. */
|
|
295
|
+
name: recipeNameSchema,
|
|
296
|
+
/**
|
|
297
|
+
* The published version. Recipe metadata is the source of truth for versions;
|
|
298
|
+
* a git tag of the shape `namespace/recipe@1.2.3` is a convenience ref that
|
|
299
|
+
* `sous repo release` keeps consistent with this field.
|
|
300
|
+
*/
|
|
301
|
+
version: semverVersionSchema,
|
|
302
|
+
/** One-paragraph summary, shown by `sous repo search` and `sous repo list`. */
|
|
303
|
+
description: z.string().optional(),
|
|
304
|
+
/**
|
|
305
|
+
* Build dependencies: fetched and addressable here, but not added to the
|
|
306
|
+
* project. Each entry is a bare ref naming a sibling recipe in this same
|
|
307
|
+
* repository, or a locator URL naming a recipe in another one.
|
|
308
|
+
*/
|
|
309
|
+
depends: refListSchema("'depends'").optional(),
|
|
310
|
+
/** Co-subscriptions: subscribing here subscribes the project to these too. */
|
|
311
|
+
subscribes: refListSchema("'subscribes'").optional(),
|
|
312
|
+
/**
|
|
313
|
+
* The files this recipe contributes. A curated bundle contributes no files of
|
|
314
|
+
* its own, so this may be omitted; it then defaults to an empty list.
|
|
315
|
+
*/
|
|
316
|
+
contents: z.array(recipeContentSchema).default([]),
|
|
317
|
+
/** Variable definitions this recipe publishes. */
|
|
318
|
+
variables: z
|
|
319
|
+
.array(variableDefinitionSchema)
|
|
320
|
+
.superRefine((definitions, ctx) => {
|
|
321
|
+
const seenNames = new Set<string>();
|
|
322
|
+
const seenEnv = new Map<string, number>();
|
|
323
|
+
definitions.forEach((definition, index) => {
|
|
324
|
+
if (seenNames.has(definition.name)) {
|
|
325
|
+
ctx.addIssue({
|
|
326
|
+
code: "custom",
|
|
327
|
+
path: [index, "name"],
|
|
328
|
+
message: `the variable '${definition.name}' is defined more than once`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
seenNames.add(definition.name);
|
|
332
|
+
|
|
333
|
+
if (definition.env !== undefined) {
|
|
334
|
+
const first = seenEnv.get(definition.env);
|
|
335
|
+
if (first !== undefined) {
|
|
336
|
+
ctx.addIssue({
|
|
337
|
+
code: "custom",
|
|
338
|
+
path: [index, "env"],
|
|
339
|
+
message:
|
|
340
|
+
`the environment variable '${definition.env}' is already claimed by ` +
|
|
341
|
+
`variables[${first}]; two definitions cannot share one environment variable`,
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
seenEnv.set(definition.env, index);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
})
|
|
349
|
+
.optional(),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/** A validated recipe manifest. */
|
|
353
|
+
export type RecipeManifest = z.infer<typeof recipeManifestSchema>;
|
|
354
|
+
|
|
355
|
+
/** One validated content group from a recipe manifest. */
|
|
356
|
+
export type RecipeContent = z.infer<typeof recipeContentSchema>;
|
|
357
|
+
|
|
358
|
+
/** One validated variable definition from a recipe manifest. */
|
|
359
|
+
export type VariableDefinition = z.infer<typeof variableDefinitionSchema>;
|
|
360
|
+
|
|
361
|
+
/** The constraints attached to a variable definition. */
|
|
362
|
+
export type VariableValidation = z.infer<typeof variableValidationSchema>;
|
|
363
|
+
|
|
364
|
+
/** What a content group contributes. */
|
|
365
|
+
export type ContentKind = (typeof CONTENT_KINDS)[number];
|
|
366
|
+
|
|
367
|
+
/** The value type a variable definition declares. */
|
|
368
|
+
export type VariableType = (typeof VARIABLE_TYPES)[number];
|
|
369
|
+
|
|
370
|
+
/** Which env file an answer is written to. */
|
|
371
|
+
export type VariableScope = (typeof VARIABLE_SCOPES)[number];
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Validates a parsed recipe manifest, throwing a ConfigError that names the
|
|
375
|
+
* file and the path of every bad field.
|
|
376
|
+
*
|
|
377
|
+
* @param value - The parsed contents of the manifest file.
|
|
378
|
+
* @param sourceLabel - The manifest's file path, named in error messages.
|
|
379
|
+
*/
|
|
380
|
+
export function parseRecipeManifest(
|
|
381
|
+
value: unknown,
|
|
382
|
+
sourceLabel: string
|
|
383
|
+
): RecipeManifest {
|
|
384
|
+
return parseFormat(recipeManifestSchema, value, sourceLabel, "recipe manifest");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The recipe's key (`namespace/name`), which is how it is stored in an index,
|
|
389
|
+
* a lockfile and a project's subscriptions.
|
|
390
|
+
*
|
|
391
|
+
* @param manifest - A validated recipe manifest.
|
|
392
|
+
*/
|
|
393
|
+
export function recipeManifestKey(manifest: RecipeManifest): string {
|
|
394
|
+
return `${manifest.namespace}/${manifest.name}`;
|
|
395
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo manifest: `sous.repo.yaml` (or `.yml`, or `.json`) at the root of a
|
|
3
|
+
* repository. It is HAND-WRITTEN by the repo's maintainers and is the entry
|
|
4
|
+
* point sous reads to learn what a repo publishes: its namespaces, and where
|
|
5
|
+
* each recipe folder lives.
|
|
6
|
+
*
|
|
7
|
+
* The manifest is deliberately declarative and executable-free. Trust in the
|
|
8
|
+
* Repositories system rests on being able to read a repo's whole surface
|
|
9
|
+
* without running any of its code, so manifests are YAML or JSON only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import {
|
|
14
|
+
extensibleObject,
|
|
15
|
+
formatVersionSchema,
|
|
16
|
+
namespaceNameSchema,
|
|
17
|
+
parseFormat,
|
|
18
|
+
relativePathSchema,
|
|
19
|
+
repoNameSchema,
|
|
20
|
+
} from "./common.js";
|
|
21
|
+
|
|
22
|
+
/** A namespace declaration. Namespaces group recipes and are not versioned. */
|
|
23
|
+
export const repoNamespaceSchema = extensibleObject({
|
|
24
|
+
/** One-line, plain-language summary of what the namespace holds. */
|
|
25
|
+
description: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Path to a recipe folder, relative to the repo root. */
|
|
29
|
+
const recipePathSchema = relativePathSchema("a recipe path");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The repo manifest schema.
|
|
33
|
+
*
|
|
34
|
+
* `name` is only a SUGGESTED short name. The short name a project actually uses
|
|
35
|
+
* is recorded in its own config by `sous repo add`, which defaults to the last
|
|
36
|
+
* URL segment and can be overridden, so two repos suggesting the same name
|
|
37
|
+
* never collide in a project.
|
|
38
|
+
*/
|
|
39
|
+
export const repoManifestSchema = extensibleObject({
|
|
40
|
+
formatVersion: formatVersionSchema,
|
|
41
|
+
/** Suggested short name for the repo. */
|
|
42
|
+
name: repoNameSchema,
|
|
43
|
+
/** One-paragraph summary of the repo, shown by `sous repo list` and `sous repo search`. */
|
|
44
|
+
description: z.string().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Where to send a contribution: a URL, or free text describing the process.
|
|
47
|
+
* Surfaced when a provider does not support `sous repo submit`, so a
|
|
48
|
+
* contributor is never left without a route.
|
|
49
|
+
*/
|
|
50
|
+
contribute: z.string().min(1, "must not be empty").optional(),
|
|
51
|
+
/** Every namespace the repo publishes, keyed by namespace name. */
|
|
52
|
+
namespaces: z.record(namespaceNameSchema, repoNamespaceSchema),
|
|
53
|
+
/**
|
|
54
|
+
* Every recipe folder in the repo, as a path relative to the repo root. Each
|
|
55
|
+
* folder must contain a recipe manifest (`sous.recipe.yaml` or a supported
|
|
56
|
+
* variant); `sous repo release` verifies that and builds the index from it.
|
|
57
|
+
*/
|
|
58
|
+
recipes: z.array(recipePathSchema).superRefine((paths, ctx) => {
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
paths.forEach((entry, index) => {
|
|
61
|
+
if (seen.has(entry)) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: [index],
|
|
65
|
+
message: `the recipe path '${entry}' is listed more than once`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
seen.add(entry);
|
|
69
|
+
});
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** A validated repo manifest. */
|
|
74
|
+
export type RepoManifest = z.infer<typeof repoManifestSchema>;
|
|
75
|
+
|
|
76
|
+
/** A validated namespace declaration from a repo manifest. */
|
|
77
|
+
export type RepoNamespace = z.infer<typeof repoNamespaceSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validates a parsed repo manifest, throwing a ConfigError that names the file
|
|
81
|
+
* and the path of every bad field.
|
|
82
|
+
*
|
|
83
|
+
* @param value - The parsed contents of the manifest file.
|
|
84
|
+
* @param sourceLabel - The manifest's file path, named in error messages.
|
|
85
|
+
*/
|
|
86
|
+
export function parseRepoManifest(value: unknown, sourceLabel: string): RepoManifest {
|
|
87
|
+
return parseFormat(repoManifestSchema, value, sourceLabel, "repo manifest");
|
|
88
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store entry marker: `.sous.entry.json`, written beside every cached
|
|
3
|
+
* recipe version in the machine-wide store
|
|
4
|
+
* (`$SOUS_HOME/cache/<repository identity>/<namespace>/<recipe>/<version>/`).
|
|
5
|
+
*
|
|
6
|
+
* The marker is MACHINE-WRITTEN and makes an entry self-describing, so the
|
|
7
|
+
* store can be swept without consulting any project: `hash` is checked against
|
|
8
|
+
* the lockfile before the entry is used, `sizeBytes` and `lastAccessAt` drive
|
|
9
|
+
* the size-capped least-recently-used collection that `sous repo gc` performs.
|
|
10
|
+
*
|
|
11
|
+
* The store is disposable by design; everything in it is re-fetchable from the
|
|
12
|
+
* pins in a project's lockfile.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import {
|
|
17
|
+
byteCountSchema,
|
|
18
|
+
contentHashSchema,
|
|
19
|
+
formatVersionSchema,
|
|
20
|
+
isoTimestampSchema,
|
|
21
|
+
namespaceNameSchema,
|
|
22
|
+
parseFormat,
|
|
23
|
+
recipeNameSchema,
|
|
24
|
+
repoIdentitySchema,
|
|
25
|
+
semverVersionSchema,
|
|
26
|
+
stableJsonStringify,
|
|
27
|
+
} from "./common.js";
|
|
28
|
+
|
|
29
|
+
/** The store entry marker schema. */
|
|
30
|
+
export const storeEntrySchema = z.strictObject({
|
|
31
|
+
formatVersion: formatVersionSchema,
|
|
32
|
+
/**
|
|
33
|
+
* The canonical identity of the repository the recipe came from, such as
|
|
34
|
+
* `github.com/sous-io/sous-recipes`. The store is machine-wide, so it records
|
|
35
|
+
* where a recipe really came from rather than what one project calls it.
|
|
36
|
+
*/
|
|
37
|
+
repo: repoIdentitySchema,
|
|
38
|
+
/** The recipe's namespace. */
|
|
39
|
+
namespace: namespaceNameSchema,
|
|
40
|
+
/** The recipe's name. */
|
|
41
|
+
name: recipeNameSchema,
|
|
42
|
+
/** The exact version cached here. */
|
|
43
|
+
version: semverVersionSchema,
|
|
44
|
+
/** Content hash of the cached files, verified before the entry is used. */
|
|
45
|
+
hash: contentHashSchema,
|
|
46
|
+
/** When the entry was fetched. */
|
|
47
|
+
fetchedAt: isoTimestampSchema,
|
|
48
|
+
/** When the entry was last read by a build. Drives least-recently-used collection. */
|
|
49
|
+
lastAccessAt: isoTimestampSchema,
|
|
50
|
+
/** Total size of the entry's files, so the store can be capped without a rescan. */
|
|
51
|
+
sizeBytes: byteCountSchema,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** A validated store entry marker. */
|
|
55
|
+
export type StoreEntry = z.infer<typeof storeEntrySchema>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates a parsed store entry marker, throwing a ConfigError that names the
|
|
59
|
+
* file and the path of every bad field.
|
|
60
|
+
*
|
|
61
|
+
* @param value - The parsed contents of the marker file.
|
|
62
|
+
* @param sourceLabel - The marker's path, named in error messages.
|
|
63
|
+
*/
|
|
64
|
+
export function parseStoreEntry(value: unknown, sourceLabel: string): StoreEntry {
|
|
65
|
+
return parseFormat(storeEntrySchema, value, sourceLabel, "store entry marker");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Serializes a store entry marker for writing, with keys sorted.
|
|
70
|
+
*
|
|
71
|
+
* @param entry - The marker to write.
|
|
72
|
+
*/
|
|
73
|
+
export function stringifyStoreEntry(entry: StoreEntry): string {
|
|
74
|
+
return stableJsonStringify(entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The recipe key (`namespace/recipe`) a store entry holds.
|
|
79
|
+
*
|
|
80
|
+
* @param entry - A validated store entry marker.
|
|
81
|
+
*/
|
|
82
|
+
export function storeEntryKey(entry: StoreEntry): string {
|
|
83
|
+
return `${entry.namespace}/${entry.name}`;
|
|
84
|
+
}
|