@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,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolver.
|
|
3
|
+
*
|
|
4
|
+
* Given what a project asked for (its subscriptions, or the refs named on a
|
|
5
|
+
* command line), the resolver decides exactly which recipe versions that means.
|
|
6
|
+
* It works the way apt does: a ref is looked up across the cached indexes of
|
|
7
|
+
* EVERY added repository at once, and a ref that genuinely exists in more than
|
|
8
|
+
* one of them is an error demanding the qualified form, never a silent
|
|
9
|
+
* first-match-wins.
|
|
10
|
+
*
|
|
11
|
+
* It then walks the dependency closure. Each resolved recipe's manifest names
|
|
12
|
+
* more refs, through `depends` (fetched and addressable, but not added to the
|
|
13
|
+
* project) and `subscribes` (co-subscribed with full semantics), and those refs
|
|
14
|
+
* resolve the same way. The manifest lives inside the recipe's own files, so
|
|
15
|
+
* the caller supplies a loader; that is also the seam where a caller decides
|
|
16
|
+
* whether it is willing to fetch anything at this point.
|
|
17
|
+
*
|
|
18
|
+
* Two things the resolver deliberately does NOT do: it never downloads
|
|
19
|
+
* anything, and it never resolves against a repository the project has not
|
|
20
|
+
* added. A dependency on an unknown repository comes back as a `MissingRepo`
|
|
21
|
+
* carrying its provenance, so the trust layer can ask about it by name.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import semver from "semver";
|
|
25
|
+
import { ConfigError } from "../errors.js";
|
|
26
|
+
import type { IndexFile } from "./formats/index-file.js";
|
|
27
|
+
import type { LockKind } from "./formats/lockfile.js";
|
|
28
|
+
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
29
|
+
import {
|
|
30
|
+
dependencyRefKey,
|
|
31
|
+
dependencyRepoUrl,
|
|
32
|
+
formatRef,
|
|
33
|
+
parseDependencyRef,
|
|
34
|
+
refKey,
|
|
35
|
+
type DependencyRef,
|
|
36
|
+
type ParsedRef,
|
|
37
|
+
} from "./ref.js";
|
|
38
|
+
import { shortNameFromIdentity } from "./identity.js";
|
|
39
|
+
import type { IndexDependency } from "./formats/index-file.js";
|
|
40
|
+
|
|
41
|
+
/** The `requestedBy` holder meaning "the project asked for this directly". */
|
|
42
|
+
export const PROJECT_REQUESTER = "project";
|
|
43
|
+
|
|
44
|
+
/** One thing to resolve. */
|
|
45
|
+
export type RefRequest = {
|
|
46
|
+
/** The ref, already parsed. */
|
|
47
|
+
ref: ParsedRef;
|
|
48
|
+
/** Whether prerelease versions may take part in range matching. */
|
|
49
|
+
prerelease?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Who asked: the literal "project" for a subscription the project holds
|
|
52
|
+
* directly, or the recipe key of the recipe whose manifest asked.
|
|
53
|
+
*/
|
|
54
|
+
requestedBy: string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether this is a co-subscription or a build dependency. Defaults to
|
|
57
|
+
* "subscribes", which is what a project's own subscriptions are.
|
|
58
|
+
*/
|
|
59
|
+
kind?: LockKind;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** What the resolver needs to know about a repository the project has added. */
|
|
63
|
+
export type ResolverRepo = {
|
|
64
|
+
/** Where the repository lives. */
|
|
65
|
+
url: string;
|
|
66
|
+
/**
|
|
67
|
+
* The repository's canonical identity, `<host>/<owner path>/<name>`. A
|
|
68
|
+
* dependency that names another repository names it by location, so this is
|
|
69
|
+
* what decides whether the project has already added it, whatever short name
|
|
70
|
+
* the project gave it.
|
|
71
|
+
*/
|
|
72
|
+
identity: string;
|
|
73
|
+
/** The provider it names, when it names one. */
|
|
74
|
+
provider?: string;
|
|
75
|
+
/** Whether it prefers a newer in-range version over the locked one. */
|
|
76
|
+
alwaysPull?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loads a resolved recipe's manifest, which is what names its dependencies.
|
|
81
|
+
* Returning undefined means "not available", and the recipe is reported under
|
|
82
|
+
* `missingManifests` rather than having its dependencies walked.
|
|
83
|
+
*/
|
|
84
|
+
export type RecipeManifestLoader = (
|
|
85
|
+
recipe: ResolvedRecipe
|
|
86
|
+
) => Promise<RecipeManifest | undefined> | RecipeManifest | undefined;
|
|
87
|
+
|
|
88
|
+
/** Everything the resolver reads. */
|
|
89
|
+
export type ResolveContext = {
|
|
90
|
+
/** The cached index of every added repository, keyed by its short name. */
|
|
91
|
+
indexes: Map<string, IndexFile>;
|
|
92
|
+
/** The added repositories, keyed by short name; the trust list, in other words. */
|
|
93
|
+
repos: Record<string, ResolverRepo>;
|
|
94
|
+
/** How a resolved recipe's manifest is loaded. */
|
|
95
|
+
loadManifest: RecipeManifestLoader;
|
|
96
|
+
/** Whether prereleases are allowed when a request does not say. Defaults to false. */
|
|
97
|
+
prerelease?: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** One recipe version the resolver settled on. */
|
|
101
|
+
export type ResolvedRecipe = {
|
|
102
|
+
/** The recipe key, `namespace/recipe`. */
|
|
103
|
+
key: string;
|
|
104
|
+
namespace: string;
|
|
105
|
+
name: string;
|
|
106
|
+
/** The short name of the repository it resolves in, as this project calls it. */
|
|
107
|
+
repo: string;
|
|
108
|
+
/** That repository's canonical identity, which the machine-wide store is keyed by. */
|
|
109
|
+
identity: string;
|
|
110
|
+
/** The exact version chosen. */
|
|
111
|
+
version: string;
|
|
112
|
+
/** The content hash the index publishes for that version. */
|
|
113
|
+
hash: string;
|
|
114
|
+
/** The git tag carrying that version. */
|
|
115
|
+
tag: string;
|
|
116
|
+
/** The recipe folder, relative to the repository root. */
|
|
117
|
+
path: string;
|
|
118
|
+
/** Whether anything holds it as a co-subscription; otherwise a build dependency. */
|
|
119
|
+
kind: LockKind;
|
|
120
|
+
/** Everyone holding it: "project", and the key of every recipe that asked. */
|
|
121
|
+
requestedBy: string[];
|
|
122
|
+
/** Every range that had to be satisfied at once, with who asked for it. */
|
|
123
|
+
ranges: Array<{ range: string; requestedBy: string }>;
|
|
124
|
+
/** Whether prereleases took part in the match. */
|
|
125
|
+
prerelease: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* What the repository's index says this exact version depends on, when the
|
|
128
|
+
* index records it. These are the versions the release resolved, so a
|
|
129
|
+
* dependency of an indexed recipe is installed at the version it was
|
|
130
|
+
* published against rather than at whatever its range would reach today.
|
|
131
|
+
*/
|
|
132
|
+
dependencies?: Record<string, IndexDependency>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** A repository something needs that the project has not added. */
|
|
136
|
+
export type MissingRepo = {
|
|
137
|
+
/**
|
|
138
|
+
* The short name to record it under: the one a ref qualified it with, or one
|
|
139
|
+
* derived from its location when a dependency named it by URL.
|
|
140
|
+
*/
|
|
141
|
+
name: string;
|
|
142
|
+
/** Its URL, which a dependency's locator carries. */
|
|
143
|
+
url?: string;
|
|
144
|
+
/** Its canonical identity, when the dependency named a location. */
|
|
145
|
+
identity?: string;
|
|
146
|
+
/** The provider its locator named, when it named one. */
|
|
147
|
+
provider?: string;
|
|
148
|
+
/** Every ref that needs it, and who asked for that ref. */
|
|
149
|
+
requiredBy: Array<{ ref: string; requestedBy: string }>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** What a resolution produced. */
|
|
153
|
+
export type ResolveResult = {
|
|
154
|
+
/** Every recipe version to install, in a stable order (by key). */
|
|
155
|
+
resolved: ResolvedRecipe[];
|
|
156
|
+
/** Repositories a dependency needs that the project has not added. */
|
|
157
|
+
missingRepos: MissingRepo[];
|
|
158
|
+
/** Resolved recipes whose manifest the loader could not produce. */
|
|
159
|
+
missingManifests: string[];
|
|
160
|
+
/** Any dependency cycle found, as the chain of recipe keys forming it. */
|
|
161
|
+
cycles: string[][];
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** A pending piece of work: one ref to resolve on behalf of one holder. */
|
|
165
|
+
type WorkItem = RefRequest & {
|
|
166
|
+
kind: LockKind;
|
|
167
|
+
/**
|
|
168
|
+
* Where the ref said the recipe lives, when a manifest named another
|
|
169
|
+
* repository by location. The project may already have added that repository
|
|
170
|
+
* under any short name at all, so it is matched by identity.
|
|
171
|
+
*/
|
|
172
|
+
remote?: {
|
|
173
|
+
/** The repository's canonical identity. */
|
|
174
|
+
identity: string;
|
|
175
|
+
/** Its HTTPS location, which is what adding it would be handed. */
|
|
176
|
+
url: string;
|
|
177
|
+
/** The provider the locator named. */
|
|
178
|
+
provider?: string;
|
|
179
|
+
/** The dependency exactly as the manifest wrote it, for messages. */
|
|
180
|
+
written: string;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Turns one entry of a manifest's `depends` or `subscribes` into a piece of
|
|
186
|
+
* work.
|
|
187
|
+
*
|
|
188
|
+
* Two things are decided here. A SIBLING ref resolves inside the declaring
|
|
189
|
+
* recipe's own repository, never across the others, because that is what
|
|
190
|
+
* writing it without a location means. And when the repository's index records
|
|
191
|
+
* what this exact version of the parent was released against, that exact
|
|
192
|
+
* version is what gets asked for, rather than whatever the declared range would
|
|
193
|
+
* reach today.
|
|
194
|
+
*
|
|
195
|
+
* @param written - The dependency as the manifest wrote it.
|
|
196
|
+
* @param parent - The recipe whose manifest declared it.
|
|
197
|
+
* @param kind - Whether it was declared as a dependency or a co-subscription.
|
|
198
|
+
*/
|
|
199
|
+
function dependencyRequest(
|
|
200
|
+
written: string,
|
|
201
|
+
parent: ResolvedRecipe,
|
|
202
|
+
kind: LockKind
|
|
203
|
+
): WorkItem {
|
|
204
|
+
const parsed = parseDependencyRef(written);
|
|
205
|
+
const pinned = parent.dependencies?.[dependencyRefKey(parsed)];
|
|
206
|
+
|
|
207
|
+
const ref: ParsedRef = { namespace: parsed.namespace };
|
|
208
|
+
if (parsed.recipe !== undefined) {
|
|
209
|
+
ref.recipe = parsed.recipe;
|
|
210
|
+
const range = pinned?.version ?? parsed.range ?? pinned?.range;
|
|
211
|
+
if (range !== undefined) ref.range = range;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const base = {
|
|
215
|
+
requestedBy: parent.key,
|
|
216
|
+
kind,
|
|
217
|
+
...(parent.prerelease ? { prerelease: true } : {}),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (parsed.kind === "sibling") {
|
|
221
|
+
return { ref: { ...ref, repo: parent.repo }, ...base };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ref,
|
|
226
|
+
...base,
|
|
227
|
+
remote: {
|
|
228
|
+
identity: pinned?.repo ?? parsed.canonicalRepo!,
|
|
229
|
+
url: dependencyRepoUrl(parsed)!,
|
|
230
|
+
...(parsed.provider === undefined ? {} : { provider: parsed.provider }),
|
|
231
|
+
written: written.trim(),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The short name of the added repository with this identity, or undefined when
|
|
238
|
+
* the project has added none. Short names are a project's own labels, so the
|
|
239
|
+
* identity is what a dependency is matched on.
|
|
240
|
+
*
|
|
241
|
+
* @param repos - The repositories the project has added.
|
|
242
|
+
* @param identity - The canonical identity to look for.
|
|
243
|
+
*/
|
|
244
|
+
function repoNamedByIdentity(
|
|
245
|
+
repos: Record<string, ResolverRepo>,
|
|
246
|
+
identity: string
|
|
247
|
+
): string | undefined {
|
|
248
|
+
for (const [name, entry] of Object.entries(repos)) {
|
|
249
|
+
if (entry.identity === identity) return name;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A short name for a repository the project has not added yet, derived from its
|
|
256
|
+
* location and made unique against the names already in use.
|
|
257
|
+
*
|
|
258
|
+
* @param repos - The repositories the project has added.
|
|
259
|
+
* @param identity - The canonical identity of the repository being named.
|
|
260
|
+
*/
|
|
261
|
+
function proposeRepoName(
|
|
262
|
+
repos: Record<string, ResolverRepo>,
|
|
263
|
+
identity: string
|
|
264
|
+
): string {
|
|
265
|
+
const base = shortNameFromIdentity(identity);
|
|
266
|
+
if (!Object.hasOwn(repos, base)) return base;
|
|
267
|
+
for (let suffix = 2; ; suffix++) {
|
|
268
|
+
const candidate = `${base}-${suffix}`;
|
|
269
|
+
if (!Object.hasOwn(repos, candidate)) return candidate;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolves a set of refs and the whole dependency closure beneath them.
|
|
275
|
+
*
|
|
276
|
+
* @param requests - What the project (or the command line) asked for.
|
|
277
|
+
* @param context - The cached indexes, the added repositories and a manifest loader.
|
|
278
|
+
*/
|
|
279
|
+
export async function resolveRefs(
|
|
280
|
+
requests: RefRequest[],
|
|
281
|
+
context: ResolveContext
|
|
282
|
+
): Promise<ResolveResult> {
|
|
283
|
+
const resolved = new Map<string, ResolvedRecipe>();
|
|
284
|
+
const missingRepos = new Map<string, MissingRepo>();
|
|
285
|
+
const missingManifests = new Set<string>();
|
|
286
|
+
const cycles: string[][] = [];
|
|
287
|
+
/** Which recipe pulled each recipe in, used to find a cycle's chain. */
|
|
288
|
+
const parents = new Map<string, string>();
|
|
289
|
+
/** Recipes whose manifest has already been walked, at the version noted. */
|
|
290
|
+
const walked = new Map<string, string>();
|
|
291
|
+
|
|
292
|
+
const queue: WorkItem[] = requests.map((request) => ({
|
|
293
|
+
...request,
|
|
294
|
+
kind: request.kind ?? "subscribes",
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
while (queue.length > 0) {
|
|
298
|
+
const item = queue.shift()!;
|
|
299
|
+
|
|
300
|
+
// A ref naming a repository the project has not added never resolves and
|
|
301
|
+
// never downloads anything; it comes back as a missing repository instead,
|
|
302
|
+
// so the trust layer can ask about it by name.
|
|
303
|
+
const qualifier = item.ref.repo;
|
|
304
|
+
if (qualifier !== undefined && !Object.hasOwn(context.repos, qualifier)) {
|
|
305
|
+
recordMissingRepo(missingRepos, {
|
|
306
|
+
name: qualifier,
|
|
307
|
+
requiredBy: [{ ref: formatRef(item.ref), requestedBy: item.requestedBy }],
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A dependency that named another repository by location is matched on that
|
|
313
|
+
// location, so a project that already added it under some other short name
|
|
314
|
+
// resolves against the copy it has. One it has not added carries its URL and
|
|
315
|
+
// its provider, which is everything the trust round needs to offer to add it.
|
|
316
|
+
if (item.remote !== undefined) {
|
|
317
|
+
const added = repoNamedByIdentity(context.repos, item.remote.identity);
|
|
318
|
+
if (added === undefined) {
|
|
319
|
+
recordMissingRepo(missingRepos, {
|
|
320
|
+
name: proposeRepoName(context.repos, item.remote.identity),
|
|
321
|
+
url: item.remote.url,
|
|
322
|
+
identity: item.remote.identity,
|
|
323
|
+
...(item.remote.provider === undefined ? {} : { provider: item.remote.provider }),
|
|
324
|
+
requiredBy: [{ ref: item.remote.written, requestedBy: item.requestedBy }],
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
item.ref = { ...item.ref, repo: added };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (item.ref.recipe === undefined) {
|
|
332
|
+
queue.push(...expandNamespace(item, context));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const recipe = resolveRecipeRef(item, context, resolved);
|
|
337
|
+
resolved.set(recipe.key, recipe);
|
|
338
|
+
|
|
339
|
+
if (item.requestedBy !== PROJECT_REQUESTER && !parents.has(recipe.key)) {
|
|
340
|
+
parents.set(recipe.key, item.requestedBy);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Walking a manifest is only worth doing once per version. A second holder
|
|
344
|
+
// of an already-walked version adds itself and stops there; that is also
|
|
345
|
+
// what keeps a dependency cycle finite.
|
|
346
|
+
if (walked.get(recipe.key) === recipe.version) {
|
|
347
|
+
const cycle = findCycle(recipe.key, item.requestedBy, parents);
|
|
348
|
+
if (cycle !== undefined) cycles.push(cycle);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
walked.set(recipe.key, recipe.version);
|
|
352
|
+
|
|
353
|
+
const manifest = await context.loadManifest(recipe);
|
|
354
|
+
if (manifest === undefined) {
|
|
355
|
+
missingManifests.add(recipe.key);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const [kind, refs] of [
|
|
360
|
+
["depends", manifest.depends ?? []],
|
|
361
|
+
["subscribes", manifest.subscribes ?? []],
|
|
362
|
+
] as Array<[LockKind, string[]]>) {
|
|
363
|
+
for (const written of refs) {
|
|
364
|
+
queue.push(dependencyRequest(written, recipe, kind));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// A recipe whose version was narrowed by a later holder has had TWO of its
|
|
370
|
+
// versions walked, and the dependencies discovered from the version that was
|
|
371
|
+
// replaced are still sitting in `resolved`. Walk the closure once more over
|
|
372
|
+
// the versions actually settled on, and keep only what that reaches.
|
|
373
|
+
await keepOnlyReachable(resolved, context);
|
|
374
|
+
|
|
375
|
+
const ordered = [...resolved.values()].sort((left, right) =>
|
|
376
|
+
left.key < right.key ? -1 : left.key > right.key ? 1 : 0
|
|
377
|
+
);
|
|
378
|
+
for (const recipe of ordered) recipe.requestedBy.sort();
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
resolved: ordered,
|
|
382
|
+
missingRepos: [...missingRepos.values()].sort((left, right) =>
|
|
383
|
+
left.name < right.name ? -1 : 1
|
|
384
|
+
),
|
|
385
|
+
missingManifests: [...missingManifests].sort(),
|
|
386
|
+
cycles,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Drops every resolved recipe the settled closure no longer reaches, and trims
|
|
392
|
+
* the holders and ranges of the ones that stay.
|
|
393
|
+
*
|
|
394
|
+
* The walk resolves refs in the order it meets them, so a recipe can be walked
|
|
395
|
+
* at one version and then walked again at a lower one once a second holder
|
|
396
|
+
* narrows its range. The lower version is what the closure settles on, but the
|
|
397
|
+
* dependencies discovered from the higher one are already in `resolved`, held by
|
|
398
|
+
* a parent that no longer declares them. Installing those is wrong twice over:
|
|
399
|
+
* they are content nothing asked for, and the lockfile would record a holder
|
|
400
|
+
* that does not hold them.
|
|
401
|
+
*
|
|
402
|
+
* So the closure is walked once more over the versions actually settled on. This
|
|
403
|
+
* re-reads manifests that have already been read, which the loader serves from
|
|
404
|
+
* the store; nothing is fetched. A recipe the loader cannot produce a manifest
|
|
405
|
+
* for keeps everything it reached, since the alternative is dropping a
|
|
406
|
+
* dependency because a manifest was unreadable.
|
|
407
|
+
*
|
|
408
|
+
* Versions are NOT re-picked. Trimming can only remove constraints, so the
|
|
409
|
+
* version already chosen still satisfies every holder that remains; re-picking
|
|
410
|
+
* could raise it and undo the narrowing that made this pass necessary.
|
|
411
|
+
*
|
|
412
|
+
* @param resolved - What the walk produced, edited in place.
|
|
413
|
+
* @param context - The manifest loader.
|
|
414
|
+
*/
|
|
415
|
+
async function keepOnlyReachable(
|
|
416
|
+
resolved: Map<string, ResolvedRecipe>,
|
|
417
|
+
context: ResolveContext
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
/** Who declares each key, and under which kind, in the settled closure. */
|
|
420
|
+
const holders = new Map<string, Map<string, LockKind>>();
|
|
421
|
+
|
|
422
|
+
/** Records one declaration, and reports whether the child is newly reached. */
|
|
423
|
+
const declare = (parent: string, child: string, kind: LockKind): boolean => {
|
|
424
|
+
const existing = holders.get(child);
|
|
425
|
+
if (existing === undefined) {
|
|
426
|
+
holders.set(child, new Map([[parent, kind]]));
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const previous = existing.get(parent);
|
|
430
|
+
existing.set(parent, previous === "subscribes" || kind === "subscribes" ? "subscribes" : kind);
|
|
431
|
+
return false;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const queue: string[] = [];
|
|
435
|
+
for (const recipe of resolved.values()) {
|
|
436
|
+
if (!recipe.requestedBy.includes(PROJECT_REQUESTER)) continue;
|
|
437
|
+
if (declare(PROJECT_REQUESTER, recipe.key, "subscribes")) queue.push(recipe.key);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
while (queue.length > 0) {
|
|
441
|
+
const key = queue.shift()!;
|
|
442
|
+
const recipe = resolved.get(key);
|
|
443
|
+
if (recipe === undefined) continue;
|
|
444
|
+
|
|
445
|
+
let manifest;
|
|
446
|
+
try {
|
|
447
|
+
manifest = await context.loadManifest(recipe);
|
|
448
|
+
} catch {
|
|
449
|
+
manifest = undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (manifest === undefined) {
|
|
453
|
+
// An unreadable manifest is already reported as a missing manifest. Keep
|
|
454
|
+
// everything this recipe held rather than dropping a dependency over it.
|
|
455
|
+
for (const other of resolved.values()) {
|
|
456
|
+
if (!other.requestedBy.includes(key)) continue;
|
|
457
|
+
const kind = other.kind;
|
|
458
|
+
if (declare(key, other.key, kind)) queue.push(other.key);
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const [kind, refs] of [
|
|
464
|
+
["depends", manifest.depends ?? []],
|
|
465
|
+
["subscribes", manifest.subscribes ?? []],
|
|
466
|
+
] as Array<[LockKind, string[]]>) {
|
|
467
|
+
for (const written of refs) {
|
|
468
|
+
let parsed: DependencyRef;
|
|
469
|
+
try {
|
|
470
|
+
parsed = parseDependencyRef(written);
|
|
471
|
+
} catch {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
// A namespace ref means every recipe in it, exactly as the walk expanded it.
|
|
475
|
+
const targets =
|
|
476
|
+
parsed.recipe === undefined
|
|
477
|
+
? [...resolved.keys()].filter((entry) =>
|
|
478
|
+
entry.startsWith(`${parsed.namespace}/`)
|
|
479
|
+
)
|
|
480
|
+
: [dependencyRefKey(parsed)];
|
|
481
|
+
for (const target of targets) {
|
|
482
|
+
if (!resolved.has(target)) continue;
|
|
483
|
+
if (declare(key, target, kind)) queue.push(target);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
for (const [key, recipe] of [...resolved]) {
|
|
490
|
+
const reached = holders.get(key);
|
|
491
|
+
if (reached === undefined) {
|
|
492
|
+
resolved.delete(key);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
recipe.requestedBy = [...reached.keys()];
|
|
496
|
+
recipe.ranges = recipe.ranges.filter((entry) => reached.has(entry.requestedBy));
|
|
497
|
+
recipe.kind = [...reached.values()].includes("subscribes") ? "subscribes" : "depends";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Records one more reason a repository is needed. */
|
|
502
|
+
function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo): void {
|
|
503
|
+
const existing = into.get(missing.name);
|
|
504
|
+
if (existing === undefined) {
|
|
505
|
+
into.set(missing.name, missing);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
for (const entry of missing.requiredBy) {
|
|
509
|
+
const duplicate = existing.requiredBy.some(
|
|
510
|
+
(known) => known.ref === entry.ref && known.requestedBy === entry.requestedBy
|
|
511
|
+
);
|
|
512
|
+
if (!duplicate) existing.requiredBy.push(entry);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Expands a namespace ref into one request per recipe in that namespace. A
|
|
518
|
+
* namespace subscription means "everything in here, including whatever is added
|
|
519
|
+
* later", so the expansion happens fresh on every resolution.
|
|
520
|
+
*
|
|
521
|
+
* @param item - The namespace request.
|
|
522
|
+
* @param context - The cached indexes and added repositories.
|
|
523
|
+
*/
|
|
524
|
+
function expandNamespace(item: WorkItem, context: ResolveContext): WorkItem[] {
|
|
525
|
+
const { namespace } = item.ref;
|
|
526
|
+
const qualifier = item.ref.repo;
|
|
527
|
+
const expanded: WorkItem[] = [];
|
|
528
|
+
const seen = new Set<string>();
|
|
529
|
+
let namespaceFound = false;
|
|
530
|
+
|
|
531
|
+
for (const [repoName, index] of context.indexes) {
|
|
532
|
+
if (qualifier !== undefined && repoName !== qualifier) continue;
|
|
533
|
+
if (!Object.hasOwn(index.namespaces, namespace)) continue;
|
|
534
|
+
namespaceFound = true;
|
|
535
|
+
|
|
536
|
+
for (const key of Object.keys(index.recipes)) {
|
|
537
|
+
if (!key.startsWith(`${namespace}/`)) continue;
|
|
538
|
+
if (seen.has(key)) continue;
|
|
539
|
+
seen.add(key);
|
|
540
|
+
expanded.push({
|
|
541
|
+
ref: {
|
|
542
|
+
namespace,
|
|
543
|
+
recipe: key.slice(namespace.length + 1),
|
|
544
|
+
...(qualifier === undefined ? {} : { repo: qualifier }),
|
|
545
|
+
// A namespace ref cannot carry a range when it is WRITTEN, but a
|
|
546
|
+
// namespace subscription entry can carry one, and a caller builds the
|
|
547
|
+
// request from that entry. When it does, the range applies to every
|
|
548
|
+
// recipe in the namespace; that is how the built-in `core`
|
|
549
|
+
// subscription stays pinned to the running sous version.
|
|
550
|
+
...(item.ref.range === undefined ? {} : { range: item.ref.range }),
|
|
551
|
+
},
|
|
552
|
+
requestedBy: item.requestedBy,
|
|
553
|
+
kind: item.kind,
|
|
554
|
+
...(item.prerelease === undefined ? {} : { prerelease: item.prerelease }),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!namespaceFound) throw unknownRefError(item, context, "namespace");
|
|
560
|
+
|
|
561
|
+
return expanded;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Resolves one recipe ref against every added repository, merging it with
|
|
566
|
+
* anything already resolved for the same recipe key.
|
|
567
|
+
*
|
|
568
|
+
* @param item - The request being resolved.
|
|
569
|
+
* @param context - The cached indexes and added repositories.
|
|
570
|
+
* @param resolved - What is resolved so far, so ranges accumulate.
|
|
571
|
+
*/
|
|
572
|
+
function resolveRecipeRef(
|
|
573
|
+
item: WorkItem,
|
|
574
|
+
context: ResolveContext,
|
|
575
|
+
resolved: Map<string, ResolvedRecipe>
|
|
576
|
+
): ResolvedRecipe {
|
|
577
|
+
const key = refKey(item.ref);
|
|
578
|
+
const qualifier = item.ref.repo;
|
|
579
|
+
|
|
580
|
+
const candidates: Array<{ repo: string; index: IndexFile }> = [];
|
|
581
|
+
for (const [repoName, index] of context.indexes) {
|
|
582
|
+
if (qualifier !== undefined && repoName !== qualifier) continue;
|
|
583
|
+
if (Object.hasOwn(index.recipes, key)) candidates.push({ repo: repoName, index });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (candidates.length === 0) throw unknownRefError(item, context, "recipe");
|
|
587
|
+
if (candidates.length > 1) throw ambiguousRefError(item, candidates.map((c) => c.repo));
|
|
588
|
+
|
|
589
|
+
const chosen = candidates[0]!;
|
|
590
|
+
const entry = chosen.index.recipes[key]!;
|
|
591
|
+
const previous = resolved.get(key);
|
|
592
|
+
|
|
593
|
+
if (previous !== undefined && previous.repo !== chosen.repo) {
|
|
594
|
+
throw new ConfigError(
|
|
595
|
+
`The recipe '${key}' is being taken from two different repositories at once: ` +
|
|
596
|
+
`'${previous.repo}' and '${chosen.repo}'.\n` +
|
|
597
|
+
` Qualify the refs that ask for it, so each one says which repository it means.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const ranges = [...(previous?.ranges ?? [])];
|
|
602
|
+
const range = item.ref.range ?? "*";
|
|
603
|
+
if (!ranges.some((known) => known.range === range && known.requestedBy === item.requestedBy)) {
|
|
604
|
+
ranges.push({ range, requestedBy: item.requestedBy });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const prerelease =
|
|
608
|
+
(previous?.prerelease ?? false) ||
|
|
609
|
+
(item.prerelease ?? context.prerelease ?? false);
|
|
610
|
+
|
|
611
|
+
const version = pickVersion(key, entry, ranges, prerelease);
|
|
612
|
+
const versionEntry = entry.versions[version]!;
|
|
613
|
+
|
|
614
|
+
const requestedBy = [...(previous?.requestedBy ?? [])];
|
|
615
|
+
if (!requestedBy.includes(item.requestedBy)) requestedBy.push(item.requestedBy);
|
|
616
|
+
|
|
617
|
+
const namespace = key.slice(0, key.indexOf("/"));
|
|
618
|
+
return {
|
|
619
|
+
key,
|
|
620
|
+
namespace,
|
|
621
|
+
name: key.slice(namespace.length + 1),
|
|
622
|
+
repo: chosen.repo,
|
|
623
|
+
identity: context.repos[chosen.repo]?.identity ?? chosen.repo,
|
|
624
|
+
version,
|
|
625
|
+
hash: versionEntry.hash,
|
|
626
|
+
tag: versionEntry.tag,
|
|
627
|
+
path: entry.path,
|
|
628
|
+
...(versionEntry.dependencies === undefined
|
|
629
|
+
? {}
|
|
630
|
+
: { dependencies: versionEntry.dependencies }),
|
|
631
|
+
// A recipe held as a co-subscription by anyone is a co-subscription; a
|
|
632
|
+
// build dependency only stays one while nothing subscribes to it.
|
|
633
|
+
kind: previous?.kind === "subscribes" || item.kind === "subscribes" ? "subscribes" : "depends",
|
|
634
|
+
requestedBy,
|
|
635
|
+
ranges,
|
|
636
|
+
prerelease,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Picks the highest published version satisfying every range at once.
|
|
642
|
+
* Prereleases stay out of the match unless they were opted into.
|
|
643
|
+
*
|
|
644
|
+
* @param key - The recipe key, for error messages.
|
|
645
|
+
* @param entry - The recipe's index entry.
|
|
646
|
+
* @param ranges - Every range that has to hold, with who asked for it.
|
|
647
|
+
* @param prerelease - Whether prereleases may match.
|
|
648
|
+
*/
|
|
649
|
+
function pickVersion(
|
|
650
|
+
key: string,
|
|
651
|
+
entry: IndexFile["recipes"][string],
|
|
652
|
+
ranges: Array<{ range: string; requestedBy: string }>,
|
|
653
|
+
prerelease: boolean
|
|
654
|
+
): string {
|
|
655
|
+
const published = Object.keys(entry.versions);
|
|
656
|
+
const eligible = prerelease
|
|
657
|
+
? published
|
|
658
|
+
: published.filter((version) => entry.versions[version]!.prerelease !== true);
|
|
659
|
+
|
|
660
|
+
let candidates = eligible;
|
|
661
|
+
for (const { range } of ranges) {
|
|
662
|
+
candidates = candidates.filter((version) =>
|
|
663
|
+
semver.satisfies(version, range, { includePrerelease: prerelease })
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
|
|
668
|
+
if (best !== null) return best;
|
|
669
|
+
|
|
670
|
+
const asked = ranges
|
|
671
|
+
.map(({ range, requestedBy }) => ` ${range} (required by ${requestedBy})`)
|
|
672
|
+
.join("\n");
|
|
673
|
+
const available = sortVersions(published).join(", ");
|
|
674
|
+
const prereleaseNote = prerelease
|
|
675
|
+
? ""
|
|
676
|
+
: "\n Prerelease versions were not considered. A subscription may opt into them " +
|
|
677
|
+
"with 'prerelease: true', and 'sous subscribe' with '--prerelease'.";
|
|
678
|
+
|
|
679
|
+
throw new ConfigError(
|
|
680
|
+
`No published version of '${key}' satisfies what was asked for.\n` +
|
|
681
|
+
` Version ${ranges.length === 1 ? "range" : "ranges"} asked for:\n${asked}\n` +
|
|
682
|
+
` Versions this repository publishes: ${available}.${prereleaseNote}`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Sorts version strings newest first, leaving anything unparsable at the end. */
|
|
687
|
+
function sortVersions(versions: string[]): string[] {
|
|
688
|
+
return [...versions].sort((left, right) => semver.rcompare(left, right, { loose: true }));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Builds the error for a ref that no added repository publishes. It names the
|
|
693
|
+
* repositories that were searched, because "not found" almost always means
|
|
694
|
+
* "the repository publishing it has not been added yet".
|
|
695
|
+
*
|
|
696
|
+
* @param item - The request that could not be resolved.
|
|
697
|
+
* @param context - The cached indexes and added repositories.
|
|
698
|
+
* @param what - Whether a namespace or a recipe was being looked for.
|
|
699
|
+
*/
|
|
700
|
+
function unknownRefError(
|
|
701
|
+
item: WorkItem,
|
|
702
|
+
context: ResolveContext,
|
|
703
|
+
what: "namespace" | "recipe"
|
|
704
|
+
): ConfigError {
|
|
705
|
+
const written = item.remote?.written ?? formatRef(item.ref);
|
|
706
|
+
const searched = [...context.indexes.keys()];
|
|
707
|
+
const where =
|
|
708
|
+
searched.length === 0
|
|
709
|
+
? " This project has added no repositories yet."
|
|
710
|
+
: ` Repositories searched: ${searched.join(", ")}.`;
|
|
711
|
+
const asker =
|
|
712
|
+
item.requestedBy === PROJECT_REQUESTER
|
|
713
|
+
? ""
|
|
714
|
+
: `\n It was required by the recipe '${item.requestedBy}'.`;
|
|
715
|
+
|
|
716
|
+
// When the ref said WHICH repository, the useful answer is what that
|
|
717
|
+
// repository does publish: a dependency that names something it has never
|
|
718
|
+
// heard of is almost always a typo or a recipe that was renamed.
|
|
719
|
+
const named = item.ref.repo;
|
|
720
|
+
const index = named === undefined ? undefined : context.indexes.get(named);
|
|
721
|
+
const publishes =
|
|
722
|
+
index === undefined
|
|
723
|
+
? ""
|
|
724
|
+
: `\n The repository '${named}' publishes: ${
|
|
725
|
+
Object.keys(index.recipes).length === 0
|
|
726
|
+
? "nothing yet"
|
|
727
|
+
: Object.keys(index.recipes).sort().join(", ")
|
|
728
|
+
}.`;
|
|
729
|
+
|
|
730
|
+
return new ConfigError(
|
|
731
|
+
`No added repository publishes the ${what} '${written}'.\n` +
|
|
732
|
+
`${where}${asker}${publishes}\n` +
|
|
733
|
+
` Add the repository that publishes it with 'sous repo add <url>', then try again.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Builds the error for a ref that resolves in more than one repository. Sous
|
|
739
|
+
* never picks a winner; it shows the qualified form for each repository and
|
|
740
|
+
* asks which one was meant.
|
|
741
|
+
*
|
|
742
|
+
* @param item - The ambiguous request.
|
|
743
|
+
* @param repos - The repositories that publish it.
|
|
744
|
+
*/
|
|
745
|
+
function ambiguousRefError(item: WorkItem, repos: string[]): ConfigError {
|
|
746
|
+
const key = refKey(item.ref);
|
|
747
|
+
const range = item.ref.range === undefined ? "" : `@${item.ref.range}`;
|
|
748
|
+
const qualified = repos.map((repo) => ` ${repo}:${key}${range}`).join("\n");
|
|
749
|
+
const asker =
|
|
750
|
+
item.requestedBy === PROJECT_REQUESTER
|
|
751
|
+
? ""
|
|
752
|
+
: ` It was required by the recipe '${item.requestedBy}'.`;
|
|
753
|
+
|
|
754
|
+
return new ConfigError(
|
|
755
|
+
`The ref '${key}' is published by more than one added repository: ${repos.join(", ")}.` +
|
|
756
|
+
`${asker}\n` +
|
|
757
|
+
` Say which repository you mean by qualifying the ref with its name:\n${qualified}`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Finds the chain of recipes leading from a recipe back to itself, or undefined
|
|
763
|
+
* when there is none. A cycle is reported rather than treated as an error: the
|
|
764
|
+
* closure is still finite, and two recipes that co-subscribe to each other are
|
|
765
|
+
* unusual but not broken.
|
|
766
|
+
*
|
|
767
|
+
* @param key - The recipe reached a second time.
|
|
768
|
+
* @param from - The recipe that reached it.
|
|
769
|
+
* @param parents - Who pulled each recipe in.
|
|
770
|
+
*/
|
|
771
|
+
function findCycle(
|
|
772
|
+
key: string,
|
|
773
|
+
from: string,
|
|
774
|
+
parents: Map<string, string>
|
|
775
|
+
): string[] | undefined {
|
|
776
|
+
if (from === PROJECT_REQUESTER) return undefined;
|
|
777
|
+
|
|
778
|
+
const chain = [key];
|
|
779
|
+
let current: string | undefined = from;
|
|
780
|
+
const guard = new Set<string>();
|
|
781
|
+
while (current !== undefined && current !== PROJECT_REQUESTER) {
|
|
782
|
+
chain.push(current);
|
|
783
|
+
if (current === key) return chain.reverse();
|
|
784
|
+
if (guard.has(current)) return undefined;
|
|
785
|
+
guard.add(current);
|
|
786
|
+
current = parents.get(current);
|
|
787
|
+
}
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|