@sous-io/sous 0.2.32 → 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 +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.md +8 -4
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +25 -17
- 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 +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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memories the project's active recipes publish, listed for the `#memories`
|
|
3
|
+
* view and for the build's check that every one of them reached an output.
|
|
4
|
+
*
|
|
5
|
+
* A recipe is ACTIVE when the project holds it directly or a co-subscription
|
|
6
|
+
* does (its lockfile kind is `subscribes`); a recipe held only through
|
|
7
|
+
* `depends` is a library and contributes nothing. A memory is a file matched by
|
|
8
|
+
* an include pattern of a manifest `contents` entry of `kind: memories`, and its
|
|
9
|
+
* virtual path is `<namespace>/<recipe>/<path>`, the path being relative to the
|
|
10
|
+
* static base of that pattern.
|
|
11
|
+
*
|
|
12
|
+
* Order: a recipe comes after every pinned recipe it depends on or subscribes
|
|
13
|
+
* to, ties go to the recipe key (bytewise), and the recipes matching
|
|
14
|
+
* `recipes.memories.first` move to the front in that same relative order. Within
|
|
15
|
+
* a recipe, files are in bytewise path order. Recipes matching
|
|
16
|
+
* `recipes.memories.exclude` are not listed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { globSync } from "glob";
|
|
22
|
+
import { compareBytewise } from "../../services/ref-resolver/index.js";
|
|
23
|
+
import { realPathInside } from "../include-resolver.js";
|
|
24
|
+
import { inferGlobBase } from "../markdown-compiler.js";
|
|
25
|
+
import { compileRecipeKeyMatcher } from "./recipe-key-matcher.js";
|
|
26
|
+
import {
|
|
27
|
+
listLockedRecipes,
|
|
28
|
+
readRecipeManifestIn,
|
|
29
|
+
type LockedRecipeLocation,
|
|
30
|
+
} from "./locked-recipes.js";
|
|
31
|
+
|
|
32
|
+
/** One memory file an active recipe publishes. */
|
|
33
|
+
export type MemoryFile = {
|
|
34
|
+
/** The recipe key, `namespace/recipe`. */
|
|
35
|
+
recipe: string;
|
|
36
|
+
/** The path inside the `#memories` view, without the `#memories/` prefix. */
|
|
37
|
+
path: string;
|
|
38
|
+
/** The real file's absolute path. */
|
|
39
|
+
file: string;
|
|
40
|
+
/** The file's path relative to its recipe's directory, for messages. */
|
|
41
|
+
relative: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** What listing the memories needs. */
|
|
45
|
+
export type MemoryListingOptions = {
|
|
46
|
+
/** The project's `.sous/` directory. */
|
|
47
|
+
sousDir: string;
|
|
48
|
+
/** The environment to read; decides where the store is. */
|
|
49
|
+
env?: NodeJS.ProcessEnv;
|
|
50
|
+
/** `recipes.memories.first`: recipes whose memories lead. */
|
|
51
|
+
first?: readonly string[] | undefined;
|
|
52
|
+
/** `recipes.memories.exclude`: recipes whose memories are left out. */
|
|
53
|
+
exclude?: readonly string[] | undefined;
|
|
54
|
+
/** The locked recipes, when the caller has already located them. */
|
|
55
|
+
locked?: LockedRecipeLocation[];
|
|
56
|
+
/**
|
|
57
|
+
* Receives a sentence for each memory skipped because its real path is
|
|
58
|
+
* outside its recipe's directory. Defaults to one line on stderr per file.
|
|
59
|
+
*/
|
|
60
|
+
onWarning?: (message: string) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Files already warned about by the default sink, so a repeated listing warns once. */
|
|
64
|
+
const warnedFiles = new Set<string>();
|
|
65
|
+
|
|
66
|
+
/** The default warning sink: one line on stderr, once per file. */
|
|
67
|
+
function warnOnce(message: string): void {
|
|
68
|
+
if (warnedFiles.has(message)) return;
|
|
69
|
+
warnedFiles.add(message);
|
|
70
|
+
process.stderr.write(`Warning: ${message}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Orders recipe keys so each comes after every pinned recipe it depends on or
|
|
75
|
+
* subscribes to, ties by key. A lockfile entry's `requestedBy` names the recipes
|
|
76
|
+
* that asked for it, which is the dependency edge read backwards. A cycle (which
|
|
77
|
+
* a resolved lockfile should not hold) leaves its members in key order at the end.
|
|
78
|
+
*
|
|
79
|
+
* @param locked - Every pinned recipe.
|
|
80
|
+
*/
|
|
81
|
+
export function dependencyOrder(locked: readonly LockedRecipeLocation[]): string[] {
|
|
82
|
+
// A recipe waits for every pinned recipe that lists it among its holders.
|
|
83
|
+
const waitingOn = new Map<string, Set<string>>();
|
|
84
|
+
for (const recipe of locked) waitingOn.set(recipe.key, new Set());
|
|
85
|
+
for (const dependency of locked) {
|
|
86
|
+
for (const holder of dependency.requestedBy) {
|
|
87
|
+
if (holder !== dependency.key) waitingOn.get(holder)?.add(dependency.key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ordered: string[] = [];
|
|
92
|
+
const done = new Set<string>();
|
|
93
|
+
const remaining = [...waitingOn.keys()].sort(compareBytewise);
|
|
94
|
+
while (remaining.length > 0) {
|
|
95
|
+
const index = remaining.findIndex((key) => [...waitingOn.get(key)!].every((dep) => done.has(dep)));
|
|
96
|
+
if (index === -1) {
|
|
97
|
+
ordered.push(...remaining);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const [key] = remaining.splice(index, 1);
|
|
101
|
+
ordered.push(key!);
|
|
102
|
+
done.add(key!);
|
|
103
|
+
}
|
|
104
|
+
return ordered;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lists the memories of every active recipe, in view order, with the recipes
|
|
109
|
+
* that `exclude` names left out.
|
|
110
|
+
*
|
|
111
|
+
* @param options - The project, and the `first` and `exclude` lists.
|
|
112
|
+
*/
|
|
113
|
+
export function listMemories(options: MemoryListingOptions): MemoryFile[] {
|
|
114
|
+
const locked =
|
|
115
|
+
options.locked ??
|
|
116
|
+
listLockedRecipes({
|
|
117
|
+
sousDir: options.sousDir,
|
|
118
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
119
|
+
});
|
|
120
|
+
if (locked.length === 0) return [];
|
|
121
|
+
|
|
122
|
+
const isFirst = compileRecipeKeyMatcher(options.first, "recipes.memories.first");
|
|
123
|
+
const isExcluded = compileRecipeKeyMatcher(options.exclude, "recipes.memories.exclude");
|
|
124
|
+
const byKey = new Map(locked.map((recipe) => [recipe.key, recipe]));
|
|
125
|
+
|
|
126
|
+
const order = dependencyOrder(locked);
|
|
127
|
+
const leading = order.filter((key) => isFirst(key));
|
|
128
|
+
const rest = order.filter((key) => !isFirst(key));
|
|
129
|
+
|
|
130
|
+
const files: MemoryFile[] = [];
|
|
131
|
+
for (const key of [...leading, ...rest]) {
|
|
132
|
+
const recipe = byKey.get(key)!;
|
|
133
|
+
if (recipe.kind !== "subscribes" || !recipe.present || isExcluded(key)) continue;
|
|
134
|
+
const manifest = readRecipeManifestIn(recipe.dir);
|
|
135
|
+
if (manifest === undefined) continue;
|
|
136
|
+
|
|
137
|
+
const mine = new Map<string, MemoryFile>();
|
|
138
|
+
for (const content of manifest.contents) {
|
|
139
|
+
if (content.kind !== "memories") continue;
|
|
140
|
+
const ignore = (content.exclude ?? []).map((pattern) => path.join(recipe.dir, pattern));
|
|
141
|
+
for (const include of content.include) {
|
|
142
|
+
const pattern = path.join(recipe.dir, include);
|
|
143
|
+
const base = inferGlobBase(pattern);
|
|
144
|
+
for (const file of globSync(pattern, { absolute: true, ignore, dot: true })) {
|
|
145
|
+
if (!isFile(file)) continue;
|
|
146
|
+
if (!realPathInside(file, recipe.dir)) {
|
|
147
|
+
(options.onWarning ?? warnOnce)(
|
|
148
|
+
`The memory ${file} of the recipe ${key} was left out, because it is a link ` +
|
|
149
|
+
`that leads outside the recipe's directory. Replace the link with the file itself.`
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const inside = path.relative(base, file).split(path.sep).join("/");
|
|
154
|
+
mine.set(file, {
|
|
155
|
+
recipe: key,
|
|
156
|
+
path: `${key}/${inside}`,
|
|
157
|
+
file,
|
|
158
|
+
relative: path.relative(recipe.dir, file).split(path.sep).join("/"),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
files.push(...[...mine.values()].sort((a, b) => compareBytewise(a.path, b.path)));
|
|
164
|
+
}
|
|
165
|
+
return files;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True when the path is a regular file. */
|
|
169
|
+
function isFile(candidate: string): boolean {
|
|
170
|
+
try {
|
|
171
|
+
return fs.statSync(candidate).isFile();
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Turning subscribed recipes into compile targets.
|
|
3
3
|
*
|
|
4
4
|
* A recipe's manifest lists what it contributes: skills, memories, prompts and
|
|
5
|
-
* config layers.
|
|
6
|
-
* project's own `entryGlob` target is: the recipe's directory is the glob
|
|
7
|
-
* the static part of each include pattern is the base the output tree
|
|
8
|
-
* and the `.tpl.` convention applies unchanged.
|
|
9
|
-
*
|
|
5
|
+
* config layers. Only skills are copied into the project, compiled exactly the
|
|
6
|
+
* way a project's own `entryGlob` target is: the recipe's directory is the glob
|
|
7
|
+
* root, the static part of each include pattern is the base the output tree
|
|
8
|
+
* mirrors, and the `.tpl.` convention applies unchanged. Memories reach an agent
|
|
9
|
+
* through include lines, never as copied files, and config layers are loaded
|
|
10
|
+
* rather than written.
|
|
10
11
|
*
|
|
11
12
|
* Two rules decide what is compiled at all:
|
|
12
13
|
*
|
|
@@ -14,10 +15,10 @@
|
|
|
14
15
|
* only through `depends` is fetched, pinned and addressable from the recipe
|
|
15
16
|
* that declared it, but its files never enter the project's output; that is
|
|
16
17
|
* the whole difference between the two dependency kinds.
|
|
17
|
-
* -
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* - Skills go to `recipes.skills.outputs`, which defaults to
|
|
19
|
+
* `.claude/skills` because that is where every agent looks. Prompts have no
|
|
20
|
+
* destination yet, so a recipe that publishes them is skipped with ONE
|
|
21
|
+
* warning naming the `recipes` config key.
|
|
21
22
|
*
|
|
22
23
|
* A recipe's `config` contents are not handled here. They are config layers, and
|
|
23
24
|
* config layers are loaded before any of this runs; see
|
|
@@ -38,8 +39,11 @@ import {
|
|
|
38
39
|
type LockedRecipeLocation,
|
|
39
40
|
} from "./locked-recipes.js";
|
|
40
41
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The content kinds whose files are copied into a project. `config` is a layer,
|
|
44
|
+
* and `memories` are reached by include lines rather than copied.
|
|
45
|
+
*/
|
|
46
|
+
export const WRITABLE_CONTENT_KINDS = ["skills"] as const;
|
|
43
47
|
|
|
44
48
|
/** One content kind whose files land somewhere in the project. */
|
|
45
49
|
export type WritableContentKind = (typeof WRITABLE_CONTENT_KINDS)[number];
|
|
@@ -48,7 +52,7 @@ export type WritableContentKind = (typeof WRITABLE_CONTENT_KINDS)[number];
|
|
|
48
52
|
export type RecipeTargetOptions = {
|
|
49
53
|
/** The project's `.sous/` directory. */
|
|
50
54
|
sousDir: string;
|
|
51
|
-
/** The merged project config, read for `
|
|
55
|
+
/** The merged project config, read for `recipes.skills.outputs`. */
|
|
52
56
|
settings: Settings;
|
|
53
57
|
/** The resolved settings scope, used to substitute `${var}` in destinations. */
|
|
54
58
|
scope?: VarScope;
|
|
@@ -98,7 +102,7 @@ export function destinationsFor(
|
|
|
98
102
|
kind: WritableContentKind,
|
|
99
103
|
options: RecipeTargetOptions
|
|
100
104
|
): string[] {
|
|
101
|
-
const configured = options.settings.
|
|
105
|
+
const configured = options.settings.recipes?.[kind]?.outputs;
|
|
102
106
|
|
|
103
107
|
if (configured === undefined || configured.length === 0) {
|
|
104
108
|
// `.claude/skills` is where every agent looks, so it is worth defaulting.
|
|
@@ -113,7 +117,7 @@ export function destinationsFor(
|
|
|
113
117
|
substituteVarsStrict(
|
|
114
118
|
destination,
|
|
115
119
|
options.scope ?? {},
|
|
116
|
-
`
|
|
120
|
+
`recipes.${kind}.outputs[${index}]`
|
|
117
121
|
)
|
|
118
122
|
)
|
|
119
123
|
);
|
|
@@ -143,7 +147,7 @@ export function buildRecipeTargets(options: RecipeTargetOptions): RecipeTargets
|
|
|
143
147
|
if (locked.length === 0) return result;
|
|
144
148
|
|
|
145
149
|
const destinationCache = new Map<WritableContentKind, string[]>();
|
|
146
|
-
|
|
150
|
+
let skippedPrompts = false;
|
|
147
151
|
const destinations = new Set<string>();
|
|
148
152
|
const watchDirs = new Set<string>();
|
|
149
153
|
|
|
@@ -158,6 +162,10 @@ export function buildRecipeTargets(options: RecipeTargetOptions): RecipeTargets
|
|
|
158
162
|
|
|
159
163
|
for (const content of manifest.contents) {
|
|
160
164
|
const kind = content.kind;
|
|
165
|
+
if (kind === "prompts") {
|
|
166
|
+
skippedPrompts = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
161
169
|
if (!isWritableKind(kind)) continue;
|
|
162
170
|
|
|
163
171
|
if (!destinationCache.has(kind)) {
|
|
@@ -165,10 +173,7 @@ export function buildRecipeTargets(options: RecipeTargetOptions): RecipeTargets
|
|
|
165
173
|
}
|
|
166
174
|
const kindDestinations = destinationCache.get(kind)!;
|
|
167
175
|
|
|
168
|
-
if (kindDestinations.length === 0)
|
|
169
|
-
kindsWithNowhereToGo.add(kind);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
176
|
+
if (kindDestinations.length === 0) continue;
|
|
172
177
|
|
|
173
178
|
for (const destination of kindDestinations) destinations.add(destination);
|
|
174
179
|
if (recipe.linked) watchDirs.add(recipe.dir);
|
|
@@ -198,16 +203,10 @@ export function buildRecipeTargets(options: RecipeTargetOptions): RecipeTargets
|
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
if (
|
|
202
|
-
const kinds = [...kindsWithNowhereToGo].sort();
|
|
206
|
+
if (skippedPrompts) {
|
|
203
207
|
result.warnings.push(
|
|
204
|
-
`Some subscribed recipes contribute
|
|
205
|
-
`
|
|
206
|
-
`Name a destination directory for each kind under the 'recipeOutputs' key of ` +
|
|
207
|
-
`your sous config, for example:\n` +
|
|
208
|
-
` recipeOutputs: { ${kinds
|
|
209
|
-
.map((kind) => `${kind}: ["\${projectRoot}/${kind}"]`)
|
|
210
|
-
.join(", ")} }`
|
|
208
|
+
`Some subscribed recipes contribute prompts, and the 'recipes' key of your sous ` +
|
|
209
|
+
`config has no destination for prompts yet, so they were skipped.`
|
|
211
210
|
);
|
|
212
211
|
}
|
|
213
212
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What was searched when a ref matched nothing.
|
|
3
3
|
*
|
|
4
|
-
* Resolving a ref itself lives in `src/
|
|
4
|
+
* Resolving a ref itself lives in `src/services/ref-resolver/`, which every command shares.
|
|
5
5
|
* All that is left here is the one thing only a repository search can say: which
|
|
6
6
|
* repositories were actually looked in, and which were trusted but unreadable,
|
|
7
7
|
* so the reader can tell "the name is wrong" from "the repository holding it
|
|
@@ -75,8 +75,15 @@ import {
|
|
|
75
75
|
type GatheredDeclaration,
|
|
76
76
|
} from "../declarations.js";
|
|
77
77
|
import type { DependencyKind } from "../formats/common.js";
|
|
78
|
-
import {
|
|
79
|
-
|
|
78
|
+
import {
|
|
79
|
+
RefSource,
|
|
80
|
+
isNamedRef,
|
|
81
|
+
locationOf,
|
|
82
|
+
namespaceOfKey,
|
|
83
|
+
rangeOf,
|
|
84
|
+
sharedRefResolver,
|
|
85
|
+
shortKey,
|
|
86
|
+
} from "../../../services/ref-resolver/index.js";
|
|
80
87
|
import type { SettledDependency } from "./settle.js";
|
|
81
88
|
|
|
82
89
|
/** A version that is ready to publish but has no tag yet. */
|
|
@@ -547,7 +554,7 @@ function declaredDependencies(
|
|
|
547
554
|
|
|
548
555
|
let readings;
|
|
549
556
|
try {
|
|
550
|
-
readings =
|
|
557
|
+
readings = sharedRefResolver().parse(written, RefSource.Manifest).refs;
|
|
551
558
|
} catch {
|
|
552
559
|
// A dependency that does not parse is already reported by validation.
|
|
553
560
|
continue;
|
|
@@ -555,32 +562,33 @@ function declaredDependencies(
|
|
|
555
562
|
// Several readings, or a browser path, are settled above or reported by
|
|
556
563
|
// the settling step; there is nothing more to record here.
|
|
557
564
|
const parsed = readings[0]!;
|
|
558
|
-
if (readings.length !== 1 || !
|
|
565
|
+
if (readings.length !== 1 || !isNamedRef(parsed)) continue;
|
|
559
566
|
|
|
560
|
-
|
|
567
|
+
const location = locationOf(parsed);
|
|
568
|
+
if (location !== undefined) {
|
|
561
569
|
// A whole namespace in another repository is read from that repository's
|
|
562
570
|
// own index by the consumer; only a recipe has a key to record.
|
|
563
|
-
if (parsed.
|
|
564
|
-
byKey.set(
|
|
571
|
+
if (parsed.kind === "recipe") {
|
|
572
|
+
byKey.set(shortKey(parsed), {
|
|
565
573
|
where: "remote",
|
|
566
|
-
repo:
|
|
567
|
-
range: parsed
|
|
574
|
+
repo: location.identity,
|
|
575
|
+
range: rangeOf(parsed) ?? "*",
|
|
568
576
|
});
|
|
569
|
-
cover(
|
|
577
|
+
cover(shortKey(parsed), { written, kind, named: true });
|
|
570
578
|
}
|
|
571
579
|
continue;
|
|
572
580
|
}
|
|
573
581
|
|
|
574
|
-
if (parsed.
|
|
575
|
-
addSibling(
|
|
576
|
-
cover(
|
|
582
|
+
if (parsed.kind === "recipe") {
|
|
583
|
+
addSibling(shortKey(parsed), rangeOf(parsed), true);
|
|
584
|
+
cover(shortKey(parsed), { written, kind, named: true });
|
|
577
585
|
continue;
|
|
578
586
|
}
|
|
579
587
|
|
|
580
588
|
// A whole-namespace dependency means every recipe in that namespace.
|
|
581
|
-
namespaces.add(parsed.
|
|
589
|
+
namespaces.add(parsed.name);
|
|
582
590
|
for (const entry of validation.recipes) {
|
|
583
|
-
if (entry.manifest.namespace !== parsed.
|
|
591
|
+
if (entry.manifest.namespace !== parsed.name) continue;
|
|
584
592
|
if (entry.key === recipe.key) continue;
|
|
585
593
|
addSibling(entry.key, undefined, false);
|
|
586
594
|
cover(entry.key, { written, kind, named: false });
|
|
@@ -24,8 +24,15 @@
|
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import semver from "semver";
|
|
26
26
|
import { hashDirectory } from "../store/hash.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
RefSource,
|
|
29
|
+
isNamedRef,
|
|
30
|
+
locationOf,
|
|
31
|
+
namespaceOf,
|
|
32
|
+
sharedRefResolver,
|
|
33
|
+
shortKey,
|
|
34
|
+
splitRecipeKey,
|
|
35
|
+
} from "../../../services/ref-resolver/index.js";
|
|
29
36
|
import type { RunOptions } from "../providers/git.js";
|
|
30
37
|
import { listRecipeTags, tagFor, withTaggedTree, type RecipeTag } from "./tags.js";
|
|
31
38
|
import { nextVersion, type BumpLevel } from "./bump.js";
|
|
@@ -346,20 +353,20 @@ export function siblingKeysOf(validation: RepoValidation, key: string): string[]
|
|
|
346
353
|
for (const written of declared) {
|
|
347
354
|
let readings;
|
|
348
355
|
try {
|
|
349
|
-
readings =
|
|
356
|
+
readings = sharedRefResolver().parse(written, RefSource.Manifest).refs;
|
|
350
357
|
} catch {
|
|
351
358
|
continue;
|
|
352
359
|
}
|
|
353
360
|
const parsed = readings[0]!;
|
|
354
|
-
if (!
|
|
361
|
+
if (!isNamedRef(parsed) || locationOf(parsed) !== undefined) continue;
|
|
355
362
|
|
|
356
|
-
if (parsed.
|
|
357
|
-
keys.push(
|
|
363
|
+
if (parsed.kind === "recipe") {
|
|
364
|
+
keys.push(shortKey(parsed));
|
|
358
365
|
continue;
|
|
359
366
|
}
|
|
360
367
|
// A whole-namespace dependency means every recipe in it.
|
|
361
368
|
for (const entry of validation.recipes) {
|
|
362
|
-
if (entry.manifest.namespace === parsed
|
|
369
|
+
if (entry.manifest.namespace === namespaceOf(parsed)!.name && entry.key !== key) {
|
|
363
370
|
keys.push(entry.key);
|
|
364
371
|
}
|
|
365
372
|
}
|
|
@@ -21,26 +21,17 @@
|
|
|
21
21
|
* naming the spellings that read one way (the `/*` form for the namespace).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import { INDEX_FILENAME } from "../formats/common.js";
|
|
24
|
+
import type { ProviderOptions, RepoProvider } from "../providers/index.js";
|
|
26
25
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
parseRef,
|
|
37
|
-
refKey,
|
|
38
|
-
type ParsedRef,
|
|
39
|
-
type RefLocation,
|
|
40
|
-
type RefReading,
|
|
41
|
-
} from "../../refs/parse.js";
|
|
42
|
-
import { settleInIndex } from "../../refs/settle.js";
|
|
43
|
-
import { RefSource } from "../../refs/scopes.js";
|
|
26
|
+
FetchedIndexLookup,
|
|
27
|
+
RefSource,
|
|
28
|
+
locationOf,
|
|
29
|
+
namespaceOf,
|
|
30
|
+
rangeOf,
|
|
31
|
+
sharedRefResolver,
|
|
32
|
+
shortKey,
|
|
33
|
+
type SousRef,
|
|
34
|
+
} from "../../../services/ref-resolver/index.js";
|
|
44
35
|
import { describeError, type RepoValidation, type ValidationProblem } from "./validate.js";
|
|
45
36
|
|
|
46
37
|
/** What one dependency settled on. */
|
|
@@ -78,8 +69,11 @@ export type SettleOptions = {
|
|
|
78
69
|
*
|
|
79
70
|
* @param readings - Every reading of the dependency.
|
|
80
71
|
*/
|
|
81
|
-
export function needsSettling(readings:
|
|
82
|
-
return
|
|
72
|
+
export function needsSettling(readings: SousRef[]): boolean {
|
|
73
|
+
return (
|
|
74
|
+
readings.length > 1 ||
|
|
75
|
+
readings.some((reading) => reading.kind === "repo" && reading.browsed !== undefined)
|
|
76
|
+
);
|
|
83
77
|
}
|
|
84
78
|
|
|
85
79
|
/**
|
|
@@ -95,19 +89,13 @@ export async function settleDependencyLocations(
|
|
|
95
89
|
validation: RepoValidation,
|
|
96
90
|
options: SettleOptions = {}
|
|
97
91
|
): Promise<SettleResult> {
|
|
98
|
-
const providers = options.providers ?? builtInProviders();
|
|
99
92
|
const settled = new Map<string, SettledDependency>();
|
|
100
93
|
const problems: ValidationProblem[] = [];
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
pending = fetchIndexAt(location, providers, options.providerOptions ?? {});
|
|
107
|
-
indexes.set(location.identity, pending);
|
|
108
|
-
}
|
|
109
|
-
return pending;
|
|
110
|
-
};
|
|
94
|
+
const lookup = new FetchedIndexLookup({
|
|
95
|
+
subject: "dependency",
|
|
96
|
+
...(options.providers === undefined ? {} : { providers: options.providers }),
|
|
97
|
+
...(options.providerOptions === undefined ? {} : { providerOptions: options.providerOptions }),
|
|
98
|
+
});
|
|
111
99
|
|
|
112
100
|
for (const recipe of validation.recipes) {
|
|
113
101
|
const declared = [...(recipe.manifest.depends ?? []), ...(recipe.manifest.subscribes ?? [])];
|
|
@@ -115,9 +103,9 @@ export async function settleDependencyLocations(
|
|
|
115
103
|
const trimmed = written.trim();
|
|
116
104
|
if (settled.has(trimmed)) continue;
|
|
117
105
|
|
|
118
|
-
let readings:
|
|
106
|
+
let readings: SousRef[];
|
|
119
107
|
try {
|
|
120
|
-
readings =
|
|
108
|
+
readings = sharedRefResolver().parse(trimmed, RefSource.Manifest).refs;
|
|
121
109
|
} catch {
|
|
122
110
|
// Validation already reported a dependency that does not parse.
|
|
123
111
|
continue;
|
|
@@ -126,7 +114,7 @@ export async function settleDependencyLocations(
|
|
|
126
114
|
|
|
127
115
|
const where = `${recipe.path} ('${trimmed}')`;
|
|
128
116
|
try {
|
|
129
|
-
settled.set(trimmed, await settleOne(
|
|
117
|
+
settled.set(trimmed, await settleOne(readings, lookup));
|
|
130
118
|
} catch (error) {
|
|
131
119
|
problems.push({ level: "error", where, message: describeError(error) });
|
|
132
120
|
}
|
|
@@ -137,101 +125,30 @@ export async function settleDependencyLocations(
|
|
|
137
125
|
}
|
|
138
126
|
|
|
139
127
|
/**
|
|
140
|
-
* Settles one dependency:
|
|
141
|
-
*
|
|
128
|
+
* Settles one dependency: the one reading whose repository's index publishes
|
|
129
|
+
* what it names.
|
|
142
130
|
*
|
|
143
|
-
* @param
|
|
144
|
-
* @param
|
|
145
|
-
* @param indexOf - Fetches (once) the index at a location.
|
|
146
|
-
* @param providers - The providers, for formatting.
|
|
131
|
+
* @param readings - Every reading of the dependency.
|
|
132
|
+
* @param lookup - The fetched indexes.
|
|
147
133
|
*/
|
|
148
134
|
async function settleOne(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
indexOf: (location: RefLocation) => Promise<IndexFile>,
|
|
152
|
-
providers: RepoProvider[]
|
|
135
|
+
readings: SousRef[],
|
|
136
|
+
lookup: FetchedIndexLookup
|
|
153
137
|
): Promise<SettledDependency> {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
/* c8 ignore next */
|
|
159
|
-
if (location === undefined) continue;
|
|
160
|
-
|
|
161
|
-
let index: IndexFile;
|
|
162
|
-
try {
|
|
163
|
-
index = await indexOf(location);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
throw new Error(
|
|
166
|
-
`the dependency reads ${readings.length === 1 ? "as a folder in" : "as something in"} ` +
|
|
167
|
-
`the repository at ${location.url}, and its index could not be read, so the release ` +
|
|
168
|
-
`cannot settle what it means. A release never guesses past an index it could not ` +
|
|
169
|
-
`read.\n ${describeError(error)}`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const settled = settleInIndex(reading, {
|
|
174
|
-
namespaces: Object.keys(index.namespaces),
|
|
175
|
-
recipes: index.recipes,
|
|
176
|
-
});
|
|
177
|
-
if (settled.length > 0) found.push({ reading, settled, index });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (found.length === 0) {
|
|
181
|
-
throw new Error(
|
|
182
|
-
`no repository it could name publishes what it names. It was read as:\n` +
|
|
183
|
-
readings.map((reading) => ` ${formatRef(reading, providers)}`).join("\n")
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const choices = found.flatMap((entry) => entry.settled);
|
|
188
|
-
if (choices.length > 1) {
|
|
189
|
-
throw new Error(
|
|
190
|
-
`it names more than one thing, and each of these publishes it:\n` +
|
|
191
|
-
choices.map((choice) => ` ${formatRef(choice, providers)}`).join("\n") +
|
|
192
|
-
`\n Write the one you mean. A namespace is written with '/*' after it, as in ` +
|
|
193
|
-
`'${spelledOut(choices.find((choice) => choice.recipe === undefined) ?? choices[0]!, providers)}', ` +
|
|
194
|
-
`and a recipe as its canonical locator above.`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const { index } = found[0]!;
|
|
199
|
-
const choice = choices[0]!;
|
|
138
|
+
const { ref: choice } = await lookup.settle(readings);
|
|
139
|
+
const location = locationOf(choice)!;
|
|
140
|
+
const index = await lookup.indexAt(location);
|
|
141
|
+
const namespace = namespaceOf(choice)?.name;
|
|
200
142
|
const keys =
|
|
201
|
-
choice.
|
|
143
|
+
choice.kind === "namespace"
|
|
202
144
|
? Object.keys(index.recipes)
|
|
203
|
-
.filter((key) => key.startsWith(`${
|
|
145
|
+
.filter((key) => key.startsWith(`${namespace}/`))
|
|
204
146
|
.sort()
|
|
205
|
-
: [
|
|
206
|
-
const range =
|
|
147
|
+
: [shortKey(choice)];
|
|
148
|
+
const range = readings.map(rangeOf).find((written) => written !== undefined);
|
|
207
149
|
return {
|
|
208
|
-
identity:
|
|
150
|
+
identity: location.identity,
|
|
209
151
|
keys,
|
|
210
152
|
...(range === undefined ? {} : { range }),
|
|
211
153
|
};
|
|
212
154
|
}
|
|
213
|
-
|
|
214
|
-
/** A reading spelled so it reads one way: a namespace with `/*` after it. */
|
|
215
|
-
function spelledOut(reading: ParsedRef, providers: RepoProvider[]): string {
|
|
216
|
-
const written = formatRef(reading, providers);
|
|
217
|
-
return reading.recipe === undefined && isNamedReading(reading) ? `${written}/*` : written;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Fetches and validates the index at a location, through its provider.
|
|
222
|
-
*
|
|
223
|
-
* @param location - Where the repository lives.
|
|
224
|
-
* @param providers - The providers to fetch through.
|
|
225
|
-
* @param options - Testing seams.
|
|
226
|
-
*/
|
|
227
|
-
async function fetchIndexAt(
|
|
228
|
-
location: RefLocation,
|
|
229
|
-
providers: RepoProvider[],
|
|
230
|
-
options: ProviderOptions
|
|
231
|
-
): Promise<IndexFile> {
|
|
232
|
-
const provider = providerById(location.provider, providers);
|
|
233
|
-
/* c8 ignore next */
|
|
234
|
-
if (provider === undefined) throw new Error(`sous has no '${location.provider}' provider.`);
|
|
235
|
-
const fetched = await provider.fetchIndex(provider.canonicalize(location.url), options);
|
|
236
|
-
return parseIndexFile(JSON.parse(fetched.text), `${location.url}/${INDEX_FILENAME}`);
|
|
237
|
-
}
|