@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
|
@@ -85,8 +85,8 @@ export default class NamespaceShow extends BaseCommand {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
const detail = flags.installed
|
|
88
|
-
? describeInstalled(inputs, args.ref, describeNamespace, "namespace")
|
|
89
|
-
: describeNamespace(inputs, args.ref);
|
|
88
|
+
? await describeInstalled(inputs, args.ref, describeNamespace, "namespace")
|
|
89
|
+
: await describeNamespace(inputs, args.ref);
|
|
90
90
|
|
|
91
91
|
heading(`The namespace ${detail.namespace}`);
|
|
92
92
|
blankLine();
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type RecipeVersionListing,
|
|
35
35
|
} from "../../lib/repos/catalog.js";
|
|
36
36
|
import { PROJECT_REQUESTER } from "../../lib/repos/resolver.js";
|
|
37
|
+
import { recipeRef } from "../../services/ref-resolver/index.js";
|
|
37
38
|
import { describeDeclaration, describeDependencyKind } from "../../lib/repos/declarations.js";
|
|
38
39
|
import { formatQuestionPlan } from "../../lib/vars/question-plan.js";
|
|
39
40
|
import { describeError } from "../../lib/repos/release/validate.js";
|
|
@@ -156,8 +157,8 @@ export default class RecipeShow extends BaseCommand {
|
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
const detail = flags.installed
|
|
159
|
-
? describeInstalled(inputs, args.ref, describeRecipe, "recipe")
|
|
160
|
-
: describeRecipe(inputs, args.ref);
|
|
160
|
+
? await describeInstalled(inputs, args.ref, describeRecipe, "recipe")
|
|
161
|
+
: await describeRecipe(inputs, args.ref);
|
|
161
162
|
|
|
162
163
|
heading(detail.key);
|
|
163
164
|
blankLine();
|
|
@@ -236,12 +237,10 @@ export default class RecipeShow extends BaseCommand {
|
|
|
236
237
|
}
|
|
237
238
|
try {
|
|
238
239
|
return await service.previewSubscription(
|
|
239
|
-
{
|
|
240
|
+
recipeRef(detail.namespace, detail.name, {
|
|
240
241
|
repo: detail.repo,
|
|
241
|
-
namespace: detail.namespace,
|
|
242
|
-
recipe: detail.name,
|
|
243
242
|
range: detail.describing,
|
|
244
|
-
},
|
|
243
|
+
}),
|
|
245
244
|
{
|
|
246
245
|
indexes: new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
|
|
247
246
|
...(semver.prerelease(detail.describing) === null ? {} : { prerelease: true }),
|
|
@@ -409,9 +408,7 @@ export default class RecipeShow extends BaseCommand {
|
|
|
409
408
|
destination:
|
|
410
409
|
entry.destinations.length > 0
|
|
411
410
|
? entry.destinations.join(", ")
|
|
412
|
-
: entry.kind
|
|
413
|
-
? "loaded as a config layer, so nothing is written"
|
|
414
|
-
: `nowhere: this project sets no 'recipeOutputs.${entry.kind}' directory`,
|
|
411
|
+
: noDestinationText(entry.kind),
|
|
415
412
|
}));
|
|
416
413
|
|
|
417
414
|
for (const line of renderTable(CONTENT_COLUMNS, rows, { indent: INDENT })) {
|
|
@@ -420,3 +417,23 @@ export default class RecipeShow extends BaseCommand {
|
|
|
420
417
|
}
|
|
421
418
|
}
|
|
422
419
|
|
|
420
|
+
/**
|
|
421
|
+
* What to say where a content kind has no destination directory: each kind
|
|
422
|
+
* reaches the project in its own way.
|
|
423
|
+
*
|
|
424
|
+
* noDestinationText("memories"); // -> "included in an agent's instructions by an include line, so nothing is written"
|
|
425
|
+
*
|
|
426
|
+
* @param kind - The content kind.
|
|
427
|
+
*/
|
|
428
|
+
function noDestinationText(kind: string): string {
|
|
429
|
+
switch (kind) {
|
|
430
|
+
case "config":
|
|
431
|
+
return "loaded as a config layer, so nothing is written";
|
|
432
|
+
case "memories":
|
|
433
|
+
return "reaches an agent through an include line in its instructions, so nothing is written";
|
|
434
|
+
case "skills":
|
|
435
|
+
return "nowhere: this project sets no 'recipes.skills.outputs' directory";
|
|
436
|
+
default:
|
|
437
|
+
return `nowhere: this project has no destination for ${kind} yet`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -3,11 +3,14 @@ import { BaseCommand } from "../../base-command.js";
|
|
|
3
3
|
import { ConfigError } from "../../lib/errors.js";
|
|
4
4
|
import { nonInteractiveError } from "../../lib/interactive.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
CatalogLookup,
|
|
7
|
+
RefPickArguments,
|
|
8
|
+
RefResolveArguments,
|
|
9
|
+
repoOf,
|
|
10
|
+
sharedRefPicker,
|
|
11
|
+
sharedRefResolver,
|
|
12
|
+
type CatalogRepo,
|
|
13
|
+
} from "../../services/ref-resolver/index.js";
|
|
11
14
|
import {
|
|
12
15
|
assessPendingWork,
|
|
13
16
|
assessProposal,
|
|
@@ -373,27 +376,33 @@ export default class RepoContribute extends BaseCommand {
|
|
|
373
376
|
shellEnv: this.shellEnv,
|
|
374
377
|
});
|
|
375
378
|
|
|
376
|
-
const repos:
|
|
379
|
+
const repos: CatalogRepo[] = service.cachedReferenceRepos();
|
|
377
380
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
381
|
+
const { refs } = await sharedRefResolver().resolve(
|
|
382
|
+
new RefResolveArguments({
|
|
383
|
+
input: ref,
|
|
384
|
+
lookup: new CatalogLookup(repos),
|
|
385
|
+
kinds: ["repo", "namespace", "recipe"],
|
|
386
|
+
refusedIsEmpty: true,
|
|
387
|
+
})
|
|
382
388
|
);
|
|
383
389
|
|
|
384
|
-
const chosen = await
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
390
|
+
const chosen = await sharedRefPicker().pick(
|
|
391
|
+
refs,
|
|
392
|
+
new RefPickArguments({
|
|
393
|
+
search: ref,
|
|
394
|
+
interactive: this.interactive,
|
|
395
|
+
acceptFirst,
|
|
396
|
+
prompt: `Which '${ref}' do you want to contribute to?`,
|
|
397
|
+
details: [
|
|
398
|
+
" No trusted repository, namespace or recipe has that name.",
|
|
399
|
+
repos.length === 0
|
|
400
|
+
? " This project trusts no repositories."
|
|
401
|
+
: ` This project trusts: ${repos.map((entry) => entry.name).join(", ")}.`,
|
|
402
|
+
],
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
return chosen.kind === "repo" ? chosen.name! : repoOf(chosen)!.name!;
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
// --- The link ---------------------------------------------------------------------------------
|
package/src/commands/repo/gc.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js"
|
|
|
17
17
|
import { resolveStoreSettings } from "../../lib/repos/store/settings.js";
|
|
18
18
|
import type { StoreKey } from "../../lib/repos/store/contract.js";
|
|
19
19
|
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
20
|
-
import { splitRecipeKey } from "../../
|
|
20
|
+
import { splitRecipeKey } from "../../services/ref-resolver/index.js";
|
|
21
21
|
import {
|
|
22
22
|
blankLine,
|
|
23
23
|
dryRunNotice,
|
|
@@ -105,7 +105,7 @@ export default class RepoRemove extends BaseCommand {
|
|
|
105
105
|
yes: flags.yes,
|
|
106
106
|
dryRun,
|
|
107
107
|
// The output files a removed recipe wrote can only be named once the
|
|
108
|
-
// `
|
|
108
|
+
// `recipes.skills.outputs` destinations have had their variables substituted.
|
|
109
109
|
scope: resolveRootScope(this.settings, this.configContext),
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../../lib/repos/catalog-display.js";
|
|
22
22
|
import { readEffectiveLinks } from "../../lib/repos/links.js";
|
|
23
23
|
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
24
|
-
import { namespaceOfKey } from "../../
|
|
24
|
+
import { namespaceOfKey } from "../../services/ref-resolver/index.js";
|
|
25
25
|
import { browsingFlags } from "../../utils/flags.js";
|
|
26
26
|
import {
|
|
27
27
|
blankLine,
|
package/src/commands/vars/ask.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* answers in `.sous/.env` or `.sous/.env.local`. By default it asks only what
|
|
6
6
|
* is unanswered or no longer fits; `--all` asks everything again.
|
|
7
7
|
*
|
|
8
|
-
* The optional name is a reference, resolved through `src/
|
|
8
|
+
* The optional name is a reference, resolved through `src/services/ref-resolver/` exactly as
|
|
9
9
|
* every other command resolves one. It may name a variable, an environment
|
|
10
10
|
* variable that answers one, a recipe, a namespace or a repository, at any
|
|
11
11
|
* level of qualification; naming anything larger than a variable asks every
|
|
@@ -38,18 +38,22 @@ import { Args, Flags } from "@oclif/core";
|
|
|
38
38
|
import { BaseCommand } from "../../base-command.js";
|
|
39
39
|
import { ConfigError } from "../../lib/errors.js";
|
|
40
40
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
ChainedLookup,
|
|
42
|
+
RefPickArguments,
|
|
43
|
+
RefResolveArguments,
|
|
44
|
+
VariableLookup,
|
|
45
|
+
namespaceOf,
|
|
46
|
+
recipeOf,
|
|
47
|
+
refKey,
|
|
48
|
+
repoOf,
|
|
49
|
+
sharedRefPicker,
|
|
50
|
+
sharedRefResolver,
|
|
51
|
+
variableRefOf,
|
|
52
|
+
type CatalogRepo,
|
|
53
|
+
type RefKind,
|
|
54
|
+
type SousRef,
|
|
55
|
+
} from "../../services/ref-resolver/index.js";
|
|
56
|
+
import { EnvVarLookup } from "../../services/ref-resolver/lookups/env-var-lookup.js";
|
|
53
57
|
import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
|
|
54
58
|
import {
|
|
55
59
|
applyProvidedAnswers,
|
|
@@ -269,10 +273,10 @@ export default class VarsAsk extends BaseCommand {
|
|
|
269
273
|
defined: DefinedVariable[],
|
|
270
274
|
ladder: LadderContext,
|
|
271
275
|
selection: Selection,
|
|
272
|
-
trusted:
|
|
276
|
+
trusted: CatalogRepo[] = []
|
|
273
277
|
): Promise<string[] | undefined> {
|
|
274
|
-
const
|
|
275
|
-
|
|
278
|
+
const lookupOf = (pool: DefinedVariable[]) =>
|
|
279
|
+
new ChainedLookup(new VariableLookup(pool, trusted), new EnvVarLookup(pool, ladder));
|
|
276
280
|
const { name, repo, namespace, vars } = selection;
|
|
277
281
|
if (name === undefined && repo === undefined && namespace === undefined && vars === undefined) {
|
|
278
282
|
return undefined;
|
|
@@ -281,36 +285,24 @@ export default class VarsAsk extends BaseCommand {
|
|
|
281
285
|
let pool = defined;
|
|
282
286
|
|
|
283
287
|
if (repo !== undefined) {
|
|
284
|
-
const match = await this.pick(
|
|
285
|
-
pool = pool.filter((entry) => entry.recipe.repo === match
|
|
288
|
+
const match = await this.pick(repo, lookupOf(pool), ["repo"], selection);
|
|
289
|
+
pool = pool.filter((entry) => entry.recipe.repo === repoOf(match)?.name);
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
if (namespace !== undefined) {
|
|
289
|
-
const match = await this.pick(
|
|
290
|
-
findNamespace(namespace, contextOf(pool)),
|
|
291
|
-
namespace,
|
|
292
|
-
selection
|
|
293
|
-
);
|
|
293
|
+
const match = await this.pick(namespace, lookupOf(pool), ["namespace"], selection);
|
|
294
294
|
pool = narrowTo(pool, match);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
if (name !== undefined) {
|
|
298
|
-
const match = await this.pick(
|
|
299
|
-
findReference(name, ALL_SCOPES, contextOf(pool)),
|
|
300
|
-
name,
|
|
301
|
-
selection
|
|
302
|
-
);
|
|
298
|
+
const match = await this.pick(name, lookupOf(pool), undefined, selection);
|
|
303
299
|
pool = narrowTo(pool, match);
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
if (vars !== undefined) {
|
|
307
303
|
const chosen: DefinedVariable[] = [];
|
|
308
304
|
for (const wanted of vars) {
|
|
309
|
-
const match = await this.pick(
|
|
310
|
-
findVariable(wanted, contextOf(pool)),
|
|
311
|
-
wanted,
|
|
312
|
-
selection
|
|
313
|
-
);
|
|
305
|
+
const match = await this.pick(wanted, lookupOf(pool), ["variable", "envVar"], selection);
|
|
314
306
|
chosen.push(...narrowTo(pool, match));
|
|
315
307
|
}
|
|
316
308
|
pool = chosen;
|
|
@@ -323,7 +315,7 @@ export default class VarsAsk extends BaseCommand {
|
|
|
323
315
|
);
|
|
324
316
|
}
|
|
325
317
|
|
|
326
|
-
return [...new Set(pool.map(
|
|
318
|
+
return [...new Set(pool.map((entry) => refKey(variableRefOf(entry))))];
|
|
327
319
|
}
|
|
328
320
|
|
|
329
321
|
/**
|
|
@@ -331,7 +323,7 @@ export default class VarsAsk extends BaseCommand {
|
|
|
331
323
|
* indexes alone. A project whose repositories cannot be read this way still
|
|
332
324
|
* answers every other form of reference, so a failure here is an empty list.
|
|
333
325
|
*/
|
|
334
|
-
private trustedRepos():
|
|
326
|
+
private trustedRepos(): CatalogRepo[] {
|
|
335
327
|
try {
|
|
336
328
|
return subscriptionServiceFor({
|
|
337
329
|
configContext: this.configContext,
|
|
@@ -347,38 +339,67 @@ export default class VarsAsk extends BaseCommand {
|
|
|
347
339
|
* Settles which of the things a reference could have meant this run proceeds
|
|
348
340
|
* with, through the rule every command shares.
|
|
349
341
|
*
|
|
350
|
-
* @param matches - What the reference could have meant, in listing order.
|
|
351
342
|
* @param search - The reference exactly as it was written.
|
|
343
|
+
* @param lookup - What the reference is searched in.
|
|
344
|
+
* @param kinds - The kinds of ref this step accepts; every kind when left out.
|
|
352
345
|
* @param selection - The selection, read for `--accept-first`.
|
|
353
346
|
*/
|
|
354
347
|
private async pick(
|
|
355
|
-
matches: ReferenceMatch[],
|
|
356
348
|
search: string,
|
|
349
|
+
lookup: ChainedLookup,
|
|
350
|
+
kinds: readonly RefKind[] | undefined,
|
|
357
351
|
selection: Selection
|
|
358
|
-
): Promise<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
352
|
+
): Promise<SousRef> {
|
|
353
|
+
const { refs } = await sharedRefResolver().resolve(
|
|
354
|
+
new RefResolveArguments({
|
|
355
|
+
input: search,
|
|
356
|
+
lookup,
|
|
357
|
+
refusedIsEmpty: true,
|
|
358
|
+
...(kinds === undefined ? {} : { kinds }),
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
// A name that answers several variables is one candidate per variable, so
|
|
362
|
+
// choosing between them is the same question as choosing between variables.
|
|
363
|
+
const candidates = refs.flatMap((ref) =>
|
|
364
|
+
ref.kind === "envVar" && (ref.variables?.length ?? 0) > 1
|
|
365
|
+
? ref.variables!.map((variable): SousRef => ({ ...ref, variables: [variable] }))
|
|
366
|
+
: [ref]
|
|
367
|
+
);
|
|
368
|
+
return sharedRefPicker().pick(
|
|
369
|
+
candidates,
|
|
370
|
+
new RefPickArguments({
|
|
371
|
+
search,
|
|
372
|
+
interactive: this.interactive,
|
|
373
|
+
acceptFirst: selection.acceptFirst,
|
|
374
|
+
announce: false,
|
|
375
|
+
details: [SEE_ALL],
|
|
376
|
+
})
|
|
377
|
+
);
|
|
366
378
|
}
|
|
367
379
|
}
|
|
368
380
|
|
|
369
|
-
/**
|
|
370
|
-
|
|
381
|
+
/**
|
|
382
|
+
* The variables one ref covers: a whole repository, namespace, recipe, or one
|
|
383
|
+
* variable, or the variable an environment variable name answers.
|
|
384
|
+
*/
|
|
385
|
+
function narrowTo(pool: DefinedVariable[], match: SousRef): DefinedVariable[] {
|
|
386
|
+
const target = match.kind === "envVar" ? match.variables?.[0] : match;
|
|
387
|
+
if (target === undefined) return [];
|
|
388
|
+
const repo = repoOf(target)?.name;
|
|
389
|
+
const namespace = namespaceOf(target)?.name;
|
|
390
|
+
const recipe = recipeOf(target)?.name;
|
|
391
|
+
|
|
371
392
|
return pool.filter((entry) => {
|
|
372
|
-
if (
|
|
373
|
-
if (
|
|
393
|
+
if (repo !== undefined && entry.recipe.repo !== repo) return false;
|
|
394
|
+
if (target.kind === "repo") return true;
|
|
374
395
|
|
|
375
|
-
if (
|
|
376
|
-
if (
|
|
396
|
+
if (namespace !== undefined && entry.recipe.namespace !== namespace) return false;
|
|
397
|
+
if (target.kind === "namespace") return true;
|
|
377
398
|
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
399
|
+
if (recipe !== undefined && entry.recipe.name !== recipe) return false;
|
|
400
|
+
if (target.kind === "recipe") return true;
|
|
380
401
|
|
|
381
|
-
return entry.definition.name ===
|
|
402
|
+
return target.kind === "variable" && entry.definition.name === target.name;
|
|
382
403
|
});
|
|
383
404
|
}
|
|
384
405
|
|
package/src/lib/build-service.ts
CHANGED
|
@@ -2,11 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import type { ConfigContext, Settings } from "./settings.js";
|
|
4
4
|
import { resolveAliases, resolveCompilation, resolveRootScope } from "./settings.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveIncludeFiles, type ViewMap } from "./include-resolver.js";
|
|
6
6
|
import type { NamespaceResolver } from "./repos/namespace-resolver.js";
|
|
7
7
|
import { createProjectNamespaceResolver } from "./repos/locked-namespace-resolver.js";
|
|
8
8
|
import { buildRecipeTargets, type RecipeTargets } from "./repos/recipe-targets.js";
|
|
9
|
-
import { CompilationService, resolveOutputPath } from "./markdown-compiler.js";
|
|
9
|
+
import { CompilationService, INCLUDE_LINE_PATTERN, resolveOutputPath } from "./markdown-compiler.js";
|
|
10
10
|
import type { CompilationConfig, CompilationTarget } from "./markdown-compiler.js";
|
|
11
11
|
import { StateService } from "./state.js";
|
|
12
12
|
import { isProtectedPath } from "./state.js";
|
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
unansweredWarning,
|
|
20
20
|
type RecipeAnswers,
|
|
21
21
|
} from "./vars/answers.js";
|
|
22
|
-
import { footer, heading, log, warning } from "../utils/formatting.js";
|
|
22
|
+
import { BULLET, footer, heading, log, warning } from "../utils/formatting.js";
|
|
23
|
+
import { sharedHashNames } from "../services/ref-resolver/index.js";
|
|
24
|
+
import { listMemories, type MemoryFile } from "./repos/recipe-memories.js";
|
|
23
25
|
|
|
24
26
|
export type BuildOptions = {
|
|
25
27
|
strict?: boolean;
|
|
@@ -146,6 +148,54 @@ export function withRecipeTargets(
|
|
|
146
148
|
return { ...config, targets: [...config.targets, ...recipes.targets] };
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The memories an active recipe publishes that no output of the build included
|
|
153
|
+
* by any route (an exact path, a glob or the `#memories` view). Recipes under
|
|
154
|
+
* `recipes.memories.exclude` are never listed.
|
|
155
|
+
*
|
|
156
|
+
* @param settings - The merged project config, for `recipes.memories`.
|
|
157
|
+
* @param sousDir - The project's `.sous/` directory.
|
|
158
|
+
* @param included - Every file the compile read, as real paths.
|
|
159
|
+
*/
|
|
160
|
+
export function unincludedMemories(
|
|
161
|
+
settings: Settings,
|
|
162
|
+
sousDir: string,
|
|
163
|
+
included: ReadonlySet<string>
|
|
164
|
+
): MemoryFile[] {
|
|
165
|
+
const memories = settings.recipes?.memories;
|
|
166
|
+
return listMemories({ sousDir, first: memories?.first, exclude: memories?.exclude }).filter(
|
|
167
|
+
(memory) => !included.has(realPath(memory.file))
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The path with symbolic links resolved, or the path itself when it cannot be read. */
|
|
172
|
+
function realPath(filePath: string): string {
|
|
173
|
+
try {
|
|
174
|
+
return fs.realpathSync(filePath);
|
|
175
|
+
} catch {
|
|
176
|
+
return filePath;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The warning for memories no output included, or undefined when there are none.
|
|
182
|
+
*
|
|
183
|
+
* @param missing - What {@link unincludedMemories} found.
|
|
184
|
+
*/
|
|
185
|
+
export function unincludedMemoriesWarning(missing: readonly MemoryFile[]): string | undefined {
|
|
186
|
+
if (missing.length === 0) return undefined;
|
|
187
|
+
const noun = missing.length === 1 ? "memory" : "memories";
|
|
188
|
+
return (
|
|
189
|
+
`${missing.length} ${noun} published by your subscribed recipes ${
|
|
190
|
+
missing.length === 1 ? "is" : "are"
|
|
191
|
+
} not included in any output, so no agent will read ${missing.length === 1 ? "it" : "them"}:\n` +
|
|
192
|
+
missing.map((memory) => `${BULLET} ${memory.recipe}: ${memory.relative}`).join("\n") +
|
|
193
|
+
`\nTo include every memory, add the line "@#memories/**/*.md" to your project's instruction ` +
|
|
194
|
+
`source (the file your AGENTS.md or CLAUDE.md is built from). To leave a recipe's memories out on purpose, ` +
|
|
195
|
+
`list the recipe in "recipes.memories.exclude" in your sous config.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
149
199
|
/**
|
|
150
200
|
* The directories a build's deletions must never reach into, for the project the
|
|
151
201
|
* options describe. Empty when the caller gave no config context, which is the
|
|
@@ -193,6 +243,7 @@ function collectIncludeGraph(
|
|
|
193
243
|
aliases?: Record<string, string[]>;
|
|
194
244
|
scope?: Record<string, string>;
|
|
195
245
|
namespaceResolver?: NamespaceResolver;
|
|
246
|
+
views?: ViewMap;
|
|
196
247
|
} = {},
|
|
197
248
|
visited: Set<string> = new Set()
|
|
198
249
|
): Set<string> {
|
|
@@ -208,21 +259,23 @@ function collectIncludeGraph(
|
|
|
208
259
|
return visited;
|
|
209
260
|
}
|
|
210
261
|
|
|
211
|
-
const includePattern = /^@([~a-zA-Z0-9_${}][a-zA-Z0-9_\-/.:${}]*\.md)$/gm;
|
|
212
262
|
const baseDir = path.dirname(filePath);
|
|
213
|
-
let match: RegExpExecArray | null;
|
|
214
263
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
264
|
+
for (const line of content.split("\n")) {
|
|
265
|
+
const match = INCLUDE_LINE_PATTERN.exec(line.trimEnd());
|
|
266
|
+
if (match === null) continue;
|
|
267
|
+
const { files, candidates } = resolveIncludeFiles(match[1].trim(), {
|
|
218
268
|
aliases: resolveOpts.aliases,
|
|
269
|
+
views: resolveOpts.views,
|
|
219
270
|
scope: resolveOpts.scope,
|
|
220
271
|
baseDir,
|
|
221
272
|
namespaceResolver: resolveOpts.namespaceResolver,
|
|
222
273
|
fromFile: filePath,
|
|
223
274
|
});
|
|
224
|
-
|
|
225
|
-
|
|
275
|
+
// A file that does not exist yet is still part of the graph, as its first candidate.
|
|
276
|
+
for (const fullPath of files.length > 0 ? files : candidates.slice(0, 1)) {
|
|
277
|
+
collectIncludeGraph(fullPath, resolveOpts, visited);
|
|
278
|
+
}
|
|
226
279
|
}
|
|
227
280
|
|
|
228
281
|
return visited;
|
|
@@ -248,6 +301,7 @@ export function findAffectedTargets(
|
|
|
248
301
|
return config.targets.filter(target => {
|
|
249
302
|
const graph = collectIncludeGraph(target.rootInputPath, {
|
|
250
303
|
aliases: config.aliases,
|
|
304
|
+
views: config.views,
|
|
251
305
|
scope: config.includeScope,
|
|
252
306
|
namespaceResolver,
|
|
253
307
|
});
|
|
@@ -313,12 +367,23 @@ export class BuildService {
|
|
|
313
367
|
const recipes = resolveRecipeTargets(settings, rootScope, options.configContext, answers);
|
|
314
368
|
for (const notice of recipes.warnings) warning(notice);
|
|
315
369
|
|
|
316
|
-
const
|
|
370
|
+
const withoutViews = withRecipeTargets(
|
|
317
371
|
resolveCompilation(settings, rootScope),
|
|
318
372
|
recipes,
|
|
319
373
|
settings,
|
|
320
374
|
rootScope
|
|
321
375
|
);
|
|
376
|
+
// The files each view lists (`#memories`), worked out once per build.
|
|
377
|
+
const config: CompilationConfig | null =
|
|
378
|
+
withoutViews === null || options.configContext === undefined
|
|
379
|
+
? withoutViews
|
|
380
|
+
: {
|
|
381
|
+
...withoutViews,
|
|
382
|
+
views: sharedHashNames().viewMap({
|
|
383
|
+
sousDir: options.configContext.sousDir,
|
|
384
|
+
settings,
|
|
385
|
+
}),
|
|
386
|
+
};
|
|
322
387
|
|
|
323
388
|
if (config) {
|
|
324
389
|
let effectiveConfig: CompilationConfig = config;
|
|
@@ -351,6 +416,15 @@ export class BuildService {
|
|
|
351
416
|
});
|
|
352
417
|
const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
|
|
353
418
|
if (!compileOk) success = false;
|
|
419
|
+
|
|
420
|
+
// Only a full build knows every route an include can take, so only a
|
|
421
|
+
// full build that compiled cleanly checks that every memory got in.
|
|
422
|
+
if (compileOk && options.configContext !== undefined) {
|
|
423
|
+
const notice = unincludedMemoriesWarning(
|
|
424
|
+
unincludedMemories(settings, options.configContext.sousDir, compiler.includedFiles())
|
|
425
|
+
);
|
|
426
|
+
if (notice !== undefined) warning(notice);
|
|
427
|
+
}
|
|
354
428
|
}
|
|
355
429
|
}
|
|
356
430
|
}
|
package/src/lib/config-schema.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { ConfigError } from "./errors.js";
|
|
22
|
+
import { compileRecipeKeyMatcher } from "./repos/recipe-key-matcher.js";
|
|
22
23
|
import { repoUrlSchema, semverRangeSchema } from "./repos/formats/common.js";
|
|
23
24
|
import { REF_KEY_PATTERN, REPO_NAME_PATTERN } from "./repos/formats/patterns.js";
|
|
24
25
|
import type { Settings } from "./settings.js";
|
|
@@ -174,27 +175,66 @@ const storeSchema = z
|
|
|
174
175
|
.strict();
|
|
175
176
|
|
|
176
177
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
178
|
+
* One list of recipe keys: each entry is a glob over `namespace/recipe` keys, or
|
|
179
|
+
* a regular expression written between two slashes (`/^tool-usage\//`). A
|
|
180
|
+
* regular expression that does not compile is an error naming the key and the
|
|
181
|
+
* value.
|
|
182
|
+
*/
|
|
183
|
+
function recipeKeyListSchema(where: string) {
|
|
184
|
+
return z.array(z.string()).superRefine((entries, context) => {
|
|
185
|
+
entries.forEach((entry, index) => {
|
|
186
|
+
try {
|
|
187
|
+
compileRecipeKeyMatcher([entry], `${where}[${index}]`);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
context.addIssue({
|
|
190
|
+
code: "custom",
|
|
191
|
+
path: [index],
|
|
192
|
+
message: error instanceof Error ? error.message : String(error),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What a project does with the content kinds the subscribed recipes publish, one
|
|
201
|
+
* block per kind.
|
|
182
202
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
203
|
+
* `skills.outputs` lists the destination directories recipe skill bundles are
|
|
204
|
+
* written into. Each is `${var}` substituted like any other config path, and
|
|
205
|
+
* several may be named so the same recipe feeds more than one agent directory
|
|
206
|
+
* (`.claude/skills` and `.codex/skills`, say). The default is
|
|
207
|
+
* `<project root>/.claude/skills`, the project root being the parent of the
|
|
208
|
+
* discovered `.sous/` directory.
|
|
209
|
+
*
|
|
210
|
+
* `memories.first` and `memories.exclude` shape which recipe memories an
|
|
211
|
+
* `@#memories` include pulls in and in what order: recipes matching `first` lead,
|
|
212
|
+
* and recipes matching `exclude` are left out. Each entry is a glob over
|
|
213
|
+
* `namespace/recipe` keys, or a regular expression written between two slashes.
|
|
214
|
+
*
|
|
215
|
+
* A recipe's `config` contents are not listed here; they are loaded as config
|
|
216
|
+
* layers rather than written anywhere.
|
|
189
217
|
*/
|
|
190
|
-
const
|
|
218
|
+
const recipesSchema = z
|
|
191
219
|
.object({
|
|
192
220
|
/** Where recipe skill bundles are written. */
|
|
193
|
-
skills: z
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
skills: z
|
|
222
|
+
.object({
|
|
223
|
+
/** Destination directories, `${var}` substituted. */
|
|
224
|
+
outputs: z.array(z.string()).optional(),
|
|
225
|
+
})
|
|
226
|
+
.strict()
|
|
227
|
+
.optional(),
|
|
228
|
+
/** Which recipe memories an `@#memories` include pulls in, and in what order. */
|
|
229
|
+
memories: z
|
|
230
|
+
.object({
|
|
231
|
+
/** Recipes whose memories come first. */
|
|
232
|
+
first: recipeKeyListSchema("recipes.memories.first").optional(),
|
|
233
|
+
/** Recipes whose memories are left out. */
|
|
234
|
+
exclude: recipeKeyListSchema("recipes.memories.exclude").optional(),
|
|
235
|
+
})
|
|
236
|
+
.strict()
|
|
237
|
+
.optional(),
|
|
198
238
|
})
|
|
199
239
|
.strict();
|
|
200
240
|
|
|
@@ -267,8 +307,8 @@ export const settingsSchema = z
|
|
|
267
307
|
.optional(),
|
|
268
308
|
/** Knobs for the machine-wide recipe store. */
|
|
269
309
|
store: storeSchema.optional(),
|
|
270
|
-
/**
|
|
271
|
-
|
|
310
|
+
/** What the project does with each content kind its subscribed recipes publish. */
|
|
311
|
+
recipes: recipesSchema.optional(),
|
|
272
312
|
/**
|
|
273
313
|
* Variable mapping records, keyed by environment variable name. Each entry
|
|
274
314
|
* binds that name to one recipe variable, which is how an answer is stored
|