@sous-io/sous 0.2.36 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -237
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -391
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -412
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1105
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A recipe's variable definitions: what each variable is called, what shape an
|
|
3
|
-
* answer takes, and what question to ask. A recipe manifest declares them
|
|
4
|
-
* (`recipe-manifest.ts` re-exports everything here), and `sous repo release`
|
|
5
|
-
* copies each version's definitions into the index, so the same fields and the
|
|
6
|
-
* same rules read both copies.
|
|
7
|
-
*
|
|
8
|
-
* This module imports nothing but the shared primitives, because the index
|
|
9
|
-
* reads it, and the index sits below the ref parser the manifest needs.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import {
|
|
14
|
-
envVarNameSchema,
|
|
15
|
-
extensibleObject,
|
|
16
|
-
forwardCompatibleObject,
|
|
17
|
-
variableNameSchema,
|
|
18
|
-
} from "./common.js";
|
|
19
|
-
|
|
20
|
-
// --- Variable definitions -----------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
/** The value types a recipe may declare for a variable. */
|
|
23
|
-
export const VARIABLE_TYPES = [
|
|
24
|
-
"string",
|
|
25
|
-
"number",
|
|
26
|
-
"boolean",
|
|
27
|
-
"enum",
|
|
28
|
-
"path",
|
|
29
|
-
"url",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
/** Which env file an answer is written to. */
|
|
33
|
-
export const VARIABLE_SCOPES = ["shared", "local"] as const;
|
|
34
|
-
|
|
35
|
-
/** Constraints checked against an answer before it is accepted or stored. */
|
|
36
|
-
const variableValidationShape = {
|
|
37
|
-
/** A regular expression the answer must match, as a string. */
|
|
38
|
-
pattern: z
|
|
39
|
-
.string()
|
|
40
|
-
.refine(
|
|
41
|
-
(value) => {
|
|
42
|
-
try {
|
|
43
|
-
new RegExp(value);
|
|
44
|
-
return true;
|
|
45
|
-
} catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{ message: "must be a valid regular expression" }
|
|
50
|
-
)
|
|
51
|
-
.optional(),
|
|
52
|
-
/** Minimum length of a string answer. */
|
|
53
|
-
minLength: z.number().int().min(0).optional(),
|
|
54
|
-
/** Maximum length of a string answer. */
|
|
55
|
-
maxLength: z.number().int().min(0).optional(),
|
|
56
|
-
/** Minimum value of a numeric answer. */
|
|
57
|
-
min: z.number().optional(),
|
|
58
|
-
/** Maximum value of a numeric answer. */
|
|
59
|
-
max: z.number().optional(),
|
|
60
|
-
/** The allowed answers. Required when the variable's type is 'enum'. */
|
|
61
|
-
enum: z.array(z.string()).min(1, "must list at least one option").optional(),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** Constraints checked against an answer before it is accepted or stored. */
|
|
65
|
-
export const variableValidationSchema = extensibleObject(variableValidationShape);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The fields of a variable definition, shared by the two schemas that read
|
|
69
|
-
* one: the hand-written manifest, and the copy a release records in the index.
|
|
70
|
-
*
|
|
71
|
-
* @param validate - The schema for the `validate` block, strict or forward compatible.
|
|
72
|
-
*/
|
|
73
|
-
function variableDefinitionShape<Validate extends z.ZodType>(validate: Validate) {
|
|
74
|
-
return {
|
|
75
|
-
/** The variable's camelCase name, as templates refer to it. */
|
|
76
|
-
name: variableNameSchema,
|
|
77
|
-
/**
|
|
78
|
-
* The environment variable an answer binds to. Authors may name an existing
|
|
79
|
-
* variable such as GITHUB_TOKEN to reuse a value already in the environment.
|
|
80
|
-
* When omitted, `sous repo release` derives an upper snake case default from
|
|
81
|
-
* the name; the runtime never derives one.
|
|
82
|
-
*/
|
|
83
|
-
env: envVarNameSchema.optional(),
|
|
84
|
-
/** The answer's type, which decides how it is validated and prompted for. */
|
|
85
|
-
type: z.enum(VARIABLE_TYPES),
|
|
86
|
-
/** The one-line question shown when the variable is asked. */
|
|
87
|
-
prompt: z.string().min(1, "must not be empty"),
|
|
88
|
-
/**
|
|
89
|
-
* The paragraph that explains the variable: what it is for, what a good
|
|
90
|
-
* answer looks like, and what changes when it is set. Shown above the
|
|
91
|
-
* question when sous asks, and by `sous vars <name>`. Required, because a
|
|
92
|
-
* consumer reading the question has no other way to learn what a publisher
|
|
93
|
-
* meant by it.
|
|
94
|
-
*/
|
|
95
|
-
description: z
|
|
96
|
-
.string({
|
|
97
|
-
error:
|
|
98
|
-
"is required: every published variable must explain itself in a sentence or " +
|
|
99
|
-
"two. The description is shown above the question when sous asks, and by " +
|
|
100
|
-
"'sous vars <name>'",
|
|
101
|
-
})
|
|
102
|
-
.min(1, "must not be empty"),
|
|
103
|
-
/**
|
|
104
|
-
* A realistic sample answer. It is documentation and prompt copy only; sous
|
|
105
|
-
* never stores it, never offers it as the answer, and never falls back to it.
|
|
106
|
-
* Use `default` for a value a project should actually start with.
|
|
107
|
-
*/
|
|
108
|
-
example: z.union([z.string(), z.number(), z.boolean()], {
|
|
109
|
-
error:
|
|
110
|
-
"is required: every published variable must show what a real answer looks like. " +
|
|
111
|
-
"The example is shown with the question and by 'sous vars <name>'; it is " +
|
|
112
|
-
"documentation only and is never stored as the answer (use 'default' for that)",
|
|
113
|
-
}),
|
|
114
|
-
/** The value offered when the question is asked with nothing else in scope. */
|
|
115
|
-
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
116
|
-
/** Whether an answer is needed for a build to run. Defaults to true. */
|
|
117
|
-
required: z.boolean().default(true),
|
|
118
|
-
/**
|
|
119
|
-
* Whether the answer is a secret. A secret is always written to the
|
|
120
|
-
* gitignored `.sous/.env.local`, never to the committed `.sous/.env`.
|
|
121
|
-
*/
|
|
122
|
-
secret: z.boolean().default(false),
|
|
123
|
-
/**
|
|
124
|
-
* Which env file the answer is written to: 'shared' is `.sous/.env`, which is
|
|
125
|
-
* committed and shared with the team; 'local' is `.sous/.env.local`, which is
|
|
126
|
-
* gitignored and machine-specific. Defaults to 'shared'.
|
|
127
|
-
*/
|
|
128
|
-
scope: z.enum(VARIABLE_SCOPES).default("shared"),
|
|
129
|
-
/** Constraints checked against an answer. */
|
|
130
|
-
validate: validate.optional(),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** What `checkVariableDefinition` reads from a definition. */
|
|
135
|
-
type CheckedDefinition = {
|
|
136
|
-
type: VariableType;
|
|
137
|
-
secret: boolean;
|
|
138
|
-
scope: VariableScope;
|
|
139
|
-
default?: string | number | boolean | undefined;
|
|
140
|
-
example: string | number | boolean;
|
|
141
|
-
validate?:
|
|
142
|
-
| { minLength?: number; maxLength?: number; min?: number; max?: number; enum?: string[] }
|
|
143
|
-
| undefined;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The rules a variable definition obeys beyond the shape of each field, shared
|
|
148
|
-
* by both schemas so a definition the index records is held to exactly the rules
|
|
149
|
-
* its manifest was.
|
|
150
|
-
*
|
|
151
|
-
* @param definition - The definition, already parsed field by field.
|
|
152
|
-
* @param ctx - The refinement context issues are added to.
|
|
153
|
-
*/
|
|
154
|
-
function checkVariableDefinition(definition: CheckedDefinition, ctx: z.RefinementCtx): void {
|
|
155
|
-
const { type, validate } = definition;
|
|
156
|
-
|
|
157
|
-
if (type === "enum" && (validate?.enum === undefined || validate.enum.length === 0)) {
|
|
158
|
-
ctx.addIssue({
|
|
159
|
-
code: "custom",
|
|
160
|
-
path: ["validate", "enum"],
|
|
161
|
-
message:
|
|
162
|
-
"a variable of type 'enum' must list its options under 'validate.enum'",
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (validate !== undefined) {
|
|
167
|
-
const { minLength, maxLength, min, max } = validate;
|
|
168
|
-
if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
|
|
169
|
-
ctx.addIssue({
|
|
170
|
-
code: "custom",
|
|
171
|
-
path: ["validate", "maxLength"],
|
|
172
|
-
message: "must not be smaller than 'validate.minLength'",
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
if (min !== undefined && max !== undefined && min > max) {
|
|
176
|
-
ctx.addIssue({
|
|
177
|
-
code: "custom",
|
|
178
|
-
path: ["validate", "max"],
|
|
179
|
-
message: "must not be smaller than 'validate.min'",
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// A secret written to the committed env file would leak, so the two fields
|
|
185
|
-
// are not allowed to contradict each other.
|
|
186
|
-
if (definition.secret && definition.scope === "shared") {
|
|
187
|
-
ctx.addIssue({
|
|
188
|
-
code: "custom",
|
|
189
|
-
path: ["scope"],
|
|
190
|
-
message:
|
|
191
|
-
"a secret variable is stored in the gitignored '.sous/.env.local', so its " +
|
|
192
|
-
"scope must be 'local' (or left unset)",
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// `default` and `example` are both literal values written by the publisher,
|
|
197
|
-
// so both have to fit the variable they describe; an example that could
|
|
198
|
-
// never be a valid answer is worse than no example at all.
|
|
199
|
-
checkDeclaredValue(definition.default, "default");
|
|
200
|
-
checkDeclaredValue(definition.example, "example");
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Checks one publisher-written literal against the declared type and the
|
|
204
|
-
* 'validate.enum' options.
|
|
205
|
-
*
|
|
206
|
-
* @param value - The literal to check, or undefined when it was omitted.
|
|
207
|
-
* @param field - The field name, used as the issue path.
|
|
208
|
-
*/
|
|
209
|
-
function checkDeclaredValue(
|
|
210
|
-
value: string | number | boolean | undefined,
|
|
211
|
-
field: "default" | "example"
|
|
212
|
-
): void {
|
|
213
|
-
if (value === undefined) return;
|
|
214
|
-
|
|
215
|
-
const expected =
|
|
216
|
-
type === "boolean" ? "boolean" : type === "number" ? "number" : "string";
|
|
217
|
-
if (typeof value !== expected) {
|
|
218
|
-
ctx.addIssue({
|
|
219
|
-
code: "custom",
|
|
220
|
-
path: [field],
|
|
221
|
-
message: `must be a ${expected}, to match the declared type '${type}'`,
|
|
222
|
-
});
|
|
223
|
-
} else if (
|
|
224
|
-
type === "enum" &&
|
|
225
|
-
validate?.enum !== undefined &&
|
|
226
|
-
!validate.enum.includes(String(value))
|
|
227
|
-
) {
|
|
228
|
-
ctx.addIssue({
|
|
229
|
-
code: "custom",
|
|
230
|
-
path: [field],
|
|
231
|
-
message: "must be one of the options listed under 'validate.enum'",
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* A published variable definition: a specification, not a value. Definitions
|
|
239
|
-
* are inert; a question is asked only when a subscribed recipe needs the
|
|
240
|
-
* variable and no valid answer is already in scope.
|
|
241
|
-
*/
|
|
242
|
-
export const variableDefinitionSchema = extensibleObject(
|
|
243
|
-
variableDefinitionShape(variableValidationSchema)
|
|
244
|
-
).superRefine(checkVariableDefinition);
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* A variable definition as a release records it in the index, so a consumer
|
|
248
|
-
* can list a recipe's questions before its files are fetched. It is the
|
|
249
|
-
* manifest's definition, field for field and rule for rule; the one difference
|
|
250
|
-
* is that a field this sous does not define is kept rather than refused,
|
|
251
|
-
* because the index is read by every sous a project happens to pin, and a
|
|
252
|
-
* later one may add a field.
|
|
253
|
-
*/
|
|
254
|
-
export const publishedVariableDefinitionSchema = forwardCompatibleObject(
|
|
255
|
-
variableDefinitionShape(forwardCompatibleObject(variableValidationShape))
|
|
256
|
-
).superRefine(checkVariableDefinition);
|
|
257
|
-
|
|
258
|
-
/** One validated variable definition from a recipe manifest. */
|
|
259
|
-
export type VariableDefinition = z.infer<typeof variableDefinitionSchema>;
|
|
260
|
-
|
|
261
|
-
/** The constraints attached to a variable definition. */
|
|
262
|
-
export type VariableValidation = z.infer<typeof variableValidationSchema>;
|
|
263
|
-
|
|
264
|
-
/** The value type a variable definition declares. */
|
|
265
|
-
export type VariableType = (typeof VARIABLE_TYPES)[number];
|
|
266
|
-
|
|
267
|
-
/** Which env file an answer is written to. */
|
|
268
|
-
export type VariableScope = (typeof VARIABLE_SCOPES)[number];
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Freshness: when sous bothers to look upstream.
|
|
3
|
-
*
|
|
4
|
-
* A build does not want to talk to the network on every run, and a lockfile
|
|
5
|
-
* means it usually has no reason to. So the rule is deliberately dull: a
|
|
6
|
-
* repository is checked when it has never been checked, when the freshness
|
|
7
|
-
* window has lapsed since the last check, or when something asks to always
|
|
8
|
-
* pull. Watch mode passes a shorter window; a one-off command can force it.
|
|
9
|
-
*
|
|
10
|
-
* The last-check time is remembered in the sidecar the index cache already
|
|
11
|
-
* writes, so there is one record per repository rather than two.
|
|
12
|
-
*
|
|
13
|
-
* A failed check NEVER breaks a build. That belongs to the index cache, which
|
|
14
|
-
* falls back to the copy it already holds; this module only decides whether to
|
|
15
|
-
* look.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import semver from "semver";
|
|
19
|
-
import type { IndexFile } from "./formats/index-file.js";
|
|
20
|
-
import type { IndexMeta } from "./providers/index-cache.js";
|
|
21
|
-
|
|
22
|
-
/** How long a check stays good when nothing says otherwise: five minutes. */
|
|
23
|
-
// The freshness default is shared with the store settings so there is one number.
|
|
24
|
-
import { DEFAULT_FRESHNESS_SECONDS } from "./store/settings.js";
|
|
25
|
-
|
|
26
|
-
/** The part of the index cache this module uses; IndexCache satisfies it. */
|
|
27
|
-
export type UpstreamCheckRecord = {
|
|
28
|
-
readMeta(repoName: string): IndexMeta | undefined;
|
|
29
|
-
writeMeta(repoName: string, meta: IndexMeta): void;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** What decides whether to look upstream. */
|
|
33
|
-
export type FreshnessInput = {
|
|
34
|
-
/** When sous last asked upstream, from the repository's sidecar. */
|
|
35
|
-
lastCheckedAt?: string;
|
|
36
|
-
/** How long a check stays good, in seconds. Defaults to five minutes. */
|
|
37
|
-
freshnessSeconds?: number;
|
|
38
|
-
/**
|
|
39
|
-
* Whether something in play prefers a newer in-range version over the locked
|
|
40
|
-
* one. Always-pull still respects the freshness window; it changes what
|
|
41
|
-
* happens after the check, not how often the check happens.
|
|
42
|
-
*/
|
|
43
|
-
alwaysPull?: boolean;
|
|
44
|
-
/** Check regardless, which is what an explicit update command does. */
|
|
45
|
-
force?: boolean;
|
|
46
|
-
/** The clock. Defaults to now. */
|
|
47
|
-
now?: Date;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* True when sous should ask upstream whether there is anything newer.
|
|
52
|
-
*
|
|
53
|
-
* shouldCheckUpstream({ lastCheckedAt: "2026-01-01T00:00:00.000Z", freshnessSeconds: 300 })
|
|
54
|
-
* // -> false a minute later, true six minutes later
|
|
55
|
-
*
|
|
56
|
-
* @param input - The last check, the window, and the flags.
|
|
57
|
-
*/
|
|
58
|
-
export function shouldCheckUpstream(input: FreshnessInput): boolean {
|
|
59
|
-
if (input.force === true) return true;
|
|
60
|
-
if (input.lastCheckedAt === undefined) return true;
|
|
61
|
-
|
|
62
|
-
const last = Date.parse(input.lastCheckedAt);
|
|
63
|
-
if (Number.isNaN(last)) return true;
|
|
64
|
-
|
|
65
|
-
const now = (input.now ?? new Date()).getTime();
|
|
66
|
-
const windowSeconds = input.freshnessSeconds ?? DEFAULT_FRESHNESS_SECONDS;
|
|
67
|
-
if (windowSeconds <= 0) return true;
|
|
68
|
-
|
|
69
|
-
const ageSeconds = (now - last) / 1000;
|
|
70
|
-
// A last-check time in the future means a clock changed under sous; check
|
|
71
|
-
// rather than trusting an answer that cannot be right.
|
|
72
|
-
if (ageSeconds < 0) return true;
|
|
73
|
-
return ageSeconds >= windowSeconds;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Records that sous just looked upstream for a repository, whether or not the
|
|
78
|
-
* look succeeded. Recording a failed check too is what stops an unreachable
|
|
79
|
-
* host from being retried on every single build.
|
|
80
|
-
*
|
|
81
|
-
* @param record - The index cache, or anything with its sidecar methods.
|
|
82
|
-
* @param identity - The repository's canonical identity, which the cache is keyed by.
|
|
83
|
-
* @param when - The moment to record. Defaults to now.
|
|
84
|
-
*/
|
|
85
|
-
export function recordUpstreamCheck(
|
|
86
|
-
record: UpstreamCheckRecord,
|
|
87
|
-
identity: string,
|
|
88
|
-
when: Date = new Date()
|
|
89
|
-
): IndexMeta {
|
|
90
|
-
const existing = record.readMeta(identity);
|
|
91
|
-
const meta: IndexMeta = {
|
|
92
|
-
fetchedAt: existing?.fetchedAt ?? when.toISOString(),
|
|
93
|
-
...(existing?.etag === undefined ? {} : { etag: existing.etag }),
|
|
94
|
-
...(existing?.ref === undefined ? {} : { ref: existing.ref }),
|
|
95
|
-
lastCheckedAt: when.toISOString(),
|
|
96
|
-
};
|
|
97
|
-
record.writeMeta(identity, meta);
|
|
98
|
-
return meta;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** What an always-pull check found. */
|
|
102
|
-
export type NewerVersion = {
|
|
103
|
-
/** The recipe key. */
|
|
104
|
-
key: string;
|
|
105
|
-
/** The version currently locked. */
|
|
106
|
-
from: string;
|
|
107
|
-
/** The newer version that also satisfies the range. */
|
|
108
|
-
to: string;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Finds a newer in-range version of a locked recipe, or undefined when the
|
|
113
|
-
* locked one is still the best the range allows. This is the whole of what
|
|
114
|
-
* always-pull does: it re-resolves WITHIN the declared range and never widens
|
|
115
|
-
* it, so a dependency's constraint still holds.
|
|
116
|
-
*
|
|
117
|
-
* @param options - The index, the recipe, its locked version and its range.
|
|
118
|
-
*/
|
|
119
|
-
export function findNewerInRange(options: {
|
|
120
|
-
/** The repository's index. */
|
|
121
|
-
index: IndexFile;
|
|
122
|
-
/** The recipe key, `namespace/recipe`. */
|
|
123
|
-
key: string;
|
|
124
|
-
/** The version the lockfile pins. */
|
|
125
|
-
lockedVersion: string;
|
|
126
|
-
/** The range the subscription or dependency declared. Defaults to any version. */
|
|
127
|
-
range?: string;
|
|
128
|
-
/** Whether prereleases take part. */
|
|
129
|
-
prerelease?: boolean;
|
|
130
|
-
}): NewerVersion | undefined {
|
|
131
|
-
const entry = options.index.recipes[options.key];
|
|
132
|
-
if (entry === undefined) return undefined;
|
|
133
|
-
|
|
134
|
-
const prerelease = options.prerelease ?? false;
|
|
135
|
-
const candidates = Object.keys(entry.versions).filter((version) => {
|
|
136
|
-
if (!prerelease && entry.versions[version]!.prerelease === true) return false;
|
|
137
|
-
return semver.satisfies(version, options.range ?? "*", { includePrerelease: prerelease });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
|
|
141
|
-
if (best === null) return undefined;
|
|
142
|
-
if (semver.lte(best, options.lockedVersion)) return undefined;
|
|
143
|
-
|
|
144
|
-
return { key: options.key, from: options.lockedVersion, to: best };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** The holder meaning the project subscribed to a recipe itself. */
|
|
148
|
-
const PROJECT = "project";
|
|
149
|
-
|
|
150
|
-
/** Every range a locked recipe's holders declared, or nothing when one cannot be told. */
|
|
151
|
-
export type DeclaredRangeLookup = {
|
|
152
|
-
/**
|
|
153
|
-
* The range the project's subscription declares for a recipe key, `"*"` when
|
|
154
|
-
* the subscription declares none, or undefined when the project holds nothing
|
|
155
|
-
* matching it any more.
|
|
156
|
-
*/
|
|
157
|
-
subscriptionRange(key: string): string | undefined;
|
|
158
|
-
/**
|
|
159
|
-
* The range one holding recipe's manifest declares for a dependency, `"*"`
|
|
160
|
-
* when it declares none, or undefined when its manifest cannot be read or no
|
|
161
|
-
* longer names that dependency.
|
|
162
|
-
*/
|
|
163
|
-
dependencyRange(holder: string, key: string): string | undefined;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The version range an always-pull check may move ONE locked recipe within, or
|
|
168
|
-
* undefined when sous cannot tell and therefore must not move it at all.
|
|
169
|
-
*
|
|
170
|
-
* Always-pull re-resolves within what was declared; it never widens it. What was
|
|
171
|
-
* declared depends on who holds the recipe. The project's own hold means the
|
|
172
|
-
* subscription's range, which may genuinely be any version. A hold by another
|
|
173
|
-
* recipe means the range that recipe's manifest declared in `depends`, and no
|
|
174
|
-
* subscription anywhere carries it, so falling back to `*` for such an entry
|
|
175
|
-
* would move it straight past the constraint the dependency declared. Several
|
|
176
|
-
* holders mean every one of their ranges at once: whitespace between comparator
|
|
177
|
-
* sets is AND in semver, which is exactly that.
|
|
178
|
-
*
|
|
179
|
-
* Returning undefined is the safe answer, and the entry stays where the lockfile
|
|
180
|
-
* pins it. It happens when a holder's declaration cannot be read and when the
|
|
181
|
-
* combined range is not one semver can express.
|
|
182
|
-
*
|
|
183
|
-
* @param key - The locked recipe key, `namespace/recipe`.
|
|
184
|
-
* @param requestedBy - Its lockfile holders: "project", and any recipe keys.
|
|
185
|
-
* @param lookup - How to read what each holder declared.
|
|
186
|
-
*/
|
|
187
|
-
export function effectiveRangeForHolders(
|
|
188
|
-
key: string,
|
|
189
|
-
requestedBy: readonly string[],
|
|
190
|
-
lookup: DeclaredRangeLookup
|
|
191
|
-
): string | undefined {
|
|
192
|
-
const ranges: string[] = [];
|
|
193
|
-
|
|
194
|
-
for (const holder of requestedBy) {
|
|
195
|
-
const declared =
|
|
196
|
-
holder === PROJECT ? lookup.subscriptionRange(key) : lookup.dependencyRange(holder, key);
|
|
197
|
-
if (declared === undefined) return undefined;
|
|
198
|
-
ranges.push(declared);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (ranges.length === 0) return undefined;
|
|
202
|
-
|
|
203
|
-
const constraints = [...new Set(ranges.filter((range) => range !== "*"))];
|
|
204
|
-
if (constraints.length === 0) return "*";
|
|
205
|
-
|
|
206
|
-
const combined = constraints.join(" ");
|
|
207
|
-
return semver.validRange(combined) === null ? undefined : combined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* How long a build waits for a repository that it is only checking so it can
|
|
212
|
-
* say whether a newer version exists: three seconds. Past that the check is
|
|
213
|
-
* abandoned, quietly, and the build goes on with what the cache already knew.
|
|
214
|
-
*/
|
|
215
|
-
export const NEWER_VERSION_CHECK_TIMEOUT_MS = 3000;
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Runs a piece of upstream work with a deadline. The work is handed an abort
|
|
219
|
-
* signal that fires at the deadline, so a request that honors it is cancelled
|
|
220
|
-
* rather than left to keep the process alive; either way the returned promise
|
|
221
|
-
* rejects at the deadline, and the timer never holds the process open itself.
|
|
222
|
-
*
|
|
223
|
-
* await withDeadline((signal) => fetchSomething(signal), 3000);
|
|
224
|
-
* // -> the result, or an error saying the repository did not answer in time
|
|
225
|
-
*
|
|
226
|
-
* @param work - The work to run, given the signal that cancels it.
|
|
227
|
-
* @param milliseconds - How long to wait.
|
|
228
|
-
*/
|
|
229
|
-
export async function withDeadline<T>(
|
|
230
|
-
work: (signal: AbortSignal) => Promise<T>,
|
|
231
|
-
milliseconds: number
|
|
232
|
-
): Promise<T> {
|
|
233
|
-
const controller = new AbortController();
|
|
234
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
235
|
-
|
|
236
|
-
const deadline = new Promise<never>((_, reject) => {
|
|
237
|
-
timer = setTimeout(() => {
|
|
238
|
-
controller.abort();
|
|
239
|
-
reject(
|
|
240
|
-
new Error(
|
|
241
|
-
`The repository did not answer within ${formatSeconds(milliseconds)}, so sous ` +
|
|
242
|
-
`stopped waiting for it.`
|
|
243
|
-
)
|
|
244
|
-
);
|
|
245
|
-
}, milliseconds);
|
|
246
|
-
timer.unref?.();
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
return await Promise.race([work(controller.signal), deadline]);
|
|
251
|
-
} finally {
|
|
252
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* A duration in plain words, such as "3 seconds" or "1 second".
|
|
258
|
-
*
|
|
259
|
-
* @param milliseconds - The duration.
|
|
260
|
-
*/
|
|
261
|
-
function formatSeconds(milliseconds: number): string {
|
|
262
|
-
const seconds = Math.round((milliseconds / 1000) * 10) / 10;
|
|
263
|
-
return seconds === 1 ? "1 second" : `${seconds} seconds`;
|
|
264
|
-
}
|