@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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo index: `sous.index.json` at a repo's root.
|
|
3
|
+
*
|
|
4
|
+
* The index is MACHINE-WRITTEN by `sous repo release` and committed alongside
|
|
5
|
+
* the recipes it describes. It is the portable contract across providers: every
|
|
6
|
+
* provider, whatever its API looks like, can hand back this one file, and it is
|
|
7
|
+
* all sous needs to resolve a ref, enumerate published versions and check
|
|
8
|
+
* whether a cached copy is current.
|
|
9
|
+
*
|
|
10
|
+
* Adding a repo fetches only this file. Nothing else is downloaded until a
|
|
11
|
+
* project subscribes to something inside it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import {
|
|
16
|
+
contentHashSchema,
|
|
17
|
+
formatVersionSchema,
|
|
18
|
+
isoTimestampSchema,
|
|
19
|
+
namespaceNameSchema,
|
|
20
|
+
parseFormat,
|
|
21
|
+
recipeKeySchema,
|
|
22
|
+
relativePathSchema,
|
|
23
|
+
repoIdentitySchema,
|
|
24
|
+
repoNameSchema,
|
|
25
|
+
semverRangeSchema,
|
|
26
|
+
semverVersionSchema,
|
|
27
|
+
stableJsonStringify,
|
|
28
|
+
} from "./common.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One dependency of one published version, as the release resolved it.
|
|
32
|
+
*
|
|
33
|
+
* A SIBLING (a recipe in this same repository) always resolves to an exact
|
|
34
|
+
* version, because the release that wrote this entry cut that sibling's tag or
|
|
35
|
+
* found it already cut. A CROSS-REPOSITORY dependency carries the identity of
|
|
36
|
+
* the repository it lives in, which is what a consumer needs in order to add
|
|
37
|
+
* that repository and find the recipe in the store; the version it resolves to
|
|
38
|
+
* belongs to that repository's own index, so this entry carries the range the
|
|
39
|
+
* manifest declared instead.
|
|
40
|
+
*/
|
|
41
|
+
export const indexDependencySchema = z
|
|
42
|
+
.strictObject({
|
|
43
|
+
/** The exact version this dependency resolved to, when the release could resolve one. */
|
|
44
|
+
version: semverVersionSchema.optional(),
|
|
45
|
+
/** The range the manifest declared, recorded when no exact version could be resolved. */
|
|
46
|
+
range: semverRangeSchema.optional(),
|
|
47
|
+
/**
|
|
48
|
+
* The canonical identity of the repository publishing it
|
|
49
|
+
* (`github.com/sous-io/sous-recipes`). Omitted for a sibling, which lives in
|
|
50
|
+
* this same repository.
|
|
51
|
+
*/
|
|
52
|
+
repo: repoIdentitySchema.optional(),
|
|
53
|
+
})
|
|
54
|
+
.refine(
|
|
55
|
+
(entry) => entry.version !== undefined || entry.range !== undefined,
|
|
56
|
+
{
|
|
57
|
+
message:
|
|
58
|
+
"must record either the exact version this dependency resolved to or the range " +
|
|
59
|
+
"the recipe declared",
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** One published version of one recipe. */
|
|
64
|
+
export const indexVersionSchema = z.strictObject({
|
|
65
|
+
/** Content hash of the recipe folder at this version, verified after every fetch. */
|
|
66
|
+
hash: contentHashSchema,
|
|
67
|
+
/**
|
|
68
|
+
* The git tag carrying this version, shaped `namespace/recipe@version`. The
|
|
69
|
+
* index's `superRefine` checks it against the key and version it sits under,
|
|
70
|
+
* so a version can never point at a branch or at another recipe's tag.
|
|
71
|
+
*/
|
|
72
|
+
tag: z.string().min(1, "must not be empty"),
|
|
73
|
+
/** True when the version is a prerelease, which ranges skip unless opted in. */
|
|
74
|
+
prerelease: z.boolean(),
|
|
75
|
+
/**
|
|
76
|
+
* True when this version is not one the repository published, but the copy of
|
|
77
|
+
* the recipe that ships inside the installed sous package, folded into the
|
|
78
|
+
* index in memory so it can be resolved like anything else.
|
|
79
|
+
*
|
|
80
|
+
* Sous never writes this onto a copy of an index it fetched; a cached index
|
|
81
|
+
* stays exactly what upstream served. The field exists so that a listing can
|
|
82
|
+
* say the version came packaged with sous rather than from the repository, and
|
|
83
|
+
* so a reader of the stand-in index sous writes for itself can tell.
|
|
84
|
+
*/
|
|
85
|
+
seeded: z.boolean().optional(),
|
|
86
|
+
/** When the version was released. */
|
|
87
|
+
releasedAt: isoTimestampSchema.optional(),
|
|
88
|
+
/**
|
|
89
|
+
* What this exact version depends on, resolved at release time and keyed
|
|
90
|
+
* `namespace/recipe`. A consumer installing this version installs these
|
|
91
|
+
* versions rather than re-resolving the ranges its manifest declared, so a
|
|
92
|
+
* published version means one thing forever.
|
|
93
|
+
*
|
|
94
|
+
* The field is additive: an index written before it existed still parses, and
|
|
95
|
+
* a consumer that finds no entry falls back to the manifest's ranges.
|
|
96
|
+
*/
|
|
97
|
+
dependencies: z.record(recipeKeySchema, indexDependencySchema).optional(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** One recipe, with every version the repo publishes of it. */
|
|
101
|
+
export const indexRecipeSchema = z.strictObject({
|
|
102
|
+
/** The recipe folder, relative to the repo root. */
|
|
103
|
+
path: relativePathSchema("a recipe path"),
|
|
104
|
+
/** One-paragraph summary, copied from the recipe manifest at release time. */
|
|
105
|
+
description: z.string().optional(),
|
|
106
|
+
/** Every published version, keyed by the exact version string. */
|
|
107
|
+
versions: z
|
|
108
|
+
.record(semverVersionSchema, indexVersionSchema)
|
|
109
|
+
.refine((versions) => Object.keys(versions).length > 0, {
|
|
110
|
+
message: "must list at least one published version",
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** One namespace declaration, copied from the repo manifest at release time. */
|
|
115
|
+
export const indexNamespaceSchema = z.strictObject({
|
|
116
|
+
description: z.string().optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** The repo index schema. */
|
|
120
|
+
export const indexFileSchema = z
|
|
121
|
+
.strictObject({
|
|
122
|
+
/**
|
|
123
|
+
* A plain-language note about where this copy of the index came from. JSON
|
|
124
|
+
* has no comment syntax and an index is machine-written, so this is the one
|
|
125
|
+
* place a writer can say something to whoever opens the file. Sous ignores
|
|
126
|
+
* the value everywhere except one place: the seed index it writes for its
|
|
127
|
+
* own built-in repository carries `SEED_INDEX_COMMENT`, which is how a
|
|
128
|
+
* later run recognizes its own placeholder and is willing to replace it.
|
|
129
|
+
*/
|
|
130
|
+
$comment: z.string().optional(),
|
|
131
|
+
formatVersion: formatVersionSchema,
|
|
132
|
+
/** The repo's suggested short name, copied from its manifest. */
|
|
133
|
+
name: repoNameSchema,
|
|
134
|
+
/** When this index was generated. */
|
|
135
|
+
generatedAt: isoTimestampSchema,
|
|
136
|
+
/** The version of sous that generated it. */
|
|
137
|
+
generator: semverVersionSchema,
|
|
138
|
+
/** Every namespace the repo publishes. */
|
|
139
|
+
namespaces: z.record(namespaceNameSchema, indexNamespaceSchema),
|
|
140
|
+
/** Every recipe the repo publishes, keyed `namespace/recipe`. */
|
|
141
|
+
recipes: z.record(recipeKeySchema, indexRecipeSchema),
|
|
142
|
+
})
|
|
143
|
+
.superRefine((index, ctx) => {
|
|
144
|
+
// A recipe whose namespace is not declared could never be resolved, so a
|
|
145
|
+
// release that produced one is broken; say which recipe and which namespace.
|
|
146
|
+
for (const key of Object.keys(index.recipes)) {
|
|
147
|
+
const namespace = key.slice(0, key.indexOf("/"));
|
|
148
|
+
if (!Object.hasOwn(index.namespaces, namespace)) {
|
|
149
|
+
ctx.addIssue({
|
|
150
|
+
code: "custom",
|
|
151
|
+
path: ["recipes", key],
|
|
152
|
+
message:
|
|
153
|
+
`belongs to the namespace '${namespace}', which this index does not ` +
|
|
154
|
+
`declare under 'namespaces'`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A version's tag is what the provider fetches, so a tag that does not
|
|
160
|
+
// name this exact recipe and version is a version pointing somewhere else.
|
|
161
|
+
// Nothing on the consumer side could otherwise tell: an index publishing
|
|
162
|
+
// `1.0.0` with `tag: "main"` would hand `git clone --branch main` a moving
|
|
163
|
+
// target, whose content changes on every push and whose pinned hash then
|
|
164
|
+
// simply starts failing. Sous writes these tags itself, so requiring the
|
|
165
|
+
// shape it writes costs a correct index nothing.
|
|
166
|
+
for (const [key, recipe] of Object.entries(index.recipes)) {
|
|
167
|
+
for (const [version, published] of Object.entries(recipe.versions)) {
|
|
168
|
+
const expected = `${key}@${version}`;
|
|
169
|
+
if (published.tag === expected) continue;
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: "custom",
|
|
172
|
+
path: ["recipes", key, "versions", version, "tag"],
|
|
173
|
+
message:
|
|
174
|
+
`is '${published.tag}', but a published version's tag names the recipe and ` +
|
|
175
|
+
`the version it carries, so this one must be '${expected}'`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/** A validated repo index. */
|
|
182
|
+
export type IndexFile = z.infer<typeof indexFileSchema>;
|
|
183
|
+
|
|
184
|
+
/** One recipe entry in a repo index. */
|
|
185
|
+
export type IndexRecipe = z.infer<typeof indexRecipeSchema>;
|
|
186
|
+
|
|
187
|
+
/** One published version entry in a repo index. */
|
|
188
|
+
export type IndexVersion = z.infer<typeof indexVersionSchema>;
|
|
189
|
+
|
|
190
|
+
/** One resolved dependency of one published version. */
|
|
191
|
+
export type IndexDependency = z.infer<typeof indexDependencySchema>;
|
|
192
|
+
|
|
193
|
+
/** One namespace entry in a repo index. */
|
|
194
|
+
export type IndexNamespace = z.infer<typeof indexNamespaceSchema>;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validates a parsed repo index, throwing a ConfigError that names the file and
|
|
198
|
+
* the path of every bad field.
|
|
199
|
+
*
|
|
200
|
+
* @param value - The parsed contents of the index file.
|
|
201
|
+
* @param sourceLabel - The index's file path or URL, named in error messages.
|
|
202
|
+
*/
|
|
203
|
+
export function parseIndexFile(value: unknown, sourceLabel: string): IndexFile {
|
|
204
|
+
return parseFormat(indexFileSchema, value, sourceLabel, "repo index");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Serializes a repo index for writing, with every object key sorted so a
|
|
209
|
+
* regenerated index produces a minimal diff.
|
|
210
|
+
*
|
|
211
|
+
* @param index - The index to write.
|
|
212
|
+
*/
|
|
213
|
+
export function stringifyIndexFile(index: IndexFile): string {
|
|
214
|
+
return stableJsonStringify(index);
|
|
215
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The links map: `sous.links.json`.
|
|
3
|
+
*
|
|
4
|
+
* A link redirects a repo's resolution away from the store and at a real
|
|
5
|
+
* working copy on disk, which is how a maintainer edits recipes: edits happen
|
|
6
|
+
* in a checkout, never in the store. `sous repo link` writes an entry;
|
|
7
|
+
* `sous repo unlink` removes it and leaves the checkout in place.
|
|
8
|
+
*
|
|
9
|
+
* Two maps are read: the project's `.sous/sous.links.json` and the machine-wide
|
|
10
|
+
* `$SOUS_HOME/sous.links.json`, with the project map winning on conflict. The
|
|
11
|
+
* file is MACHINE-WRITTEN and machine-local; it is never committed, because a
|
|
12
|
+
* link bypasses versions, the lockfile and freshness checks, and those bypasses
|
|
13
|
+
* belong to one person's machine rather than to the team.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import {
|
|
18
|
+
absolutePathSchema,
|
|
19
|
+
formatVersionSchema,
|
|
20
|
+
isoTimestampSchema,
|
|
21
|
+
parseFormat,
|
|
22
|
+
repoNameSchema,
|
|
23
|
+
stableJsonStringify,
|
|
24
|
+
} from "./common.js";
|
|
25
|
+
|
|
26
|
+
/** How the linked working copy came to exist. */
|
|
27
|
+
export const LINK_ORIGINS = ["clone", "path"] as const;
|
|
28
|
+
|
|
29
|
+
/** One linked repo. */
|
|
30
|
+
export const repoLinkSchema = z.strictObject({
|
|
31
|
+
/** Absolute path to the working copy sous reads instead of the store. */
|
|
32
|
+
path: absolutePathSchema,
|
|
33
|
+
/** When the link was created. */
|
|
34
|
+
linkedAt: isoTimestampSchema,
|
|
35
|
+
/**
|
|
36
|
+
* Whether sous cloned the working copy itself ('clone') or was pointed at an
|
|
37
|
+
* existing checkout ('path'). Unlinking never deletes either, but the origin
|
|
38
|
+
* tells the user what sous put there.
|
|
39
|
+
*/
|
|
40
|
+
origin: z.enum(LINK_ORIGINS),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** The links map schema. */
|
|
44
|
+
export const linksMapSchema = z.strictObject({
|
|
45
|
+
formatVersion: formatVersionSchema,
|
|
46
|
+
/** Every linked repo, keyed by the repo's configured short name. */
|
|
47
|
+
links: z.record(repoNameSchema, repoLinkSchema),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** A validated links map. */
|
|
51
|
+
export type LinksMap = z.infer<typeof linksMapSchema>;
|
|
52
|
+
|
|
53
|
+
/** One validated link entry. */
|
|
54
|
+
export type RepoLink = z.infer<typeof repoLinkSchema>;
|
|
55
|
+
|
|
56
|
+
/** How the linked working copy came to exist. */
|
|
57
|
+
export type LinkOrigin = (typeof LINK_ORIGINS)[number];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validates a parsed links map, throwing a ConfigError that names the file and
|
|
61
|
+
* the path of every bad field.
|
|
62
|
+
*
|
|
63
|
+
* @param value - The parsed contents of the links file.
|
|
64
|
+
* @param sourceLabel - The links file's path, named in error messages.
|
|
65
|
+
*/
|
|
66
|
+
export function parseLinksMap(value: unknown, sourceLabel: string): LinksMap {
|
|
67
|
+
return parseFormat(linksMapSchema, value, sourceLabel, "links map");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An empty links map, for a project or machine with nothing linked. */
|
|
71
|
+
export function createEmptyLinksMap(): LinksMap {
|
|
72
|
+
return { formatVersion: 1, links: {} };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Serializes a links map for writing, with keys sorted.
|
|
77
|
+
*
|
|
78
|
+
* @param map - The links map to write.
|
|
79
|
+
*/
|
|
80
|
+
export function stringifyLinksMap(map: LinksMap): string {
|
|
81
|
+
return stableJsonStringify(map);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Merges a machine-wide links map with a project's, with the project's entries
|
|
86
|
+
* winning, which is the precedence `sous repo link` documents.
|
|
87
|
+
*
|
|
88
|
+
* @param global - The machine-wide map, or undefined when there is none.
|
|
89
|
+
* @param project - The project's map, or undefined when there is none.
|
|
90
|
+
*/
|
|
91
|
+
export function mergeLinksMaps(
|
|
92
|
+
global: LinksMap | undefined,
|
|
93
|
+
project: LinksMap | undefined
|
|
94
|
+
): Record<string, RepoLink> {
|
|
95
|
+
return { ...(global?.links ?? {}), ...(project?.links ?? {}) };
|
|
96
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project lockfile: `.sous/sous.lock.json`, committed to the project.
|
|
3
|
+
*
|
|
4
|
+
* The lockfile is MACHINE-WRITTEN and records the exact version and content
|
|
5
|
+
* hash of everything a project currently uses, so a fresh clone restores
|
|
6
|
+
* deterministically with no prompts and no version drift. Together with repo
|
|
7
|
+
* trust it is the supply-chain defense: nothing new enters a project except
|
|
8
|
+
* through an explicit, visible change to these files.
|
|
9
|
+
*
|
|
10
|
+
* A repository appears twice over: the project's own short name is the key of
|
|
11
|
+
* the `repos` map and is what every recipe entry and every message names, while
|
|
12
|
+
* the entry's `identity` is what the machine-wide store is keyed by.
|
|
13
|
+
*
|
|
14
|
+
* `requestedBy` is what makes removal safe. Every entry lists who holds it, the
|
|
15
|
+
* literal string `project` for something the project subscribed to directly and
|
|
16
|
+
* a recipe key for something pulled in as a dependency. Unsubscribing removes
|
|
17
|
+
* one holder; the entry itself goes only when the last holder does.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { repoIdentity } from "../identity.js";
|
|
22
|
+
import { requireProvider } from "../providers/index.js";
|
|
23
|
+
import {
|
|
24
|
+
contentHashSchema,
|
|
25
|
+
formatVersionSchema,
|
|
26
|
+
parseFormat,
|
|
27
|
+
recipeKeySchema,
|
|
28
|
+
repoIdentitySchema,
|
|
29
|
+
repoNameSchema,
|
|
30
|
+
repoUrlSchema,
|
|
31
|
+
semverVersionSchema,
|
|
32
|
+
stableJsonStringify,
|
|
33
|
+
} from "./common.js";
|
|
34
|
+
|
|
35
|
+
/** The literal `requestedBy` holder meaning "the project subscribed to this directly". */
|
|
36
|
+
export const PROJECT_HOLDER = "project";
|
|
37
|
+
|
|
38
|
+
/** How a locked recipe entered the project. */
|
|
39
|
+
export const LOCK_KINDS = ["subscribes", "depends"] as const;
|
|
40
|
+
|
|
41
|
+
/** One repo the project resolves against. */
|
|
42
|
+
export const lockedRepoSchema = z
|
|
43
|
+
.strictObject({
|
|
44
|
+
/**
|
|
45
|
+
* Where the repo lives, as recorded when it was added: a URL, or an absolute
|
|
46
|
+
* path for a repository on this machine read through the `local` provider.
|
|
47
|
+
*/
|
|
48
|
+
url: repoUrlSchema,
|
|
49
|
+
/**
|
|
50
|
+
* The repository's canonical identity, derived from that URL. The keys of
|
|
51
|
+
* `repos` are the project's own short names, which no other project has to
|
|
52
|
+
* agree with; this is what the machine-wide store and the index cache file
|
|
53
|
+
* the repository under, so a restore finds the same cached copy every other
|
|
54
|
+
* project uses.
|
|
55
|
+
*
|
|
56
|
+
* Optional ON READ only, for lockfiles written before the store was keyed by
|
|
57
|
+
* identity: an entry without one has its identity derived from `url` below,
|
|
58
|
+
* exactly as the writer would have derived it. Every lockfile sous writes
|
|
59
|
+
* carries it, so the field fills itself in on the next write.
|
|
60
|
+
*/
|
|
61
|
+
identity: repoIdentitySchema.optional(),
|
|
62
|
+
/** Content hash of the index this lock was resolved against, when known. */
|
|
63
|
+
indexHash: contentHashSchema.optional(),
|
|
64
|
+
})
|
|
65
|
+
.transform((entry, ctx) => {
|
|
66
|
+
if (entry.identity !== undefined) return { ...entry, identity: entry.identity };
|
|
67
|
+
|
|
68
|
+
// An older lockfile recorded only the URL. Deriving the identity through the
|
|
69
|
+
// provider that handles that URL is what the writer itself does, so the
|
|
70
|
+
// answer is the one the entry would have carried had it been written today.
|
|
71
|
+
try {
|
|
72
|
+
const identity = repoIdentity(requireProvider(entry.url).canonicalize(entry.url));
|
|
73
|
+
return { ...entry, identity };
|
|
74
|
+
} catch {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: "custom",
|
|
77
|
+
path: ["identity"],
|
|
78
|
+
message:
|
|
79
|
+
`is missing, and sous could not work one out from the url '${entry.url}' ` +
|
|
80
|
+
`because no provider recognizes it. Add an 'identity' to this entry, or ` +
|
|
81
|
+
`remove the lockfile and subscribe again to have sous rebuild it.`,
|
|
82
|
+
});
|
|
83
|
+
return z.NEVER;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/** One locked recipe. */
|
|
88
|
+
export const lockedRecipeSchema = z.strictObject({
|
|
89
|
+
/** The short name of the repo it came from; must appear under `repos`. */
|
|
90
|
+
repo: repoNameSchema,
|
|
91
|
+
/** The exact version resolved. */
|
|
92
|
+
version: semverVersionSchema,
|
|
93
|
+
/** Content hash of that version, verified against the store after every fetch. */
|
|
94
|
+
hash: contentHashSchema,
|
|
95
|
+
/**
|
|
96
|
+
* Who holds this entry: `project` for a direct subscription, or the ref key of
|
|
97
|
+
* a recipe that requires it. Used to refcount removal.
|
|
98
|
+
*/
|
|
99
|
+
requestedBy: z
|
|
100
|
+
.array(z.string().min(1, "must not be empty"))
|
|
101
|
+
.min(1, "must name at least one holder"),
|
|
102
|
+
/** Whether the holder relationship is a co-subscription or a build dependency. */
|
|
103
|
+
kind: z.enum(LOCK_KINDS),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** The lockfile schema. */
|
|
107
|
+
export const lockfileSchema = z
|
|
108
|
+
.strictObject({
|
|
109
|
+
formatVersion: formatVersionSchema,
|
|
110
|
+
/** Every repo the locked recipes came from, keyed by short name. */
|
|
111
|
+
repos: z.record(repoNameSchema, lockedRepoSchema),
|
|
112
|
+
/** Every locked recipe, keyed `namespace/recipe`. */
|
|
113
|
+
recipes: z.record(recipeKeySchema, lockedRecipeSchema),
|
|
114
|
+
})
|
|
115
|
+
.superRefine((lock, ctx) => {
|
|
116
|
+
// A recipe pointing at a repo the lockfile does not describe cannot be
|
|
117
|
+
// restored, so name the pair rather than failing later at fetch time.
|
|
118
|
+
for (const [key, entry] of Object.entries(lock.recipes)) {
|
|
119
|
+
if (!Object.hasOwn(lock.repos, entry.repo)) {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: "custom",
|
|
122
|
+
path: ["recipes", key, "repo"],
|
|
123
|
+
message:
|
|
124
|
+
`names the repo '${entry.repo}', which this lockfile does not describe ` +
|
|
125
|
+
`under 'repos'`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** A validated lockfile. */
|
|
132
|
+
export type Lockfile = z.infer<typeof lockfileSchema>;
|
|
133
|
+
|
|
134
|
+
/** One locked repo entry. */
|
|
135
|
+
export type LockedRepo = z.infer<typeof lockedRepoSchema>;
|
|
136
|
+
|
|
137
|
+
/** One locked recipe entry. */
|
|
138
|
+
export type LockedRecipe = z.infer<typeof lockedRecipeSchema>;
|
|
139
|
+
|
|
140
|
+
/** How a locked recipe entered the project. */
|
|
141
|
+
export type LockKind = (typeof LOCK_KINDS)[number];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validates a parsed lockfile, throwing a ConfigError that names the file and
|
|
145
|
+
* the path of every bad field.
|
|
146
|
+
*
|
|
147
|
+
* @param value - The parsed contents of the lockfile.
|
|
148
|
+
* @param sourceLabel - The lockfile's path, named in error messages.
|
|
149
|
+
*/
|
|
150
|
+
export function parseLockfile(value: unknown, sourceLabel: string): Lockfile {
|
|
151
|
+
return parseFormat(lockfileSchema, value, sourceLabel, "lockfile");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** An empty lockfile, for a project that has locked nothing yet. */
|
|
155
|
+
export function createEmptyLockfile(): Lockfile {
|
|
156
|
+
return { formatVersion: 1, repos: {}, recipes: {} };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Serializes a lockfile for writing, with every object key sorted so the
|
|
161
|
+
* committed file changes only when its content genuinely does.
|
|
162
|
+
*
|
|
163
|
+
* @param lockfile - The lockfile to write.
|
|
164
|
+
*/
|
|
165
|
+
export function stringifyLockfile(lockfile: Lockfile): string {
|
|
166
|
+
return stableJsonStringify(lockfile);
|
|
167
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free regular expressions shared by the Repositories on-disk
|
|
3
|
+
* formats, the ref parser and the sous config schema.
|
|
4
|
+
*
|
|
5
|
+
* This module deliberately imports nothing, so `config-schema.ts` can reuse the
|
|
6
|
+
* patterns without pulling in zod schemas, semver, or the rest of the repos
|
|
7
|
+
* layer. `formats/common.ts` re-exports everything here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lowercase kebab-case identifier: starts with a letter, then letters, digits
|
|
12
|
+
* or hyphens. Used for repo short names, namespace names and recipe names
|
|
13
|
+
* (`sous-recipes`, `tool-usage`, `automated-browser-tasks`).
|
|
14
|
+
*/
|
|
15
|
+
export const KEBAB_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
16
|
+
|
|
17
|
+
/** A repo's configured short name, as used by the `repo:` ref qualifier. */
|
|
18
|
+
export const REPO_NAME_PATTERN = KEBAB_NAME_PATTERN;
|
|
19
|
+
|
|
20
|
+
/** A namespace name. */
|
|
21
|
+
export const NAMESPACE_NAME_PATTERN = KEBAB_NAME_PATTERN;
|
|
22
|
+
|
|
23
|
+
/** A recipe name, unique within its namespace. */
|
|
24
|
+
export const RECIPE_NAME_PATTERN = KEBAB_NAME_PATTERN;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A ref key: either a bare namespace (`workflow`) or a fully qualified recipe
|
|
28
|
+
* (`workflow/task-files`). Never carries a repo qualifier or a version range.
|
|
29
|
+
*/
|
|
30
|
+
export const REF_KEY_PATTERN = /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)?$/;
|
|
31
|
+
|
|
32
|
+
/** A recipe key, which always has both segments (`workflow/task-files`). */
|
|
33
|
+
export const RECIPE_KEY_PATTERN = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A repository's canonical identity: the host, then the path it lives at, all
|
|
37
|
+
* lowercase (`github.com/sous-io/sous-recipes`). It is what every machine-wide
|
|
38
|
+
* key uses, because a project's short name for a repository is its own label
|
|
39
|
+
* and no other project has to agree with it.
|
|
40
|
+
*/
|
|
41
|
+
export const REPO_IDENTITY_PATTERN = /^[^\s/]+(\/[^\s/]+)+$/;
|
|
42
|
+
|
|
43
|
+
/** A content hash, written as the algorithm name followed by lowercase hex. */
|
|
44
|
+
export const CONTENT_HASH_PATTERN = /^sha256-[0-9a-f]{64}$/;
|
|
45
|
+
|
|
46
|
+
/** An environment variable name, as declared by a recipe variable definition. */
|
|
47
|
+
export const ENV_VAR_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
48
|
+
|
|
49
|
+
/** A camelCase variable name, as declared by a recipe variable definition. */
|
|
50
|
+
export const VARIABLE_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An ISO 8601 timestamp carrying an explicit offset (`Z` or `+hh:mm`). Machine
|
|
54
|
+
* written timestamps come from `new Date().toISOString()`, which matches.
|
|
55
|
+
*/
|
|
56
|
+
export const ISO_TIMESTAMP_PATTERN =
|
|
57
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|