@sous-io/sous 0.2.31 → 0.2.33
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 +7 -6
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +8 -4
- package/docs/markdown/configuration.md +28 -4
- package/docs/markdown/repositories-authoring.md +22 -2
- package/docs/markdown/repositories-consuming.md +40 -11
- package/docs/markdown/repositories-file-formats.md +28 -13
- package/docs/markdown/repositories-quickstart.md +1 -1
- package/docs/markdown/repositories.md +14 -9
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +32 -17
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +8 -2
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +29 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +27 -15
- package/src/commands/lock/rebuild.ts +3 -6
- package/src/commands/namespace/show.ts +2 -2
- package/src/commands/recipe/show.ts +26 -9
- package/src/commands/repo/contribute.ts +32 -23
- package/src/commands/repo/gc.ts +1 -1
- package/src/commands/repo/remove.ts +1 -1
- package/src/commands/repo/search.ts +1 -1
- package/src/commands/vars/ask.ts +75 -54
- package/src/lib/build-service.ts +85 -11
- package/src/lib/config-schema.ts +59 -19
- package/src/lib/include-resolver.ts +307 -69
- package/src/lib/markdown-compiler.ts +273 -81
- package/src/lib/project-scaffold/templates.ts +19 -11
- package/src/lib/repos/catalog-inputs.ts +1 -1
- package/src/lib/repos/catalog.ts +98 -72
- package/src/lib/repos/declarations.ts +22 -12
- package/src/lib/repos/formats/index-file.ts +1 -1
- package/src/lib/repos/formats/recipe-manifest.ts +3 -4
- package/src/lib/repos/lock-service.ts +1 -1
- package/src/lib/repos/locked-namespace-resolver.ts +14 -6
- package/src/lib/repos/locked-recipe-lookup.ts +123 -0
- package/src/lib/repos/locked-recipes.ts +1 -1
- package/src/lib/repos/memories-hash-name.ts +33 -0
- package/src/lib/repos/namespace-resolver.ts +198 -119
- package/src/lib/repos/recipe-config-layers.ts +29 -1
- package/src/lib/repos/recipe-key-matcher.ts +62 -0
- package/src/lib/repos/recipe-memories.ts +175 -0
- package/src/lib/repos/recipe-targets.ts +27 -28
- package/src/lib/repos/ref-search.ts +1 -1
- package/src/lib/repos/release/index-builder.ts +23 -15
- package/src/lib/repos/release/plan.ts +14 -7
- package/src/lib/repos/release/settle.ts +38 -121
- package/src/lib/repos/release/submit-checkout.ts +42 -23
- package/src/lib/repos/release/tags.ts +1 -1
- package/src/lib/repos/resolver.ts +90 -72
- package/src/lib/repos/subscription-service.ts +192 -170
- package/src/lib/settings.ts +37 -33
- package/src/lib/vars/ask.ts +3 -3
- package/src/lib/vars/mappings.ts +17 -9
- package/src/services/ref-resolver/container.ts +107 -0
- package/src/services/ref-resolver/dependency.ts +60 -0
- package/src/services/ref-resolver/format.ts +197 -0
- package/src/services/ref-resolver/glob.ts +115 -0
- package/src/services/ref-resolver/hash-names.ts +155 -0
- package/src/services/ref-resolver/index.ts +55 -0
- package/src/services/ref-resolver/injectable.ts +36 -0
- package/src/services/ref-resolver/location.ts +62 -0
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
- package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
- package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
- package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
- package/src/services/ref-resolver/named-refs.ts +144 -0
- package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
- package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
- package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
- package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
- package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
- package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
- package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
- package/src/services/ref-resolver/parts.ts +115 -0
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
- package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
- package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
- package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
- package/src/services/ref-resolver/ref-helpers.ts +62 -0
- package/src/services/ref-resolver/ref-picker-service.ts +204 -0
- package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
- package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
- package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
- package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
- package/src/services/ref-resolver/source.ts +48 -0
- package/src/services/ref-resolver/tokens.ts +25 -0
- package/src/services/ref-resolver/types.ts +155 -0
- package/src/templating/init-liquid-engine.ts +25 -18
- package/src/lib/refs/find.ts +0 -770
- package/src/lib/refs/index.ts +0 -14
- package/src/lib/refs/parse.ts +0 -844
- package/src/lib/refs/pick.ts +0 -147
- package/src/lib/refs/scopes.ts +0 -95
- package/src/lib/refs/settle.ts +0 -219
package/src/lib/settings.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type DiscoveredConfig,
|
|
15
15
|
} from "./config-discovery.js";
|
|
16
16
|
import { ConfigError } from "./errors.js";
|
|
17
|
+
import { sharedHashNames } from "../services/ref-resolver/index.js";
|
|
17
18
|
import { resolveSousHome } from "./sous-home.js";
|
|
18
19
|
import { validateSettings } from "./config-schema.js";
|
|
19
20
|
import { applyRepoDefaults } from "./repos/defaults.js";
|
|
@@ -148,23 +149,25 @@ type StoreConfig = {
|
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
|
-
*
|
|
152
|
-
* destination directories per content kind. Each destination is `${var}`
|
|
153
|
-
* substituted like any other config path, and a kind may name several so the
|
|
154
|
-
* same recipe feeds more than one agent directory.
|
|
152
|
+
* What a project does with each content kind its subscribed recipes publish.
|
|
155
153
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
154
|
+
* `skills.outputs` lists the destination directories for recipe skills, each
|
|
155
|
+
* `${var}` substituted; the default is `<project root>/.claude/skills`, the
|
|
156
|
+
* project root being the parent of `.sous/`. `memories.first` and
|
|
157
|
+
* `memories.exclude` are lists of globs, or `/.../` regular expressions, over
|
|
158
|
+
* `namespace/recipe` keys.
|
|
160
159
|
*/
|
|
161
|
-
type
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
type RecipesConfig = {
|
|
161
|
+
skills?: {
|
|
162
|
+
/** Where recipe skill bundles are written. */
|
|
163
|
+
outputs?: string[];
|
|
164
|
+
};
|
|
165
|
+
memories?: {
|
|
166
|
+
/** Recipes whose memories come first. */
|
|
167
|
+
first?: string[];
|
|
168
|
+
/** Recipes whose memories are left out. */
|
|
169
|
+
exclude?: string[];
|
|
170
|
+
};
|
|
168
171
|
};
|
|
169
172
|
|
|
170
173
|
/** Configuration for a launchable tool (e.g. claude, codex). */
|
|
@@ -204,9 +207,9 @@ export type Settings = {
|
|
|
204
207
|
/** Knobs for the machine-wide recipe store. */
|
|
205
208
|
store?: StoreConfig;
|
|
206
209
|
/**
|
|
207
|
-
*
|
|
210
|
+
* What the project does with each content kind its subscribed recipes publish.
|
|
208
211
|
*/
|
|
209
|
-
|
|
212
|
+
recipes?: RecipesConfig;
|
|
210
213
|
/**
|
|
211
214
|
* Variable mapping records, keyed by environment variable name, each bound to
|
|
212
215
|
* one recipe variable written as `namespace/recipe/variableName` with an
|
|
@@ -820,30 +823,29 @@ function resolveAnswerLayer(
|
|
|
820
823
|
}
|
|
821
824
|
|
|
822
825
|
/**
|
|
823
|
-
* Built-in `@include` aliases
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
+
* Built-in `@include` aliases: every `#` name the registry holds
|
|
827
|
+
* (`sharedHashNames`), each standing for its directories in this project. They
|
|
828
|
+
* are reserved (user `_aliases` may not start with `#`), so a user alias can
|
|
829
|
+
* never shadow one.
|
|
826
830
|
*
|
|
827
|
-
* -
|
|
831
|
+
* - `#project` is the consuming project's root (`projectRoot`).
|
|
828
832
|
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
833
|
+
* Files that used to be reached through a built-in alias pointing inside the
|
|
834
|
+
* sous package are published as recipes now, and a recipe's files are addressed
|
|
835
|
+
* by its namespace (`@~workflow/task-files/_partials/resume-task.md`),
|
|
836
|
+
* resolved against what the project has pinned. A `~` reference is NOT an
|
|
837
|
+
* alias: it is resolved separately, after the alias map has been tried; see
|
|
838
|
+
* `locked-namespace-resolver.ts`.
|
|
835
839
|
*/
|
|
836
840
|
export function buildBuiltInAliases(scope: VarScope): AliasMap {
|
|
837
|
-
|
|
838
|
-
if (scope.projectRoot) builtIns["~project"] = [scope.projectRoot];
|
|
839
|
-
return builtIns;
|
|
841
|
+
return sharedHashNames().aliasMap(scope as Record<string, string>);
|
|
840
842
|
}
|
|
841
843
|
|
|
842
844
|
/**
|
|
843
845
|
* Resolve the full `@include` alias map: built-ins, then the config's
|
|
844
846
|
* `_aliases` block (user entries prepend, so they are tried first and fall
|
|
845
847
|
* through to built-in bases of the same name). User alias names starting
|
|
846
|
-
* with `~` are
|
|
848
|
+
* with `~` or `#` are a ConfigError (reserved).
|
|
847
849
|
*
|
|
848
850
|
* @param settings - The loaded settings (for the `_aliases` block).
|
|
849
851
|
* @param scope - The resolved settings scope (for ${var} substitution + projectRoot).
|
|
@@ -853,7 +855,9 @@ export function resolveAliases(settings: Settings, scope: VarScope): AliasMap {
|
|
|
853
855
|
builtIns: buildBuiltInAliases(scope),
|
|
854
856
|
userAliases: [settings._aliases],
|
|
855
857
|
scope,
|
|
856
|
-
onError:
|
|
858
|
+
onError: (message) => {
|
|
859
|
+
throw new ConfigError(message);
|
|
860
|
+
},
|
|
857
861
|
});
|
|
858
862
|
}
|
|
859
863
|
|
|
@@ -1002,7 +1006,7 @@ export function resolveCompilation(
|
|
|
1002
1006
|
|
|
1003
1007
|
/* c8 ignore start */
|
|
1004
1008
|
// Glob target: expand pattern into one CompilationTarget per matched file, skipping dirs.
|
|
1005
|
-
// A leading alias (
|
|
1009
|
+
// A leading alias (`#project/skills/**`) expands to one candidate pattern per alias
|
|
1006
1010
|
// base; the first base that matches any files wins, mirroring the first-existing-wins
|
|
1007
1011
|
// rule of @include resolution.
|
|
1008
1012
|
const rawPattern = substituteVarsStrict(target.entryGlob!, targetScope, `${where}.entryGlob`);
|
package/src/lib/vars/ask.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { choicePrompt } from "../../utils/choice-prompt.js";
|
|
|
42
42
|
import { confirmPrompt } from "../../utils/confirm-prompt.js";
|
|
43
43
|
import { valuePrompt } from "../../utils/value-prompt.js";
|
|
44
44
|
import { ENV_VAR_NAME_PATTERN } from "../repos/formats/patterns.js";
|
|
45
|
-
import {
|
|
45
|
+
import { refKey, variableRefOf } from "../../services/ref-resolver/index.js";
|
|
46
46
|
import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
|
|
47
47
|
import {
|
|
48
48
|
definedVariableKey,
|
|
@@ -152,7 +152,7 @@ export interface AskOptions {
|
|
|
152
152
|
* Limit the run to these variables. Each entry is a variable's bare name, its
|
|
153
153
|
* `namespace/recipe.name` key, or its fully qualified
|
|
154
154
|
* `repository:namespace/recipe.name` reference. A command that resolved a
|
|
155
|
-
* reference through `src/
|
|
155
|
+
* reference through `src/services/ref-resolver/` passes the fully qualified form, which is
|
|
156
156
|
* the only spelling that cannot mean two variables at once.
|
|
157
157
|
*/
|
|
158
158
|
only?: string[];
|
|
@@ -182,7 +182,7 @@ export function answerFileFor(definition: VariableDefinition): AnswerFile {
|
|
|
182
182
|
function isNamed(defined: DefinedVariable, only: string[] | undefined): boolean {
|
|
183
183
|
if (only === undefined) return false;
|
|
184
184
|
const key = definedVariableKey(defined);
|
|
185
|
-
const qualified =
|
|
185
|
+
const qualified = refKey(variableRefOf(defined));
|
|
186
186
|
return only.some(
|
|
187
187
|
(name) => name === defined.definition.name || name === key || name === qualified
|
|
188
188
|
);
|
package/src/lib/vars/mappings.ts
CHANGED
|
@@ -27,8 +27,14 @@ import {
|
|
|
27
27
|
updateManagedLayer,
|
|
28
28
|
} from "../repos/managed-layer.js";
|
|
29
29
|
import { VARIABLE_NAME_PATTERN } from "../repos/formats/patterns.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
30
|
+
import {
|
|
31
|
+
RefSource,
|
|
32
|
+
formatRef,
|
|
33
|
+
namespaceOf,
|
|
34
|
+
parseNamedRef,
|
|
35
|
+
repoOf,
|
|
36
|
+
type NamedRef,
|
|
37
|
+
} from "../../services/ref-resolver/index.js";
|
|
32
38
|
import type { DefinedVariable } from "./definition-source.js";
|
|
33
39
|
|
|
34
40
|
/** File name of the machine-written mapping record layer. */
|
|
@@ -85,18 +91,20 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
85
91
|
// ref part is read by the one ref parser and held to the stored form.
|
|
86
92
|
const slash = trimmed.lastIndexOf("/");
|
|
87
93
|
const variable = slash === -1 ? "" : trimmed.slice(slash + 1);
|
|
88
|
-
let ref:
|
|
94
|
+
let ref: NamedRef | undefined;
|
|
89
95
|
try {
|
|
90
|
-
ref =
|
|
96
|
+
ref = parseNamedRef(trimmed.slice(0, Math.max(slash, 0)), RefSource.CommandLine);
|
|
91
97
|
} catch (error) {
|
|
92
98
|
fail((error as Error).message.split("\n")[0]!.replace(/^Invalid ref '[^']*': /, ""));
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
if (recipe === undefined || ref.range !== undefined) {
|
|
100
|
+
if (ref?.kind !== "recipe" || ref.range !== undefined) {
|
|
96
101
|
fail("a target names a namespace, a recipe and a variable, joined by slashes.");
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const repo = repoOf(ref!)?.name;
|
|
104
|
+
const namespace = namespaceOf(ref!)!.name;
|
|
105
|
+
const recipe = ref!.kind === "recipe" ? ref!.name : "";
|
|
106
|
+
if (formatRef(ref!) !== formatRef(ref!).toLowerCase()) {
|
|
107
|
+
fail(`the repository, namespace and recipe in '${formatRef(ref!)}' must be lowercase.`);
|
|
100
108
|
}
|
|
101
109
|
if (!VARIABLE_NAME_PATTERN.test(variable)) {
|
|
102
110
|
fail(
|
|
@@ -105,7 +113,7 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
105
113
|
);
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
const parsed: MappingTarget = { namespace, recipe
|
|
116
|
+
const parsed: MappingTarget = { namespace, recipe, variable };
|
|
109
117
|
if (repo !== undefined) parsed.repo = repo;
|
|
110
118
|
return parsed;
|
|
111
119
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Inversify container of the ref services.
|
|
3
|
+
*
|
|
4
|
+
* Every part is bound once and shared: the parser, every splitter, every
|
|
5
|
+
* pruner, the resolver and the picker. Splitters, pruners and providers are
|
|
6
|
+
* bound many times under one token each (`REF_TOKENS`), and the parser and the
|
|
7
|
+
* resolver receive them as lists, so a plugin adds a splitter, a pruner or a
|
|
8
|
+
* provider by binding one more under the same token on a container it builds
|
|
9
|
+
* with `createRefContainer()`:
|
|
10
|
+
*
|
|
11
|
+
* const container = createRefContainer();
|
|
12
|
+
* container.bind(REF_TOKENS.Splitter).to(MyNewFormSplitter);
|
|
13
|
+
* const resolver = container.get<RefResolverService>(REF_TOKENS.Resolver);
|
|
14
|
+
*
|
|
15
|
+
* The `#` names are bound the same way: one more `HashName` under
|
|
16
|
+
* `REF_TOKENS.HashName` registers a view or a built-in, and a name bound twice
|
|
17
|
+
* is an error when the registry is first read.
|
|
18
|
+
*
|
|
19
|
+
* Callers that have no container of their own use `sharedRefResolver`,
|
|
20
|
+
* `sharedHashNames` and `sharedRefPicker`, which come from one container built the first time they
|
|
21
|
+
* are read.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Container } from "inversify";
|
|
25
|
+
import { builtInProviders } from "../../lib/repos/providers/index.js";
|
|
26
|
+
import { EnvVarSplitter } from "./parser/env-var-splitter.js";
|
|
27
|
+
import { LocationSplitter } from "./parser/location-splitter.js";
|
|
28
|
+
import { NamePathSplitter } from "./parser/name-path-splitter.js";
|
|
29
|
+
import { QuerySplitter } from "./parser/query-splitter.js";
|
|
30
|
+
import { RangeSplitter } from "./parser/range-splitter.js";
|
|
31
|
+
import { RefParser } from "./parser/ref-parser.js";
|
|
32
|
+
import { RepoQualifierSplitter } from "./parser/repo-qualifier-splitter.js";
|
|
33
|
+
import { CommandLinePruner } from "./pruners/command-line-pruner.js";
|
|
34
|
+
import { IncludePruner } from "./pruners/include-pruner.js";
|
|
35
|
+
import { ManifestPruner } from "./pruners/manifest-pruner.js";
|
|
36
|
+
import { ConfigPruner, LockfilePruner } from "./pruners/stored-key-pruner.js";
|
|
37
|
+
import { MemoriesHashName } from "../../lib/repos/memories-hash-name.js";
|
|
38
|
+
import { HashNameRegistry, ProjectHashName } from "./hash-names.js";
|
|
39
|
+
import { RefPickerService } from "./ref-picker-service.js";
|
|
40
|
+
import { RefResolverService } from "./ref-resolver-service.js";
|
|
41
|
+
import { REF_TOKENS } from "./tokens.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds a container holding every ref service. Each call returns a fresh
|
|
45
|
+
* container, so a plugin or a test may bind more splitters or pruners without
|
|
46
|
+
* touching anyone else's.
|
|
47
|
+
*/
|
|
48
|
+
export function createRefContainer(): Container {
|
|
49
|
+
const container = new Container({ defaultScope: "Singleton" });
|
|
50
|
+
|
|
51
|
+
for (const provider of builtInProviders()) {
|
|
52
|
+
container.bind(REF_TOKENS.Provider).toConstantValue(provider);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const splitter of [
|
|
56
|
+
QuerySplitter,
|
|
57
|
+
RangeSplitter,
|
|
58
|
+
LocationSplitter,
|
|
59
|
+
RepoQualifierSplitter,
|
|
60
|
+
NamePathSplitter,
|
|
61
|
+
EnvVarSplitter,
|
|
62
|
+
]) {
|
|
63
|
+
container.bind(REF_TOKENS.Splitter).to(splitter);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const pruner of [
|
|
67
|
+
CommandLinePruner,
|
|
68
|
+
ConfigPruner,
|
|
69
|
+
ManifestPruner,
|
|
70
|
+
LockfilePruner,
|
|
71
|
+
IncludePruner,
|
|
72
|
+
]) {
|
|
73
|
+
container.bind(REF_TOKENS.Pruner).to(pruner);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
container.bind(REF_TOKENS.HashName).to(ProjectHashName);
|
|
77
|
+
container.bind(REF_TOKENS.HashName).to(MemoriesHashName);
|
|
78
|
+
container.bind(REF_TOKENS.HashNames).to(HashNameRegistry);
|
|
79
|
+
container.bind(REF_TOKENS.Parser).to(RefParser);
|
|
80
|
+
container.bind(REF_TOKENS.Resolver).to(RefResolverService);
|
|
81
|
+
container.bind(REF_TOKENS.Picker).to(RefPickerService);
|
|
82
|
+
return container;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The container the shared instances come from, built on first use. */
|
|
86
|
+
let sharedContainer: Container | undefined;
|
|
87
|
+
|
|
88
|
+
/** The one container callers with no container of their own share. */
|
|
89
|
+
export function getSharedRefContainer(): Container {
|
|
90
|
+
sharedContainer ??= createRefContainer();
|
|
91
|
+
return sharedContainer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The shared resolver, for callers that have no container. */
|
|
95
|
+
export function sharedRefResolver(): RefResolverService {
|
|
96
|
+
return getSharedRefContainer().get<RefResolverService>(REF_TOKENS.Resolver);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The shared registry of `#` names, for callers that have no container. */
|
|
100
|
+
export function sharedHashNames(): HashNameRegistry {
|
|
101
|
+
return getSharedRefContainer().get<HashNameRegistry>(REF_TOKENS.HashNames);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The shared picker, for callers that have no container. */
|
|
105
|
+
export function sharedRefPicker(): RefPickerService {
|
|
106
|
+
return getSharedRefContainer().get<RefPickerService>(REF_TOKENS.Picker);
|
|
107
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settling a manifest dependency that reads more than one way, from what is
|
|
3
|
+
* already at hand.
|
|
4
|
+
*
|
|
5
|
+
* A manifest entry usually says exactly what it means. A GitLab URL with nested
|
|
6
|
+
* groups does not say where the project path ends, and a browser URL names a
|
|
7
|
+
* folder, so the parser returns every reading. A release settles such an entry
|
|
8
|
+
* and records the repository it settled on beside each key it resolved
|
|
9
|
+
* (`RecordedDependencies`); a consumer reads that record and never probes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { sharedRefResolver } from "./container.js";
|
|
13
|
+
import {
|
|
14
|
+
isConfirmedByRecord,
|
|
15
|
+
type RecordedDependencies,
|
|
16
|
+
} from "./lookups/recorded-dependency-lookup.js";
|
|
17
|
+
import { RefSource } from "./source.js";
|
|
18
|
+
import type { SousRef } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/** What `settleDependency` may consult, beyond the dependency itself. */
|
|
21
|
+
export type SettleDependencyOptions = {
|
|
22
|
+
/** What the declaring version's index entry records about its dependencies. */
|
|
23
|
+
recorded?: RecordedDependencies;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a reading is already known to be right from what is on this
|
|
26
|
+
* machine: its repository is trusted and its cached index publishes what it
|
|
27
|
+
* names. Used only when the index recorded nothing, and it never fetches.
|
|
28
|
+
*/
|
|
29
|
+
known?: (reading: SousRef) => boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The one reading a manifest dependency settles on, or undefined when it reads
|
|
34
|
+
* more than one way and nothing at hand says which: the declaring recipe's
|
|
35
|
+
* index recorded no answer, and what this machine already knows does not pick
|
|
36
|
+
* exactly one.
|
|
37
|
+
*
|
|
38
|
+
* settleDependency("workflow/alpha");
|
|
39
|
+
* // -> the recipe workflow/alpha
|
|
40
|
+
*
|
|
41
|
+
* @param written - The dependency as the manifest wrote it.
|
|
42
|
+
* @param options - The index's record, and what is known locally.
|
|
43
|
+
* @throws A ConfigError when a manifest may not write the entry that way.
|
|
44
|
+
*/
|
|
45
|
+
export function settleDependency(
|
|
46
|
+
written: string,
|
|
47
|
+
options: SettleDependencyOptions = {}
|
|
48
|
+
): SousRef | undefined {
|
|
49
|
+
const { refs } = sharedRefResolver().parse(written, RefSource.Manifest);
|
|
50
|
+
if (refs.length === 1) return refs[0];
|
|
51
|
+
|
|
52
|
+
const confirmed = refs.filter((ref) => isConfirmedByRecord(ref, options.recorded));
|
|
53
|
+
if (confirmed.length === 1) return confirmed[0];
|
|
54
|
+
|
|
55
|
+
if (options.known !== undefined) {
|
|
56
|
+
const known = refs.filter(options.known);
|
|
57
|
+
if (known.length === 1) return known[0];
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Printing a ref: its key (what everything is stored and compared under) and
|
|
3
|
+
* its canonical written form (how sous prints it).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
builtInProviders,
|
|
8
|
+
providerById,
|
|
9
|
+
type RepoProvider,
|
|
10
|
+
} from "../../lib/repos/providers/index.js";
|
|
11
|
+
import { repoOf } from "./parts.js";
|
|
12
|
+
import { KIND_LABELS, type RepoRef, type SousRef } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The part of a key that names the repository: its short name, or its
|
|
16
|
+
* identity when it was named by location.
|
|
17
|
+
*/
|
|
18
|
+
function repoPart(repo: RepoRef | undefined): string | undefined {
|
|
19
|
+
if (repo === undefined) return undefined;
|
|
20
|
+
return repo.name ?? repo.location?.identity;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The path part of a key, without any repository.
|
|
25
|
+
*
|
|
26
|
+
* @param ref - Any ref.
|
|
27
|
+
*/
|
|
28
|
+
function pathPart(ref: SousRef): string {
|
|
29
|
+
switch (ref.kind) {
|
|
30
|
+
case "repo":
|
|
31
|
+
return ref.browsed === undefined ? "" : `tree/${ref.browsed}`;
|
|
32
|
+
case "namespace":
|
|
33
|
+
return ref.name;
|
|
34
|
+
case "recipe":
|
|
35
|
+
return ref.namespace === undefined ? ref.name : `${ref.namespace.name}/${ref.name}`;
|
|
36
|
+
case "recipeFile":
|
|
37
|
+
return `${pathPart(ref.recipe)}/${ref.path}`;
|
|
38
|
+
case "variable": {
|
|
39
|
+
const recipe = ref.recipe === undefined ? "" : `${pathPart(ref.recipe)}.`;
|
|
40
|
+
return `${recipe}${ref.name}`;
|
|
41
|
+
}
|
|
42
|
+
case "envVar":
|
|
43
|
+
return ref.name;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The canonical key of a ref, with its version range and query values left
|
|
49
|
+
* off, so the same thing is never keyed twice under two spellings. A
|
|
50
|
+
* repository qualifier is part of the key, because the same recipe name in two
|
|
51
|
+
* repositories is two things.
|
|
52
|
+
*
|
|
53
|
+
* refKey(namespace("workflow")); // -> "workflow"
|
|
54
|
+
* refKey(recipe("task-files", "workflow", "sous-recipes")); // -> "sous-recipes:workflow/task-files"
|
|
55
|
+
* refKey(variable("apiUrl", recipe)); // -> "workflow/task-files.apiUrl"
|
|
56
|
+
*
|
|
57
|
+
* @param ref - Any ref.
|
|
58
|
+
*/
|
|
59
|
+
export function refKey(ref: SousRef): string {
|
|
60
|
+
const repo = ref.kind === "envVar" ? undefined : repoPart(repoOf(ref));
|
|
61
|
+
const rest = pathPart(ref);
|
|
62
|
+
if (ref.kind === "repo") return repo === undefined ? rest : rest === "" ? repo : `${repo}:${rest}`;
|
|
63
|
+
return repo === undefined ? rest : `${repo}:${rest}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The key of a ref together with its kind, which is what tells a namespace
|
|
68
|
+
* `workflow` from a repository `workflow` apart.
|
|
69
|
+
*
|
|
70
|
+
* @param ref - Any ref.
|
|
71
|
+
*/
|
|
72
|
+
export function refIdentity(ref: SousRef): string {
|
|
73
|
+
return `${ref.kind} ${refKey(ref)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes the repository qualifier in front of what is named inside it: `name:`
|
|
78
|
+
* for a repository this project knows by short name (whatever else it knows of
|
|
79
|
+
* it), and the provider's canonical locator for one known only by location.
|
|
80
|
+
*/
|
|
81
|
+
function qualified(
|
|
82
|
+
repo: RepoRef | undefined,
|
|
83
|
+
rest: string,
|
|
84
|
+
providers: RepoProvider[]
|
|
85
|
+
): string {
|
|
86
|
+
if (repo === undefined) return rest;
|
|
87
|
+
if (repo.name !== undefined) return rest === "" ? repo.name : `${repo.name}:${rest}`;
|
|
88
|
+
if (repo.location !== undefined) {
|
|
89
|
+
const { host, repoPath } = repo.location;
|
|
90
|
+
const provider = providerById(repo.location.provider, providers);
|
|
91
|
+
if (provider !== undefined && provider.id !== "local") {
|
|
92
|
+
return provider.formatLocator(host, repoPath, rest);
|
|
93
|
+
}
|
|
94
|
+
return rest === "" ? repo.location.url : `${repo.location.url}/${rest}`;
|
|
95
|
+
}
|
|
96
|
+
return rest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The canonical written form of a ref, which is how sous prints it: the short
|
|
101
|
+
* form for a short ref and each provider's own locator for a located one, then
|
|
102
|
+
* the version range, then the query values.
|
|
103
|
+
*
|
|
104
|
+
* formatRef(parse("https://github.com/o/r/w/a@^1")[0]); // -> "github://o/r/w/a@^1"
|
|
105
|
+
*
|
|
106
|
+
* @param ref - Any ref.
|
|
107
|
+
* @param providers - The providers that format locators. Defaults to the built-ins.
|
|
108
|
+
*/
|
|
109
|
+
export function formatRef(ref: SousRef, providers: RepoProvider[] = builtInProviders()): string {
|
|
110
|
+
const body = qualified(ref.kind === "envVar" ? undefined : repoOf(ref), pathPart(ref), providers);
|
|
111
|
+
const written =
|
|
112
|
+
ref.kind === "recipe" || ref.kind === "recipeFile" || ref.kind === "repo"
|
|
113
|
+
? ref.range
|
|
114
|
+
: undefined;
|
|
115
|
+
const range = written === undefined ? "" : `@${written}`;
|
|
116
|
+
const pairs = Object.entries(ref.vars ?? {}).map(
|
|
117
|
+
([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
|
|
118
|
+
);
|
|
119
|
+
return `${body}${range}${pairs.length === 0 ? "" : `?${pairs.join("&")}`}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What kind of thing a ref names, in a sentence.
|
|
124
|
+
*
|
|
125
|
+
* @param ref - Any ref.
|
|
126
|
+
*/
|
|
127
|
+
export function refKindLabel(ref: SousRef): string {
|
|
128
|
+
return KIND_LABELS[ref.kind];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How a ref is written to a person, so they can type it back: its key when a
|
|
133
|
+
* repository's short name qualifies it, and its canonical locator when only a
|
|
134
|
+
* location does (a ref that has not been matched to a repository this project
|
|
135
|
+
* knows by name).
|
|
136
|
+
*
|
|
137
|
+
* refSpelling(recipe "task-files" in "sous-recipes") // -> "sous-recipes:workflow/task-files"
|
|
138
|
+
* refSpelling(a recipe at a located repository) // -> its canonical locator
|
|
139
|
+
*
|
|
140
|
+
* @param ref - Any ref.
|
|
141
|
+
*/
|
|
142
|
+
export function refSpelling(ref: SousRef): string {
|
|
143
|
+
const repo = ref.kind === "envVar" ? undefined : repoOf(ref);
|
|
144
|
+
if (repo !== undefined && repo.name === undefined && repo.location !== undefined) {
|
|
145
|
+
return formatRef(ref);
|
|
146
|
+
}
|
|
147
|
+
return refKey(ref);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The repository a person is told a ref lives in: its short name, or where it lives. */
|
|
151
|
+
function repoLabel(repo: RepoRef | undefined): string {
|
|
152
|
+
return repo?.name ?? repo?.location?.url ?? "unknown";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Describes one ref in the words a person choosing between several needs: the
|
|
157
|
+
* fully qualified name, and what it actually means.
|
|
158
|
+
*
|
|
159
|
+
* @param ref - A known ref, as a lookup returns it.
|
|
160
|
+
*/
|
|
161
|
+
export function describeRef(ref: SousRef): string {
|
|
162
|
+
const summary = ref.description === undefined ? "" : `: ${ref.description}`;
|
|
163
|
+
const key = refSpelling(ref);
|
|
164
|
+
|
|
165
|
+
switch (ref.kind) {
|
|
166
|
+
case "repo": {
|
|
167
|
+
const where = ref.location?.url;
|
|
168
|
+
return `${key} (the repository '${ref.name ?? key}'${where === undefined ? "" : `, at ${where}`})`;
|
|
169
|
+
}
|
|
170
|
+
case "namespace":
|
|
171
|
+
return (
|
|
172
|
+
`${key} (the whole namespace '${ref.name}' in the ` +
|
|
173
|
+
`repository '${repoLabel(ref.repo)}')`
|
|
174
|
+
);
|
|
175
|
+
case "recipe":
|
|
176
|
+
return (
|
|
177
|
+
`${key} (the recipe '${ref.name}' in the namespace ` +
|
|
178
|
+
`'${ref.namespace?.name ?? "unknown"}' of the repository ` +
|
|
179
|
+
`'${repoLabel(repoOf(ref))}'${summary})`
|
|
180
|
+
);
|
|
181
|
+
case "recipeFile":
|
|
182
|
+
return `${key} (the file '${ref.path}' of the recipe '${pathPart(ref.recipe)}'${summary})`;
|
|
183
|
+
case "variable":
|
|
184
|
+
return (
|
|
185
|
+
`${key} (the variable '${ref.name}' of the recipe ` +
|
|
186
|
+
`'${ref.recipe === undefined ? "unknown" : pathPart(ref.recipe)}'${summary})`
|
|
187
|
+
);
|
|
188
|
+
case "envVar": {
|
|
189
|
+
const first = ref.variables?.[0];
|
|
190
|
+
const answers =
|
|
191
|
+
first === undefined
|
|
192
|
+
? ""
|
|
193
|
+
: `answering '${first.name}' of the recipe '${first.recipe === undefined ? "unknown" : pathPart(first.recipe)}'`;
|
|
194
|
+
return `${ref.name} (the environment variable${answers === "" ? "" : ` ${answers}`}${summary})`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|