@sous-io/sous 0.2.32 → 0.2.34
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 +1 -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-variables.md +16 -0
- package/docs/markdown/repositories.md +8 -4
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
- 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 +21 -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 +93 -15
- package/src/lib/config-schema.ts +59 -19
- package/src/lib/include-resolver.ts +307 -69
- package/src/lib/markdown-compiler.ts +289 -82
- 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 +32 -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/answers.ts +9 -1
- package/src/lib/vars/ask.ts +3 -3
- package/src/lib/vars/display.ts +1 -3
- package/src/lib/vars/index.ts +1 -0
- package/src/lib/vars/mappings.ts +17 -9
- package/src/lib/vars/secrets.ts +131 -0
- 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 +33 -18
- package/src/templating/lib/secret-scope.ts +41 -0
- package/src/templating/tags/exportScalarVarsJs.ts +5 -2
- package/src/templating/tags/showVars.ts +8 -2
- 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/refs/pick.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settling on one of the things a reference could have meant.
|
|
3
|
-
*
|
|
4
|
-
* Finding is separate from choosing on purpose: `findReference` is pure and
|
|
5
|
-
* tells the caller everything a word could have meant, and this is the one
|
|
6
|
-
* place that decides which of them the run proceeds with. Every command that
|
|
7
|
-
* takes a reference goes through it, so a word that means two things behaves
|
|
8
|
-
* the same everywhere:
|
|
9
|
-
*
|
|
10
|
-
* - one match proceeds, and what it resolved to is written as the facts about
|
|
11
|
-
* it followed by one sentence (`formatResolvedReference`), so the reader
|
|
12
|
-
* sees what the word they typed actually meant;
|
|
13
|
-
* - several matches are offered as a list to choose from;
|
|
14
|
-
* - `--accept-first` takes the first one in the documented listing order;
|
|
15
|
-
* - a run with no terminal fails, naming the question it could not ask and
|
|
16
|
-
* the flag that would have answered it (`src/lib/interactive.ts` holds that
|
|
17
|
-
* rule, and every prompt in sous is gated by it).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { ConfigError } from "../errors.js";
|
|
21
|
-
import { nonInteractiveError } from "../interactive.js";
|
|
22
|
-
import { formatParagraph, log } from "../../utils/formatting.js";
|
|
23
|
-
import { askChoice } from "../../utils/prompts.js";
|
|
24
|
-
import {
|
|
25
|
-
formatResolvedReference,
|
|
26
|
-
resolvedReferenceFacts,
|
|
27
|
-
} from "../repos/reference-report.js";
|
|
28
|
-
import { describeReference, type ReferenceMatch } from "./find.js";
|
|
29
|
-
|
|
30
|
-
/** The flag that answers "which one did you mean?" ahead of time. */
|
|
31
|
-
export const ACCEPT_FIRST_FLAG = "--accept-first";
|
|
32
|
-
|
|
33
|
-
/** How `pickReference` should behave. */
|
|
34
|
-
export type PickReferenceOptions = {
|
|
35
|
-
/** The reference exactly as it was written, for every message. */
|
|
36
|
-
search: string;
|
|
37
|
-
/** Whether a question may be asked. A run that may not fails instead. */
|
|
38
|
-
interactive: boolean;
|
|
39
|
-
/** Take the first match rather than asking, because `--accept-first` was passed. */
|
|
40
|
-
acceptFirst?: boolean;
|
|
41
|
-
/** The question to ask when there is more than one match. */
|
|
42
|
-
prompt?: string;
|
|
43
|
-
/**
|
|
44
|
-
* Write the facts about what the reference resolved to. On by default; a
|
|
45
|
-
* caller that reports the resolution itself turns it off, and is still told
|
|
46
|
-
* when `--accept-first` settled an ambiguous word.
|
|
47
|
-
*/
|
|
48
|
-
announce?: boolean;
|
|
49
|
-
/** Extra lines for the error a reference that matched nothing raises. */
|
|
50
|
-
details?: string[];
|
|
51
|
-
/** Where a resolution is reported. Defaults to the console. */
|
|
52
|
-
write?: (message: string) => void;
|
|
53
|
-
/** How the choice is asked. Defaults to the shared choice prompt. */
|
|
54
|
-
choose?: (message: string, matches: ReferenceMatch[]) => Promise<ReferenceMatch>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The one match a reference proceeds with.
|
|
59
|
-
*
|
|
60
|
-
* @param matches - What the reference could have meant, in listing order.
|
|
61
|
-
* @param options - What was searched for, and whether a question may be asked.
|
|
62
|
-
* @returns The match the run proceeds with.
|
|
63
|
-
*/
|
|
64
|
-
export async function pickReference(
|
|
65
|
-
matches: ReferenceMatch[],
|
|
66
|
-
options: PickReferenceOptions
|
|
67
|
-
): Promise<ReferenceMatch> {
|
|
68
|
-
const write = options.write ?? ((message: string) => log(message));
|
|
69
|
-
|
|
70
|
-
if (matches.length === 0) {
|
|
71
|
-
throw new ConfigError(
|
|
72
|
-
[`Nothing called '${options.search}' was found.`, ...(options.details ?? [])].join("\n")
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const first = matches[0]!;
|
|
77
|
-
|
|
78
|
-
if (matches.length === 1) {
|
|
79
|
-
if (options.announce !== false) announce(write, first, options.search);
|
|
80
|
-
return first;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (options.acceptFirst === true) {
|
|
84
|
-
const reason =
|
|
85
|
-
`'${options.search}' named ${matches.length} things, and ` +
|
|
86
|
-
`'${ACCEPT_FIRST_FLAG}' was passed, so the first one listed is being used.`;
|
|
87
|
-
|
|
88
|
-
// A caller that reports the resolution itself still has to be told that the
|
|
89
|
-
// word was ambiguous, so the sentence is written even when the facts are not.
|
|
90
|
-
if (options.announce === false) {
|
|
91
|
-
for (const line of formatParagraph(reason)) write(line);
|
|
92
|
-
} else {
|
|
93
|
-
announce(write, first, options.search, reason);
|
|
94
|
-
}
|
|
95
|
-
return first;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!options.interactive) {
|
|
99
|
-
// The example is a spelling that says more than what was typed, so a
|
|
100
|
-
// reference that is already its own fully qualified name is not offered
|
|
101
|
-
// back unchanged.
|
|
102
|
-
const example =
|
|
103
|
-
matches.find((match) => match.key !== options.search)?.key ?? first.key;
|
|
104
|
-
|
|
105
|
-
throw nonInteractiveError({
|
|
106
|
-
prompt: `which '${options.search}' you meant`,
|
|
107
|
-
remedy:
|
|
108
|
-
`write the full reference (for example '${example}'), or pass ` +
|
|
109
|
-
`'${ACCEPT_FIRST_FLAG}' to take the first candidate listed above.`,
|
|
110
|
-
details: [
|
|
111
|
-
`'${options.search}' matched ${matches.length} things:`,
|
|
112
|
-
...matches.map((match) => ` ${describeReference(match)}`),
|
|
113
|
-
],
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const choose =
|
|
118
|
-
options.choose ??
|
|
119
|
-
((message: string, offered: ReferenceMatch[]) =>
|
|
120
|
-
askChoice(
|
|
121
|
-
message,
|
|
122
|
-
offered.map((match) => ({ name: describeReference(match), value: match }))
|
|
123
|
-
));
|
|
124
|
-
|
|
125
|
-
return choose(options.prompt ?? `Which '${options.search}' did you mean?`, matches);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Writes what a reference resolved to, as the key and value list every other
|
|
130
|
-
* set of facts in the CLI is written as, followed by one sentence saying why
|
|
131
|
-
* that candidate won.
|
|
132
|
-
*
|
|
133
|
-
* @param write - Where the report goes.
|
|
134
|
-
* @param match - The match the run proceeds with.
|
|
135
|
-
* @param search - The reference exactly as it was written.
|
|
136
|
-
* @param reason - The closing sentence, when it was not simply the only match.
|
|
137
|
-
*/
|
|
138
|
-
function announce(
|
|
139
|
-
write: (message: string) => void,
|
|
140
|
-
match: ReferenceMatch,
|
|
141
|
-
search: string,
|
|
142
|
-
reason?: string
|
|
143
|
-
): void {
|
|
144
|
-
for (const line of formatResolvedReference(resolvedReferenceFacts(match, search, reason))) {
|
|
145
|
-
write(line);
|
|
146
|
-
}
|
|
147
|
-
}
|
package/src/lib/refs/scopes.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The kinds of thing a reference on the command line can name.
|
|
3
|
-
*
|
|
4
|
-
* Every sous command that takes a `<ref>` or a `[NAME]` argument is naming one
|
|
5
|
-
* of these five things, and each command accepts only the kinds that make sense
|
|
6
|
-
* for it: `sous subscribe` accepts a namespace or a recipe, `sous vars ask`
|
|
7
|
-
* accepts all five. The scope list a command passes to `findReference` is
|
|
8
|
-
* therefore part of that command's contract, and it is the only thing that
|
|
9
|
-
* differs between one command's resolution and another's.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** One kind of thing a reference can name. */
|
|
13
|
-
export enum SousScope {
|
|
14
|
-
/** A repository this project trusts, named by its short name. */
|
|
15
|
-
Repository = "repository",
|
|
16
|
-
/** A namespace published by a repository. */
|
|
17
|
-
Namespace = "namespace",
|
|
18
|
-
/** A recipe published in a namespace. */
|
|
19
|
-
Recipe = "recipe",
|
|
20
|
-
/** A variable declared by a recipe, named as the recipe's author named it. */
|
|
21
|
-
VariableName = "variableName",
|
|
22
|
-
/** An environment variable name that answers a variable. */
|
|
23
|
-
EnvVarName = "envVarName",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Where a ref was written. `parseRef` reads every form wherever a ref comes
|
|
28
|
-
* from, and the place decides which of those forms it allows: a refused form
|
|
29
|
-
* is an error saying what to write there instead.
|
|
30
|
-
*/
|
|
31
|
-
export enum RefSource {
|
|
32
|
-
/**
|
|
33
|
-
* A ref typed on the command line. Every form is allowed: a bare name, a
|
|
34
|
-
* namespace and recipe, a `repo:` qualifier, a version range, a
|
|
35
|
-
* provider-scheme locator, an HTTPS, SSH or scheme-less URL, and a browser
|
|
36
|
-
* URL copied from a host's file view. Names may be written in any case.
|
|
37
|
-
*/
|
|
38
|
-
CommandLine = "commandLine",
|
|
39
|
-
/**
|
|
40
|
-
* A subscription key in a config layer. Only the stored form is allowed:
|
|
41
|
-
* `namespace` or `namespace/recipe`, lowercase. The repository a
|
|
42
|
-
* subscription resolves into is recorded in the lockfile, and its range in
|
|
43
|
-
* the entry's own `range` field.
|
|
44
|
-
*/
|
|
45
|
-
Config = "config",
|
|
46
|
-
/**
|
|
47
|
-
* An entry of a recipe manifest's `depends` or `subscribes` list. A bare ref
|
|
48
|
-
* names a recipe in the same repository; every locator and URL form names
|
|
49
|
-
* one in another repository. A `repo:` qualifier is refused, because it is
|
|
50
|
-
* one project's private name for a repository, and so is a local path.
|
|
51
|
-
*/
|
|
52
|
-
Manifest = "manifest",
|
|
53
|
-
/**
|
|
54
|
-
* A key sous wrote itself: the lockfile, the index and the store. Only the
|
|
55
|
-
* canonical `namespace` or `namespace/recipe` is allowed.
|
|
56
|
-
*/
|
|
57
|
-
Lockfile = "lockfile",
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Every scope, in the order matches of equal specificity are listed in. It is
|
|
62
|
-
* the order the one-word ref search has always used (a whole namespace before
|
|
63
|
-
* the recipes inside it), widened to the other three kinds: the broadest thing
|
|
64
|
-
* a word could have meant is offered first, and the environment variable names
|
|
65
|
-
* (the least likely reading of a plain word) come last.
|
|
66
|
-
*/
|
|
67
|
-
export const SCOPE_ORDER: readonly SousScope[] = [
|
|
68
|
-
SousScope.Repository,
|
|
69
|
-
SousScope.Namespace,
|
|
70
|
-
SousScope.Recipe,
|
|
71
|
-
SousScope.VariableName,
|
|
72
|
-
SousScope.EnvVarName,
|
|
73
|
-
] as const;
|
|
74
|
-
|
|
75
|
-
/** Every scope, for a command that accepts anything a reference can name. */
|
|
76
|
-
export const ALL_SCOPES: readonly SousScope[] = SCOPE_ORDER;
|
|
77
|
-
|
|
78
|
-
/** What each scope is called in a sentence. */
|
|
79
|
-
export const SCOPE_LABELS: Record<SousScope, string> = {
|
|
80
|
-
[SousScope.Repository]: "repository",
|
|
81
|
-
[SousScope.Namespace]: "namespace",
|
|
82
|
-
[SousScope.Recipe]: "recipe",
|
|
83
|
-
[SousScope.VariableName]: "variable",
|
|
84
|
-
[SousScope.EnvVarName]: "environment variable",
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Where a scope sits in the listing order.
|
|
89
|
-
*
|
|
90
|
-
* @param scope - The scope to rank.
|
|
91
|
-
*/
|
|
92
|
-
export function scopeRank(scope: SousScope): number {
|
|
93
|
-
const rank = SCOPE_ORDER.indexOf(scope);
|
|
94
|
-
return rank === -1 ? SCOPE_ORDER.length : rank;
|
|
95
|
-
}
|
package/src/lib/refs/settle.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settling what a ref means, once the index that can say is at hand.
|
|
3
|
-
*
|
|
4
|
-
* `parseRef` returns every reading of a written ref. Some readings are settled
|
|
5
|
-
* the moment they are read (`workflow/alpha`), and some need the index of the
|
|
6
|
-
* repository they point at: a browser URL names a folder, and only the `path`
|
|
7
|
-
* each recipe's index entry records says which recipe that folder is; a GitLab
|
|
8
|
-
* URL with nested groups can be read as several repositories, and only their
|
|
9
|
-
* indexes say which one publishes what was named.
|
|
10
|
-
*
|
|
11
|
-
* Names are matched the same way everywhere: the exact spelling first, then
|
|
12
|
-
* ignoring case. A name that still matches several things is a question for
|
|
13
|
-
* `pickReference`.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
isBrowsedReading,
|
|
18
|
-
isNamedReading,
|
|
19
|
-
namespaceOfKey,
|
|
20
|
-
parseRef,
|
|
21
|
-
refKey,
|
|
22
|
-
splitRecipeKey,
|
|
23
|
-
type ParsedRef,
|
|
24
|
-
type RefReading,
|
|
25
|
-
} from "./parse.js";
|
|
26
|
-
import { RefSource } from "./scopes.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The names that match a written one: those spelled exactly as written, or,
|
|
30
|
-
* when there is none, those that differ only in case.
|
|
31
|
-
*
|
|
32
|
-
* matchNames("Workflow", ["workflow", "tooling"]); // -> ["workflow"]
|
|
33
|
-
* matchNames("workflow", ["workflow", "Workflow"]); // -> ["workflow"]
|
|
34
|
-
*
|
|
35
|
-
* @param written - The name as written.
|
|
36
|
-
* @param names - The names it could mean.
|
|
37
|
-
*/
|
|
38
|
-
export function matchNames(written: string, names: Iterable<string>): string[] {
|
|
39
|
-
const all = [...names];
|
|
40
|
-
const exact = all.filter((name) => name === written);
|
|
41
|
-
if (exact.length > 0) return exact;
|
|
42
|
-
const folded = written.toLowerCase();
|
|
43
|
-
return all.filter((name) => name.toLowerCase() === folded);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** What settling needs to know about one repository's index. */
|
|
47
|
-
export type SettleIndex = {
|
|
48
|
-
/** Every namespace the index publishes. */
|
|
49
|
-
namespaces: Iterable<string>;
|
|
50
|
-
/** Every recipe it publishes, keyed `namespace/recipe`, with the folder it lives in. */
|
|
51
|
-
recipes: Record<string, { path: string }>;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* What a reading names in one repository's index, spelled the way the index
|
|
56
|
-
* spells it. A named reading that the index does not publish settles to
|
|
57
|
-
* nothing; a browsed path settles through the recipe paths.
|
|
58
|
-
*
|
|
59
|
-
* settleInIndex({ namespace: "Workflow", recipe: "alpha" }, index);
|
|
60
|
-
* // -> [{ namespace: "workflow", recipe: "alpha" }]
|
|
61
|
-
*
|
|
62
|
-
* @param reading - One reading of a written ref.
|
|
63
|
-
* @param index - The index of the repository the reading points at.
|
|
64
|
-
*/
|
|
65
|
-
export function settleInIndex(reading: RefReading, index: SettleIndex): ParsedRef[] {
|
|
66
|
-
const carried = {
|
|
67
|
-
...(reading.location === undefined ? {} : { location: reading.location }),
|
|
68
|
-
...("range" in reading && reading.range !== undefined ? { range: reading.range } : {}),
|
|
69
|
-
...("repo" in reading && reading.repo !== undefined ? { repo: reading.repo } : {}),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
if (isBrowsedReading(reading)) {
|
|
73
|
-
return settleBrowsed(reading.browsed, index).map((settled) => ({ ...settled, ...carried }));
|
|
74
|
-
}
|
|
75
|
-
if (!isNamedReading(reading)) return [];
|
|
76
|
-
|
|
77
|
-
if (reading.recipe === undefined) {
|
|
78
|
-
return matchNames(reading.namespace, index.namespaces).map((namespace) => ({
|
|
79
|
-
namespace,
|
|
80
|
-
...carried,
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return matchNames(refKey(reading), Object.keys(index.recipes)).map((key) => {
|
|
85
|
-
const { namespace, name } = splitRecipeKey(key);
|
|
86
|
-
return { namespace, recipe: name, ...carried };
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* What a browser path names, through the folder each recipe's index entry
|
|
92
|
-
* records. The path still carries the branch in front (which may itself hold
|
|
93
|
-
* slashes), so every split is tried: the folder of a recipe, or anything inside
|
|
94
|
-
* it, names that recipe; a folder holding recipes of exactly one namespace
|
|
95
|
-
* names that namespace.
|
|
96
|
-
*
|
|
97
|
-
* settleBrowsed("main/recipes/workflow/alpha/SKILL.md", index);
|
|
98
|
-
* // -> [{ namespace: "workflow", recipe: "alpha" }]
|
|
99
|
-
*
|
|
100
|
-
* @param browsed - The path after `tree/` or `blob/`, branch included.
|
|
101
|
-
* @param index - The repository's index.
|
|
102
|
-
*/
|
|
103
|
-
export function settleBrowsed(browsed: string, index: SettleIndex): ParsedRef[] {
|
|
104
|
-
const segments = browsed.split("/").filter((segment) => segment.length > 0);
|
|
105
|
-
const recipes = Object.entries(index.recipes).map(([key, entry]) => ({
|
|
106
|
-
key,
|
|
107
|
-
path: entry.path.replace(/^\.?\/+|\/+$/g, ""),
|
|
108
|
-
}));
|
|
109
|
-
|
|
110
|
-
const found = new Map<string, ParsedRef>();
|
|
111
|
-
|
|
112
|
-
// A recipe's folder, or a file inside it. The deepest recipe wins, so a
|
|
113
|
-
// recipe nested inside another's folder is named by its own path.
|
|
114
|
-
for (let start = 1; start < segments.length; start++) {
|
|
115
|
-
const folder = segments.slice(start).join("/");
|
|
116
|
-
const holding = recipes
|
|
117
|
-
.filter((recipe) => folder === recipe.path || folder.startsWith(`${recipe.path}/`))
|
|
118
|
-
.sort((left, right) => right.path.length - left.path.length);
|
|
119
|
-
const deepest = holding[0];
|
|
120
|
-
if (deepest !== undefined) {
|
|
121
|
-
const { namespace, name } = splitRecipeKey(deepest.key);
|
|
122
|
-
found.set(deepest.key, { namespace, recipe: name });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (found.size > 0) return [...found.values()];
|
|
126
|
-
|
|
127
|
-
// A folder above recipes: a namespace, when everything under it is one.
|
|
128
|
-
for (let start = 1; start < segments.length; start++) {
|
|
129
|
-
const folder = segments.slice(start).join("/");
|
|
130
|
-
const under = recipes.filter((recipe) => recipe.path.startsWith(`${folder}/`));
|
|
131
|
-
const namespaces = new Set(under.map((recipe) => namespaceOfKey(recipe.key)));
|
|
132
|
-
if (namespaces.size === 1) {
|
|
133
|
-
const namespace = [...namespaces][0]!;
|
|
134
|
-
found.set(namespace, { namespace });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return [...found.values()];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** What a repository's index recorded about one dependency, as far as settling needs. */
|
|
141
|
-
export type RecordedDependencies = Record<string, { repo?: string } | undefined>;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* The readings of a manifest dependency that the declaring recipe's index
|
|
145
|
-
* confirms. A release settles a dependency that reads more than one way (a
|
|
146
|
-
* GitLab URL with nested groups, a browser URL) and records the repository it
|
|
147
|
-
* settled on beside each key it resolved; a consumer reads that record and never
|
|
148
|
-
* probes. A dependency that reads one way needs no record and comes back as it
|
|
149
|
-
* is.
|
|
150
|
-
*
|
|
151
|
-
* A named reading is confirmed by a record under its own key (or, for a whole
|
|
152
|
-
* namespace, under a key inside it) carrying its repository; a browsed reading
|
|
153
|
-
* by any record carrying its repository.
|
|
154
|
-
*
|
|
155
|
-
* @param readings - Every reading of the dependency, from `parseRef`.
|
|
156
|
-
* @param recorded - What the declaring version's index entry records, keyed `namespace/recipe`.
|
|
157
|
-
* @returns The confirmed readings: one is the answer; none or several leave it unsettled.
|
|
158
|
-
*/
|
|
159
|
-
export function confirmedReadings(
|
|
160
|
-
readings: RefReading[],
|
|
161
|
-
recorded: RecordedDependencies | undefined
|
|
162
|
-
): RefReading[] {
|
|
163
|
-
if (readings.length === 1) return readings;
|
|
164
|
-
const records = Object.entries(recorded ?? {});
|
|
165
|
-
|
|
166
|
-
return readings.filter((reading) => {
|
|
167
|
-
const identity = reading.location?.identity;
|
|
168
|
-
if (identity === undefined) return false;
|
|
169
|
-
return records.some(([key, record]) => {
|
|
170
|
-
if (record?.repo !== identity) return false;
|
|
171
|
-
if (isBrowsedReading(reading)) return true;
|
|
172
|
-
if (!isNamedReading(reading)) return false;
|
|
173
|
-
return reading.recipe === undefined
|
|
174
|
-
? key.startsWith(`${reading.namespace}/`)
|
|
175
|
-
: key === refKey(reading);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** What `settleDependency` may consult, beyond the dependency itself. */
|
|
181
|
-
export type SettleDependencyOptions = {
|
|
182
|
-
/** What the declaring version's index entry records about its dependencies. */
|
|
183
|
-
recorded?: RecordedDependencies;
|
|
184
|
-
/**
|
|
185
|
-
* Whether a reading is already known to be right from what is on this
|
|
186
|
-
* machine: its repository is trusted and its cached index publishes what it
|
|
187
|
-
* names. Used only when the index recorded nothing, and it never fetches.
|
|
188
|
-
*/
|
|
189
|
-
known?: (reading: RefReading) => boolean;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The one reading a manifest dependency settles on, or undefined when it reads
|
|
194
|
-
* more than one way and nothing at hand says which: the declaring recipe's
|
|
195
|
-
* index recorded no answer, and what this machine already knows does not pick
|
|
196
|
-
* exactly one.
|
|
197
|
-
*
|
|
198
|
-
* settleDependency("workflow/alpha");
|
|
199
|
-
* // -> { namespace: "workflow", recipe: "alpha" }
|
|
200
|
-
*
|
|
201
|
-
* @param written - The dependency as the manifest wrote it.
|
|
202
|
-
* @param options - The index's record, and what is known locally.
|
|
203
|
-
*/
|
|
204
|
-
export function settleDependency(
|
|
205
|
-
written: string,
|
|
206
|
-
options: SettleDependencyOptions = {}
|
|
207
|
-
): RefReading | undefined {
|
|
208
|
-
const readings = parseRef(written, RefSource.Manifest);
|
|
209
|
-
if (readings.length === 1) return readings[0];
|
|
210
|
-
|
|
211
|
-
const confirmed = confirmedReadings(readings, options.recorded);
|
|
212
|
-
if (confirmed.length === 1) return confirmed[0];
|
|
213
|
-
|
|
214
|
-
if (options.known !== undefined) {
|
|
215
|
-
const known = readings.filter(options.known);
|
|
216
|
-
if (known.length === 1) return known[0];
|
|
217
|
-
}
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|