@sous-io/sous 0.2.36 → 1.0.1
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 +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -237
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -391
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -412
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1105
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The real namespace resolver: the one the compiler uses.
|
|
3
|
-
*
|
|
4
|
-
* Phase 4 defined the `NamespaceResolver` contract and shipped an in-memory
|
|
5
|
-
* implementation for tests. This is the implementation backed by the project's
|
|
6
|
-
* actual state: the lockfile says which recipes are in play and at what version,
|
|
7
|
-
* the links map and the store say where each one's files are, and the
|
|
8
|
-
* lockfile's holder lists say what each recipe is allowed to address.
|
|
9
|
-
*
|
|
10
|
-
* The scoping rule is the whole point, and it is enforced by handing
|
|
11
|
-
* `StaticNamespaceResolver` the right two maps:
|
|
12
|
-
*
|
|
13
|
-
* - a file inside a recipe may address only that recipe's own declared
|
|
14
|
-
* dependencies (`depends` plus `subscribes`), at their pinned versions. The
|
|
15
|
-
* lockfile records which keys each recipe holds, however its manifest wrote
|
|
16
|
-
* them (a sibling ref, a locator, a URL copied from the browser), so that
|
|
17
|
-
* record is what decides it;
|
|
18
|
-
* - a file in the project's own templates may address every recipe the
|
|
19
|
-
* lockfile pins, whatever holds it: the project, a set it subscribes to,
|
|
20
|
-
* or another recipe's `depends`.
|
|
21
|
-
*
|
|
22
|
-
* When a project template names a recipe the lockfile does not pin, the error
|
|
23
|
-
* says which recipe used to bring it in, when the cached indexes or the config
|
|
24
|
-
* can tell.
|
|
25
|
-
*
|
|
26
|
-
* A project with no lockfile entries gets no resolver at all, so a project that
|
|
27
|
-
* uses no repositories behaves exactly as it did before: a `~` reference in an
|
|
28
|
-
* include line is just a relative path.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import semver from "semver";
|
|
32
|
-
import type { Settings } from "../settings.js";
|
|
33
|
-
import { resolveStoreRoot } from "../sous-home.js";
|
|
34
|
-
import {
|
|
35
|
-
RefSource,
|
|
36
|
-
namespaceOfKey,
|
|
37
|
-
parseNamedRef,
|
|
38
|
-
shortKey,
|
|
39
|
-
} from "../../services/ref-resolver/index.js";
|
|
40
|
-
import {
|
|
41
|
-
StaticNamespaceResolver,
|
|
42
|
-
type DroppedRecipe,
|
|
43
|
-
type NamespaceResolver,
|
|
44
|
-
} from "./namespace-resolver.js";
|
|
45
|
-
import {
|
|
46
|
-
listLockedRecipes,
|
|
47
|
-
readProjectLockfile,
|
|
48
|
-
type LockedRecipeLocation,
|
|
49
|
-
} from "./locked-recipes.js";
|
|
50
|
-
import { createIndexCache } from "./providers/index.js";
|
|
51
|
-
import type { IndexFile } from "./formats/index-file.js";
|
|
52
|
-
import type { Lockfile } from "./formats/lockfile.js";
|
|
53
|
-
|
|
54
|
-
/** How the project's namespace resolver is built. */
|
|
55
|
-
export type ProjectNamespaceResolverOptions = {
|
|
56
|
-
/** The project's `.sous/` directory, which holds the lockfile and the links map. */
|
|
57
|
-
sousDir: string;
|
|
58
|
-
/** The merged project config, read for its subscriptions. */
|
|
59
|
-
settings: Settings;
|
|
60
|
-
/** The environment to read; decides where the store and machine-wide links map are. */
|
|
61
|
-
env?: NodeJS.ProcessEnv;
|
|
62
|
-
/** The locked recipes, when the caller has already located them. */
|
|
63
|
-
locked?: LockedRecipeLocation[];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Builds the namespace resolver for a project, or undefined when the project
|
|
68
|
-
* locks no recipes at all.
|
|
69
|
-
*
|
|
70
|
-
* createProjectNamespaceResolver({ sousDir, settings })
|
|
71
|
-
* // -> resolves "@~workflow/task-files/_partials/resume.md" (or a glob such as
|
|
72
|
-
* // "@~*" + "/*" + "/memories/*.md") to the pinned
|
|
73
|
-
* // recipe directory, when the project (or the including recipe) may address it
|
|
74
|
-
*
|
|
75
|
-
* @param options - The project's `.sous/` directory, its config and the environment.
|
|
76
|
-
*/
|
|
77
|
-
export function createProjectNamespaceResolver(
|
|
78
|
-
options: ProjectNamespaceResolverOptions
|
|
79
|
-
): NamespaceResolver | undefined {
|
|
80
|
-
const locked =
|
|
81
|
-
options.locked ??
|
|
82
|
-
listLockedRecipes({
|
|
83
|
-
sousDir: options.sousDir,
|
|
84
|
-
...(options.env === undefined ? {} : { env: options.env }),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (locked.length === 0) return undefined;
|
|
88
|
-
|
|
89
|
-
const recipes: Record<string, string> = {};
|
|
90
|
-
const repos: Record<string, string> = {};
|
|
91
|
-
const dependencies: Record<string, string[]> = {};
|
|
92
|
-
|
|
93
|
-
for (const recipe of locked) {
|
|
94
|
-
recipes[recipe.key] = recipe.dir;
|
|
95
|
-
repos[recipe.key] = recipe.repo;
|
|
96
|
-
|
|
97
|
-
// Every recipe holding this one declared it, in `depends` or `subscribes`,
|
|
98
|
-
// and the lockfile records the key that declaration resolved to.
|
|
99
|
-
for (const holder of recipe.requestedBy) {
|
|
100
|
-
(dependencies[holder] ??= []).push(recipe.key);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// No `projectScope`: a project template may address every recipe the
|
|
105
|
-
// lockfile pins, and those are exactly the recipes this resolver knows.
|
|
106
|
-
return new StaticNamespaceResolver({
|
|
107
|
-
recipes,
|
|
108
|
-
repos,
|
|
109
|
-
dependencies,
|
|
110
|
-
explainMissing: (recipe) => explainUnpinnedRecipe(recipe, options),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Why the lockfile does not pin a recipe a project template asked for, when
|
|
116
|
-
* that can be known without the network. Two things are checked, in order:
|
|
117
|
-
*
|
|
118
|
-
* - a subscription to it (or to its namespace) that is switched off;
|
|
119
|
-
* - another published version of a pinned recipe that brought it in, read
|
|
120
|
-
* from the cached indexes of the lockfile's repositories. That version may
|
|
121
|
-
* declare it directly, or declare a recipe that no longer is pinned either
|
|
122
|
-
* and that declares it in turn; the shortest such chain is reported, and
|
|
123
|
-
* among the versions of one pinned recipe the newest wins. This is the
|
|
124
|
-
* usual story of a set that dropped one of its members, and the members'
|
|
125
|
-
* own libraries with it.
|
|
126
|
-
*
|
|
127
|
-
* Only ever runs while an error is being reported, so it reads the lockfile
|
|
128
|
-
* and the cached indexes then rather than on every build.
|
|
129
|
-
*
|
|
130
|
-
* @param recipe - The `namespace/recipe` the template asked for.
|
|
131
|
-
* @param options - The project's `.sous/` directory, its config and the environment.
|
|
132
|
-
*/
|
|
133
|
-
export function explainUnpinnedRecipe(
|
|
134
|
-
recipe: string,
|
|
135
|
-
options: Pick<ProjectNamespaceResolverOptions, "sousDir" | "settings" | "env">
|
|
136
|
-
): DroppedRecipe | undefined {
|
|
137
|
-
const namespace = namespaceOfKey(recipe);
|
|
138
|
-
for (const [key, entry] of Object.entries(options.settings.subscriptions ?? {})) {
|
|
139
|
-
if (entry?.enabled !== false) continue;
|
|
140
|
-
let ref: string;
|
|
141
|
-
try {
|
|
142
|
-
ref = shortKey(parseNamedRef(key, RefSource.Config));
|
|
143
|
-
} catch {
|
|
144
|
-
// The config schema already refuses a key that is not a ref.
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (ref === recipe || ref === namespace) {
|
|
148
|
-
return { by: "disabled-subscription", subscription: key };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let lock: Lockfile;
|
|
153
|
-
try {
|
|
154
|
-
lock = readProjectLockfile(options.sousDir);
|
|
155
|
-
} catch {
|
|
156
|
-
// The build reports a broken lockfile on its own; an error message is no
|
|
157
|
-
// place to report it a second time.
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const published = publishedVersions(lock, options.env ?? process.env);
|
|
162
|
-
|
|
163
|
-
/** What one published version of a recipe declares, as recipe keys. */
|
|
164
|
-
const declaredBy = (key: string, version: string): string[] =>
|
|
165
|
-
Object.keys(published.get(key)?.[version]?.dependencies ?? {});
|
|
166
|
-
|
|
167
|
-
// Breadth first from every other version of every pinned recipe, through
|
|
168
|
-
// recipes the lockfile does not pin, so the shortest chain is found first.
|
|
169
|
-
type Step = { key: string; root: { recipe: string; version: string }; through: string[] };
|
|
170
|
-
const queue: Step[] = [];
|
|
171
|
-
for (const holder of Object.keys(lock.recipes).sort()) {
|
|
172
|
-
const pinned = lock.recipes[holder]!.version;
|
|
173
|
-
const versions = Object.keys(published.get(holder) ?? {})
|
|
174
|
-
.filter((version) => version !== pinned && semver.valid(version) !== null)
|
|
175
|
-
.sort(semver.rcompare);
|
|
176
|
-
for (const version of versions) {
|
|
177
|
-
for (const key of declaredBy(holder, version)) {
|
|
178
|
-
queue.push({ key, root: { recipe: holder, version }, through: [] });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const seen = new Set<string>();
|
|
184
|
-
while (queue.length > 0) {
|
|
185
|
-
const step = queue.shift()!;
|
|
186
|
-
if (step.key === recipe) {
|
|
187
|
-
const root = step.root;
|
|
188
|
-
return {
|
|
189
|
-
by: "recipe",
|
|
190
|
-
recipe: root.recipe,
|
|
191
|
-
declaredAt: root.version,
|
|
192
|
-
pinned: lock.recipes[root.recipe]!.version,
|
|
193
|
-
...(step.through.length > 0 ? { through: step.through } : {}),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
// A pinned recipe is explained by its own entry, and each recipe is
|
|
197
|
-
// walked once.
|
|
198
|
-
if (Object.hasOwn(lock.recipes, step.key) || seen.has(step.key)) continue;
|
|
199
|
-
seen.add(step.key);
|
|
200
|
-
for (const version of Object.keys(published.get(step.key) ?? {})) {
|
|
201
|
-
for (const key of declaredBy(step.key, version)) {
|
|
202
|
-
queue.push({ key, root: step.root, through: [...step.through, step.key] });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Every published version of every recipe in the cached indexes of the
|
|
212
|
-
* repositories the lockfile names, keyed by recipe. A repository with no
|
|
213
|
-
* cached index contributes nothing.
|
|
214
|
-
*
|
|
215
|
-
* @param lock - The project's lockfile.
|
|
216
|
-
* @param env - The environment, which decides where the store is.
|
|
217
|
-
*/
|
|
218
|
-
function publishedVersions(
|
|
219
|
-
lock: Lockfile,
|
|
220
|
-
env: NodeJS.ProcessEnv
|
|
221
|
-
): Map<string, IndexFile["recipes"][string]["versions"]> {
|
|
222
|
-
const cache = createIndexCache({ storeRoot: resolveStoreRoot(env) });
|
|
223
|
-
const found = new Map<string, IndexFile["recipes"][string]["versions"]>();
|
|
224
|
-
const identities = new Set(Object.values(lock.repos).map((repo) => repo.identity));
|
|
225
|
-
for (const identity of [...identities].sort()) {
|
|
226
|
-
const index = cache.readCached(identity);
|
|
227
|
-
for (const [key, entry] of Object.entries(index?.recipes ?? {})) {
|
|
228
|
-
if (!found.has(key)) found.set(key, entry.versions);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return found;
|
|
232
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What an include line may name: the recipes the lockfile pins.
|
|
3
|
-
*
|
|
4
|
-
* A `RefLookup` over the pinned recipes, answering a `recipeFile` reading (the
|
|
5
|
-
* namespace and recipe names, and the path inside the recipe) with every pinned
|
|
6
|
-
* recipe whose names match it. Names are compared as the ref service compares
|
|
7
|
-
* every name: exact spelling first, ignoring case as the fallback, and a name
|
|
8
|
-
* may be a glob (`*`, `**`, `?`, `[..]`, `{a,b}`). The path inside the recipe
|
|
9
|
-
* is carried through as written; expanding it into files is the include
|
|
10
|
-
* resolver's job, because only it knows which directory each recipe lives in.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
compareBytewise,
|
|
15
|
-
matchName,
|
|
16
|
-
repoOf,
|
|
17
|
-
type NameMatch,
|
|
18
|
-
type RefMatch,
|
|
19
|
-
type SousRef,
|
|
20
|
-
type SyncRefLookup,
|
|
21
|
-
} from "../../services/ref-resolver/index.js";
|
|
22
|
-
|
|
23
|
-
/** One pinned recipe, as far as a lookup needs to know it. */
|
|
24
|
-
export type RecipeFileEntry = {
|
|
25
|
-
/** The recipe's namespace. */
|
|
26
|
-
namespace: string;
|
|
27
|
-
/** The recipe's name. */
|
|
28
|
-
name: string;
|
|
29
|
-
/** The short name of the repository it came from, when known. */
|
|
30
|
-
repo?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** The weakest of two matches: a name that matched only ignoring case spoils an exact one. */
|
|
34
|
-
function combine(left: NameMatch, right: NameMatch): NameMatch {
|
|
35
|
-
if (left === undefined || right === undefined) return undefined;
|
|
36
|
-
return left === "exact" && right === "exact" ? "exact" : "folded";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Answers which pinned recipes an include reading names. */
|
|
40
|
-
export class LockedRecipeFileLookup implements SyncRefLookup {
|
|
41
|
-
/**
|
|
42
|
-
* @param entries - Every recipe the lockfile pins.
|
|
43
|
-
*/
|
|
44
|
-
constructor(private readonly entries: readonly RecipeFileEntry[]) {}
|
|
45
|
-
|
|
46
|
-
/** The distinct namespaces of the pinned recipes, sorted. */
|
|
47
|
-
namespaces(): string[] {
|
|
48
|
-
return [...new Set(this.entries.map((entry) => entry.namespace))].sort(compareBytewise);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The pinned recipes of the namespaces a written namespace matches, sorted
|
|
53
|
-
* by key: those whose spelling matches exactly, or, when none does, those
|
|
54
|
-
* that differ only in case.
|
|
55
|
-
*
|
|
56
|
-
* @param written - The namespace as written, possibly a glob.
|
|
57
|
-
*/
|
|
58
|
-
recipesInNamespace(written: string): string[] {
|
|
59
|
-
const matched = this.entries
|
|
60
|
-
.map((entry) => ({ entry, match: matchName(written, entry.namespace) }))
|
|
61
|
-
.filter((hit) => hit.match !== undefined);
|
|
62
|
-
const exact = matched.filter((hit) => hit.match === "exact");
|
|
63
|
-
return (exact.length > 0 ? exact : matched)
|
|
64
|
-
.map((hit) => `${hit.entry.namespace}/${hit.entry.name}`)
|
|
65
|
-
.sort(compareBytewise);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* True when a written namespace matches a pinned one in some spelling.
|
|
70
|
-
*
|
|
71
|
-
* @param written - The namespace as written, possibly a glob.
|
|
72
|
-
*/
|
|
73
|
-
knowsNamespace(written: string): boolean {
|
|
74
|
-
return this.entries.some((entry) => matchName(written, entry.namespace) !== undefined);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
findSync(candidate: SousRef): RefMatch[] {
|
|
78
|
-
if (candidate.kind !== "recipeFile") return [];
|
|
79
|
-
const written = candidate.recipe;
|
|
80
|
-
const namespaceName = written.namespace?.name;
|
|
81
|
-
if (namespaceName === undefined) return [];
|
|
82
|
-
const repoName = repoOf(candidate)?.name;
|
|
83
|
-
|
|
84
|
-
const found: RefMatch[] = [];
|
|
85
|
-
for (const entry of this.entries) {
|
|
86
|
-
let match = combine(
|
|
87
|
-
matchName(namespaceName, entry.namespace),
|
|
88
|
-
matchName(written.name, entry.name)
|
|
89
|
-
);
|
|
90
|
-
if (repoName !== undefined) {
|
|
91
|
-
match = combine(
|
|
92
|
-
match,
|
|
93
|
-
entry.repo === undefined ? undefined : matchName(repoName, entry.repo)
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
if (match === undefined) continue;
|
|
97
|
-
found.push({
|
|
98
|
-
ref: {
|
|
99
|
-
kind: "recipeFile",
|
|
100
|
-
path: candidate.path,
|
|
101
|
-
recipe: {
|
|
102
|
-
kind: "recipe",
|
|
103
|
-
name: entry.name,
|
|
104
|
-
namespace: { kind: "namespace", name: entry.namespace },
|
|
105
|
-
},
|
|
106
|
-
...(candidate.glob === undefined ? {} : { glob: true as const }),
|
|
107
|
-
...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
|
|
108
|
-
},
|
|
109
|
-
exactSpelling: match === "exact",
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return found.sort((left, right) => compareBytewise(keyOf(left.ref), keyOf(right.ref)));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async find(candidate: SousRef): Promise<RefMatch[]> {
|
|
116
|
-
return this.findSync(candidate);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** The `namespace/recipe` key of a recipe file ref, for ordering. */
|
|
121
|
-
function keyOf(ref: SousRef): string {
|
|
122
|
-
return ref.kind === "recipeFile" ? `${ref.recipe.namespace?.name}/${ref.recipe.name}` : "";
|
|
123
|
-
}
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a locked recipe's files actually are.
|
|
3
|
-
*
|
|
4
|
-
* The lockfile pins a recipe to a repository, a version and a content hash; it
|
|
5
|
-
* does not say where the bytes live. Three consumers need that answer and must
|
|
6
|
-
* all get the same one: the namespace resolver (which recipe directory a
|
|
7
|
-
* `~namespace` include lands in), the variable definition source (which
|
|
8
|
-
* manifests `sous vars` reads), and the recipe compile targets (which files a
|
|
9
|
-
* build copies or renders).
|
|
10
|
-
*
|
|
11
|
-
* There are two possible homes, and the order matters. A LINKED repository is
|
|
12
|
-
* read from its working copy, because a link is a deliberate instruction to
|
|
13
|
-
* bypass versions and the lockfile; everything else is read from the immutable
|
|
14
|
-
* store entry at the locked version. Nothing here fetches anything: a recipe the
|
|
15
|
-
* store does not hold yet is reported as absent so the caller can restore it.
|
|
16
|
-
*
|
|
17
|
-
* Reading is done once per call and cached inside the returned list, because a
|
|
18
|
-
* build asks for the same answer several times in a row.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import fs from "node:fs";
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
import { resolveStoreRoot } from "../sous-home.js";
|
|
24
|
-
import { parseLockfile, type LockKind, type Lockfile } from "./formats/lockfile.js";
|
|
25
|
-
import { parseRecipeManifest, type RecipeManifest } from "./formats/recipe-manifest.js";
|
|
26
|
-
import { parseRepoManifest, type RepoManifest } from "./formats/repo-manifest.js";
|
|
27
|
-
import { LOCKFILE_FILENAME } from "./formats/common.js";
|
|
28
|
-
import {
|
|
29
|
-
findRecipeManifest,
|
|
30
|
-
findRepoManifest,
|
|
31
|
-
loadJsonFile,
|
|
32
|
-
loadManifestFile,
|
|
33
|
-
} from "./load-manifest.js";
|
|
34
|
-
import { readEffectiveLinks } from "./links.js";
|
|
35
|
-
import { identitySegments } from "./identity.js";
|
|
36
|
-
import { splitRecipeKey } from "../../services/ref-resolver/index.js";
|
|
37
|
-
import { PROJECT_HOLDER } from "./formats/lockfile.js";
|
|
38
|
-
|
|
39
|
-
/** One locked recipe, together with the directory its files are read from. */
|
|
40
|
-
export type LockedRecipeLocation = {
|
|
41
|
-
/** The recipe key, `namespace/recipe`. */
|
|
42
|
-
key: string;
|
|
43
|
-
/** The short name of the repository it came from. */
|
|
44
|
-
repo: string;
|
|
45
|
-
/** The recipe's namespace. */
|
|
46
|
-
namespace: string;
|
|
47
|
-
/** The recipe's name. */
|
|
48
|
-
name: string;
|
|
49
|
-
/** The exact version the lockfile pins. */
|
|
50
|
-
version: string;
|
|
51
|
-
/** The content hash the lockfile pins. */
|
|
52
|
-
hash: string;
|
|
53
|
-
/** Whether anything holds it as a co-subscription, or only as a build dependency. */
|
|
54
|
-
kind: LockKind;
|
|
55
|
-
/** Everyone holding it: "project", and the key of every recipe that asked. */
|
|
56
|
-
requestedBy: string[];
|
|
57
|
-
/** Absolute directory holding the recipe's files. */
|
|
58
|
-
dir: string;
|
|
59
|
-
/** True when `dir` is a linked working copy rather than a store entry. */
|
|
60
|
-
linked: boolean;
|
|
61
|
-
/** True when `dir` exists on disk right now. */
|
|
62
|
-
present: boolean;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/** How locked recipes are located. */
|
|
66
|
-
export type LockedRecipeOptions = {
|
|
67
|
-
/** The project's `.sous/` directory, which holds the lockfile and the links map. */
|
|
68
|
-
sousDir: string;
|
|
69
|
-
/** The environment to read; decides where the store and the machine-wide links map are. */
|
|
70
|
-
env?: NodeJS.ProcessEnv;
|
|
71
|
-
/** The store root to use instead of the one the environment implies. */
|
|
72
|
-
storeRoot?: string;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Reads and validates a project's lockfile, returning an empty one when the
|
|
77
|
-
* project has locked nothing yet.
|
|
78
|
-
*
|
|
79
|
-
* @param sousDir - The project's `.sous/` directory.
|
|
80
|
-
*/
|
|
81
|
-
export function readProjectLockfile(sousDir: string): Lockfile {
|
|
82
|
-
const filePath = path.join(sousDir, LOCKFILE_FILENAME);
|
|
83
|
-
if (!fs.existsSync(filePath)) return { formatVersion: 1, repos: {}, recipes: {} };
|
|
84
|
-
return parseLockfile(loadJsonFile(filePath, "lockfile"), filePath);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Reads and validates the recipe manifest in a directory, returning undefined
|
|
89
|
-
* when the directory has none or does not exist. A recipe that is not on disk
|
|
90
|
-
* yet is an ordinary state (a fresh clone before restore), never an error here.
|
|
91
|
-
*
|
|
92
|
-
* @param recipeDir - The directory holding the recipe's files.
|
|
93
|
-
*/
|
|
94
|
-
export function readRecipeManifestIn(recipeDir: string): RecipeManifest | undefined {
|
|
95
|
-
let manifestPath: string | undefined;
|
|
96
|
-
try {
|
|
97
|
-
manifestPath = findRecipeManifest(recipeDir);
|
|
98
|
-
} catch {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
if (manifestPath === undefined) return undefined;
|
|
102
|
-
return parseRecipeManifest(loadManifestFile(manifestPath), manifestPath);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Reads and validates the repo manifest at the root of a checkout, returning
|
|
107
|
-
* undefined when there is none.
|
|
108
|
-
*
|
|
109
|
-
* @param repoRoot - The checkout's root directory.
|
|
110
|
-
*/
|
|
111
|
-
export function readRepoManifestIn(repoRoot: string): RepoManifest | undefined {
|
|
112
|
-
let manifestPath: string | undefined;
|
|
113
|
-
try {
|
|
114
|
-
manifestPath = findRepoManifest(repoRoot);
|
|
115
|
-
} catch {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
if (manifestPath === undefined) return undefined;
|
|
119
|
-
return parseRepoManifest(loadManifestFile(manifestPath), manifestPath);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Maps every recipe a linked checkout publishes to its directory, by reading the
|
|
124
|
-
* checkout's repo manifest and then each recipe folder's own manifest. A folder
|
|
125
|
-
* the repo manifest lists but that holds no readable recipe manifest is skipped
|
|
126
|
-
* rather than failing the build: a working copy is edited by hand and is allowed
|
|
127
|
-
* to be mid-change.
|
|
128
|
-
*
|
|
129
|
-
* @param checkoutDir - The linked working copy's root directory.
|
|
130
|
-
*/
|
|
131
|
-
export function mapLinkedRecipes(checkoutDir: string): Record<string, string> {
|
|
132
|
-
const manifest = readRepoManifestIn(checkoutDir);
|
|
133
|
-
if (manifest === undefined) return {};
|
|
134
|
-
|
|
135
|
-
const found: Record<string, string> = {};
|
|
136
|
-
for (const relative of manifest.recipes) {
|
|
137
|
-
const recipeDir = path.join(checkoutDir, relative);
|
|
138
|
-
const recipe = readRecipeManifestIn(recipeDir);
|
|
139
|
-
if (recipe === undefined) continue;
|
|
140
|
-
found[`${recipe.namespace}/${recipe.name}`] = recipeDir;
|
|
141
|
-
}
|
|
142
|
-
return found;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Every recipe the lockfile pins, with the directory each one's files are read
|
|
147
|
-
* from. Linked repositories win over the store, and a recipe whose directory is
|
|
148
|
-
* not there yet comes back with `present: false`.
|
|
149
|
-
*
|
|
150
|
-
* @param options - The project's `.sous/` directory and the environment.
|
|
151
|
-
*/
|
|
152
|
-
export function listLockedRecipes(
|
|
153
|
-
options: LockedRecipeOptions
|
|
154
|
-
): LockedRecipeLocation[] {
|
|
155
|
-
const env = options.env ?? process.env;
|
|
156
|
-
const lock = readProjectLockfile(options.sousDir);
|
|
157
|
-
const keys = Object.keys(lock.recipes);
|
|
158
|
-
if (keys.length === 0) return [];
|
|
159
|
-
|
|
160
|
-
const storeRoot = options.storeRoot ?? resolveStoreRoot(env);
|
|
161
|
-
const links = readEffectiveLinks(options.sousDir, env);
|
|
162
|
-
const linkedRecipes = new Map<string, Record<string, string>>();
|
|
163
|
-
|
|
164
|
-
const located: LockedRecipeLocation[] = [];
|
|
165
|
-
|
|
166
|
-
for (const key of keys.sort()) {
|
|
167
|
-
const entry = lock.recipes[key]!;
|
|
168
|
-
const { namespace, name } = splitRecipeKey(key);
|
|
169
|
-
|
|
170
|
-
let dir: string | undefined;
|
|
171
|
-
let linked = false;
|
|
172
|
-
|
|
173
|
-
const checkout = links[entry.repo]?.path;
|
|
174
|
-
if (checkout !== undefined) {
|
|
175
|
-
if (!linkedRecipes.has(entry.repo)) {
|
|
176
|
-
linkedRecipes.set(entry.repo, mapLinkedRecipes(checkout));
|
|
177
|
-
}
|
|
178
|
-
dir = linkedRecipes.get(entry.repo)![key];
|
|
179
|
-
linked = dir !== undefined;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (dir === undefined) {
|
|
183
|
-
// The store is machine-wide, so it files an entry under the repository's
|
|
184
|
-
// canonical identity rather than under this project's short name for it.
|
|
185
|
-
const identity = lock.repos[entry.repo]?.identity;
|
|
186
|
-
dir =
|
|
187
|
-
identity === undefined
|
|
188
|
-
? undefined
|
|
189
|
-
: path.join(storeRoot, ...identitySegments(identity), namespace, name, entry.version);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
located.push({
|
|
193
|
-
key,
|
|
194
|
-
repo: entry.repo,
|
|
195
|
-
namespace,
|
|
196
|
-
name,
|
|
197
|
-
version: entry.version,
|
|
198
|
-
hash: entry.hash,
|
|
199
|
-
kind: entry.kind,
|
|
200
|
-
requestedBy: [...entry.requestedBy],
|
|
201
|
-
dir: dir ?? "",
|
|
202
|
-
linked,
|
|
203
|
-
present: dir !== undefined && fs.existsSync(dir),
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return located;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* The lockfile keys one subscription holds DIRECTLY. A recipe ref holds its own
|
|
212
|
-
* key; a namespace ref holds every recipe in that namespace.
|
|
213
|
-
*
|
|
214
|
-
* Only entries the project itself holds count. A recipe that arrived purely as
|
|
215
|
-
* another recipe's `depends` sits under the same namespace but was never the
|
|
216
|
-
* project's to hold, and counting it made `sous unsubscribe <namespace>` report
|
|
217
|
-
* it as having "stayed" when the project had never held it in the first place.
|
|
218
|
-
*
|
|
219
|
-
* @param lock - The lockfile as it stands.
|
|
220
|
-
* @param key - The subscription's ref key: a namespace, or `namespace/recipe`.
|
|
221
|
-
*/
|
|
222
|
-
export function keysHeldBySubscription(lock: Lockfile, key: string): string[] {
|
|
223
|
-
const heldByProject = (entry: string): boolean =>
|
|
224
|
-
lock.recipes[entry]?.requestedBy.includes(PROJECT_HOLDER) === true;
|
|
225
|
-
|
|
226
|
-
if (key.includes("/")) {
|
|
227
|
-
return heldByProject(key) ? [key] : [];
|
|
228
|
-
}
|
|
229
|
-
return Object.keys(lock.recipes)
|
|
230
|
-
.filter((entry) => entry.startsWith(`${key}/`) && heldByProject(entry))
|
|
231
|
-
.sort();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Every locked recipe reachable from a set of keys through the lockfile's
|
|
236
|
-
* holder lists: the keys themselves, whatever they hold, and so on.
|
|
237
|
-
*
|
|
238
|
-
* @param lock - The lockfile.
|
|
239
|
-
* @param roots - The keys to start from.
|
|
240
|
-
*/
|
|
241
|
-
export function lockedClosure(lock: Lockfile, roots: string[]): string[] {
|
|
242
|
-
const reached = new Set(roots.filter((key) => Object.hasOwn(lock.recipes, key)));
|
|
243
|
-
const queue = [...reached];
|
|
244
|
-
while (queue.length > 0) {
|
|
245
|
-
const holder = queue.shift()!;
|
|
246
|
-
for (const [key, entry] of Object.entries(lock.recipes)) {
|
|
247
|
-
if (reached.has(key) || !entry.requestedBy.includes(holder)) continue;
|
|
248
|
-
reached.add(key);
|
|
249
|
-
queue.push(key);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return [...reached].sort();
|
|
253
|
-
}
|
|
254
|
-
|