@sous-io/sous 0.2.32 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/docs/markdown/_sidebar.md +1 -0
- package/docs/markdown/commands.md +8 -4
- package/docs/markdown/configuration.md +28 -4
- package/docs/markdown/repositories-authoring.md +22 -2
- package/docs/markdown/repositories-consuming.md +40 -11
- package/docs/markdown/repositories-file-formats.md +28 -13
- package/docs/markdown/repositories-quickstart.md +1 -1
- package/docs/markdown/repositories-variables.md +16 -0
- package/docs/markdown/repositories.md +8 -4
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +27 -15
- package/src/commands/lock/rebuild.ts +3 -6
- package/src/commands/namespace/show.ts +2 -2
- package/src/commands/recipe/show.ts +26 -9
- package/src/commands/repo/contribute.ts +32 -23
- package/src/commands/repo/gc.ts +1 -1
- package/src/commands/repo/remove.ts +1 -1
- package/src/commands/repo/search.ts +1 -1
- package/src/commands/vars/ask.ts +75 -54
- package/src/lib/build-service.ts +93 -15
- package/src/lib/config-schema.ts +59 -19
- package/src/lib/include-resolver.ts +307 -69
- package/src/lib/markdown-compiler.ts +289 -82
- package/src/lib/project-scaffold/templates.ts +19 -11
- package/src/lib/repos/catalog-inputs.ts +1 -1
- package/src/lib/repos/catalog.ts +98 -72
- package/src/lib/repos/declarations.ts +22 -12
- package/src/lib/repos/formats/index-file.ts +1 -1
- package/src/lib/repos/formats/recipe-manifest.ts +3 -4
- package/src/lib/repos/lock-service.ts +1 -1
- package/src/lib/repos/locked-namespace-resolver.ts +14 -6
- package/src/lib/repos/locked-recipe-lookup.ts +123 -0
- package/src/lib/repos/locked-recipes.ts +1 -1
- package/src/lib/repos/memories-hash-name.ts +33 -0
- package/src/lib/repos/namespace-resolver.ts +198 -119
- package/src/lib/repos/recipe-config-layers.ts +29 -1
- package/src/lib/repos/recipe-key-matcher.ts +62 -0
- package/src/lib/repos/recipe-memories.ts +175 -0
- package/src/lib/repos/recipe-targets.ts +32 -28
- package/src/lib/repos/ref-search.ts +1 -1
- package/src/lib/repos/release/index-builder.ts +23 -15
- package/src/lib/repos/release/plan.ts +14 -7
- package/src/lib/repos/release/settle.ts +38 -121
- package/src/lib/repos/release/submit-checkout.ts +42 -23
- package/src/lib/repos/release/tags.ts +1 -1
- package/src/lib/repos/resolver.ts +90 -72
- package/src/lib/repos/subscription-service.ts +192 -170
- package/src/lib/settings.ts +37 -33
- package/src/lib/vars/answers.ts +9 -1
- package/src/lib/vars/ask.ts +3 -3
- package/src/lib/vars/display.ts +1 -3
- package/src/lib/vars/index.ts +1 -0
- package/src/lib/vars/mappings.ts +17 -9
- package/src/lib/vars/secrets.ts +131 -0
- package/src/services/ref-resolver/container.ts +107 -0
- package/src/services/ref-resolver/dependency.ts +60 -0
- package/src/services/ref-resolver/format.ts +197 -0
- package/src/services/ref-resolver/glob.ts +115 -0
- package/src/services/ref-resolver/hash-names.ts +155 -0
- package/src/services/ref-resolver/index.ts +55 -0
- package/src/services/ref-resolver/injectable.ts +36 -0
- package/src/services/ref-resolver/location.ts +62 -0
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
- package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
- package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
- package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
- package/src/services/ref-resolver/named-refs.ts +144 -0
- package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
- package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
- package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
- package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
- package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
- package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
- package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
- package/src/services/ref-resolver/parts.ts +115 -0
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
- package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
- package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
- package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
- package/src/services/ref-resolver/ref-helpers.ts +62 -0
- package/src/services/ref-resolver/ref-picker-service.ts +204 -0
- package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
- package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
- package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
- package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
- package/src/services/ref-resolver/source.ts +48 -0
- package/src/services/ref-resolver/tokens.ts +25 -0
- package/src/services/ref-resolver/types.ts +155 -0
- package/src/templating/init-liquid-engine.ts +33 -18
- package/src/templating/lib/secret-scope.ts +41 -0
- package/src/templating/tags/exportScalarVarsJs.ts +5 -2
- package/src/templating/tags/showVars.ts +8 -2
- package/src/lib/refs/find.ts +0 -770
- package/src/lib/refs/index.ts +0 -14
- package/src/lib/refs/parse.ts +0 -844
- package/src/lib/refs/pick.ts +0 -147
- package/src/lib/refs/scopes.ts +0 -95
- package/src/lib/refs/settle.ts +0 -219
package/sous.config.schema.json
CHANGED
|
@@ -295,26 +295,38 @@
|
|
|
295
295
|
},
|
|
296
296
|
"additionalProperties": false
|
|
297
297
|
},
|
|
298
|
-
"
|
|
298
|
+
"recipes": {
|
|
299
299
|
"type": "object",
|
|
300
300
|
"properties": {
|
|
301
301
|
"skills": {
|
|
302
|
-
"type": "
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {
|
|
304
|
+
"outputs": {
|
|
305
|
+
"type": "array",
|
|
306
|
+
"items": {
|
|
307
|
+
"type": "string"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"additionalProperties": false
|
|
306
312
|
},
|
|
307
313
|
"memories": {
|
|
308
|
-
"type": "
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"
|
|
317
|
-
|
|
314
|
+
"type": "object",
|
|
315
|
+
"properties": {
|
|
316
|
+
"first": {
|
|
317
|
+
"type": "array",
|
|
318
|
+
"items": {
|
|
319
|
+
"type": "string"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"exclude": {
|
|
323
|
+
"type": "array",
|
|
324
|
+
"items": {
|
|
325
|
+
"type": "string"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
"additionalProperties": false
|
|
318
330
|
}
|
|
319
331
|
},
|
|
320
332
|
"additionalProperties": false
|
|
@@ -24,7 +24,7 @@ import { recipeFilesDirectory } from "../../lib/repos/catalog-inputs.js";
|
|
|
24
24
|
import { readRecipeManifestIn } from "../../lib/repos/locked-recipes.js";
|
|
25
25
|
import { createEmptyLockfile } from "../../lib/repos/formats/lockfile.js";
|
|
26
26
|
import type { LockRepoInput } from "../../lib/repos/lock-service.js";
|
|
27
|
-
import { RefSource,
|
|
27
|
+
import { RefSource, parseNamedRef } from "../../services/ref-resolver/index.js";
|
|
28
28
|
import {
|
|
29
29
|
PROJECT_REQUESTER,
|
|
30
30
|
resolveRefs,
|
|
@@ -133,12 +133,9 @@ export default class LockRebuild extends BaseCommand {
|
|
|
133
133
|
.sort()
|
|
134
134
|
.map((key) => {
|
|
135
135
|
const entry = subscriptions[key]!;
|
|
136
|
-
const parsed = parseShortRef(key, RefSource.Config);
|
|
137
136
|
return {
|
|
138
|
-
ref:
|
|
139
|
-
|
|
140
|
-
...(entry.range === undefined ? {} : { range: entry.range }),
|
|
141
|
-
},
|
|
137
|
+
ref: parseNamedRef(key, RefSource.Config),
|
|
138
|
+
...(entry.range === undefined ? {} : { range: entry.range }),
|
|
142
139
|
requestedBy: PROJECT_REQUESTER,
|
|
143
140
|
kind: "subscribes" as const,
|
|
144
141
|
...(entry.prerelease === true ? { prerelease: true } : {}),
|
|
@@ -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;
|
|
@@ -116,13 +118,15 @@ export function resolveRecipeTargets(
|
|
|
116
118
|
|
|
117
119
|
const missing = unansweredWarning(resolved, rootScope);
|
|
118
120
|
if (missing !== undefined) recipes.warnings.push(missing);
|
|
119
|
-
return recipes;
|
|
121
|
+
return { ...recipes, secretVariables: [...resolved.secrets] };
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
125
|
* Adds the recipe targets to a project's own compilation config. A project with
|
|
124
126
|
* no compilation block of its own still compiles its recipes, so the config is
|
|
125
|
-
* created when there is none and there is something to compile.
|
|
127
|
+
* created when there is none and there is something to compile. The names the
|
|
128
|
+
* recipes declare secret go onto the config either way, because a project's own
|
|
129
|
+
* template may dump the scope too.
|
|
126
130
|
*
|
|
127
131
|
* @param config - The project's own compilation config, or null when it has none.
|
|
128
132
|
* @param recipes - The targets the subscribed recipes contribute.
|
|
@@ -135,15 +139,65 @@ export function withRecipeTargets(
|
|
|
135
139
|
settings: Settings,
|
|
136
140
|
rootScope: Record<string, string>
|
|
137
141
|
): CompilationConfig | null {
|
|
138
|
-
|
|
142
|
+
const secrets = recipes.secretVariables?.length ? { secretVariables: recipes.secretVariables } : {};
|
|
143
|
+
if (recipes.targets.length === 0) return config === null ? null : { ...config, ...secrets };
|
|
139
144
|
if (config === null) {
|
|
140
145
|
return {
|
|
141
146
|
targets: recipes.targets,
|
|
142
147
|
aliases: resolveAliases(settings, rootScope),
|
|
143
148
|
includeScope: rootScope,
|
|
149
|
+
...secrets,
|
|
144
150
|
};
|
|
145
151
|
}
|
|
146
|
-
return { ...config, targets: [...config.targets, ...recipes.targets] };
|
|
152
|
+
return { ...config, targets: [...config.targets, ...recipes.targets], ...secrets };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The memories an active recipe publishes that no output of the build included
|
|
157
|
+
* by any route (an exact path, a glob or the `#memories` view). Recipes under
|
|
158
|
+
* `recipes.memories.exclude` are never listed.
|
|
159
|
+
*
|
|
160
|
+
* @param settings - The merged project config, for `recipes.memories`.
|
|
161
|
+
* @param sousDir - The project's `.sous/` directory.
|
|
162
|
+
* @param included - Every file the compile read, as real paths.
|
|
163
|
+
*/
|
|
164
|
+
export function unincludedMemories(
|
|
165
|
+
settings: Settings,
|
|
166
|
+
sousDir: string,
|
|
167
|
+
included: ReadonlySet<string>
|
|
168
|
+
): MemoryFile[] {
|
|
169
|
+
const memories = settings.recipes?.memories;
|
|
170
|
+
return listMemories({ sousDir, first: memories?.first, exclude: memories?.exclude }).filter(
|
|
171
|
+
(memory) => !included.has(realPath(memory.file))
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The path with symbolic links resolved, or the path itself when it cannot be read. */
|
|
176
|
+
function realPath(filePath: string): string {
|
|
177
|
+
try {
|
|
178
|
+
return fs.realpathSync(filePath);
|
|
179
|
+
} catch {
|
|
180
|
+
return filePath;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The warning for memories no output included, or undefined when there are none.
|
|
186
|
+
*
|
|
187
|
+
* @param missing - What {@link unincludedMemories} found.
|
|
188
|
+
*/
|
|
189
|
+
export function unincludedMemoriesWarning(missing: readonly MemoryFile[]): string | undefined {
|
|
190
|
+
if (missing.length === 0) return undefined;
|
|
191
|
+
const noun = missing.length === 1 ? "memory" : "memories";
|
|
192
|
+
return (
|
|
193
|
+
`${missing.length} ${noun} published by your subscribed recipes ${
|
|
194
|
+
missing.length === 1 ? "is" : "are"
|
|
195
|
+
} not included in any output, so no agent will read ${missing.length === 1 ? "it" : "them"}:\n` +
|
|
196
|
+
missing.map((memory) => `${BULLET} ${memory.recipe}: ${memory.relative}`).join("\n") +
|
|
197
|
+
`\nTo include every memory, add the line "@#memories/**/*.md" to your project's instruction ` +
|
|
198
|
+
`source (the file your AGENTS.md or CLAUDE.md is built from). To leave a recipe's memories out on purpose, ` +
|
|
199
|
+
`list the recipe in "recipes.memories.exclude" in your sous config.`
|
|
200
|
+
);
|
|
147
201
|
}
|
|
148
202
|
|
|
149
203
|
/**
|
|
@@ -193,6 +247,7 @@ function collectIncludeGraph(
|
|
|
193
247
|
aliases?: Record<string, string[]>;
|
|
194
248
|
scope?: Record<string, string>;
|
|
195
249
|
namespaceResolver?: NamespaceResolver;
|
|
250
|
+
views?: ViewMap;
|
|
196
251
|
} = {},
|
|
197
252
|
visited: Set<string> = new Set()
|
|
198
253
|
): Set<string> {
|
|
@@ -208,21 +263,23 @@ function collectIncludeGraph(
|
|
|
208
263
|
return visited;
|
|
209
264
|
}
|
|
210
265
|
|
|
211
|
-
const includePattern = /^@([~a-zA-Z0-9_${}][a-zA-Z0-9_\-/.:${}]*\.md)$/gm;
|
|
212
266
|
const baseDir = path.dirname(filePath);
|
|
213
|
-
let match: RegExpExecArray | null;
|
|
214
267
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
268
|
+
for (const line of content.split("\n")) {
|
|
269
|
+
const match = INCLUDE_LINE_PATTERN.exec(line.trimEnd());
|
|
270
|
+
if (match === null) continue;
|
|
271
|
+
const { files, candidates } = resolveIncludeFiles(match[1].trim(), {
|
|
218
272
|
aliases: resolveOpts.aliases,
|
|
273
|
+
views: resolveOpts.views,
|
|
219
274
|
scope: resolveOpts.scope,
|
|
220
275
|
baseDir,
|
|
221
276
|
namespaceResolver: resolveOpts.namespaceResolver,
|
|
222
277
|
fromFile: filePath,
|
|
223
278
|
});
|
|
224
|
-
|
|
225
|
-
|
|
279
|
+
// A file that does not exist yet is still part of the graph, as its first candidate.
|
|
280
|
+
for (const fullPath of files.length > 0 ? files : candidates.slice(0, 1)) {
|
|
281
|
+
collectIncludeGraph(fullPath, resolveOpts, visited);
|
|
282
|
+
}
|
|
226
283
|
}
|
|
227
284
|
|
|
228
285
|
return visited;
|
|
@@ -248,6 +305,7 @@ export function findAffectedTargets(
|
|
|
248
305
|
return config.targets.filter(target => {
|
|
249
306
|
const graph = collectIncludeGraph(target.rootInputPath, {
|
|
250
307
|
aliases: config.aliases,
|
|
308
|
+
views: config.views,
|
|
251
309
|
scope: config.includeScope,
|
|
252
310
|
namespaceResolver,
|
|
253
311
|
});
|
|
@@ -313,12 +371,23 @@ export class BuildService {
|
|
|
313
371
|
const recipes = resolveRecipeTargets(settings, rootScope, options.configContext, answers);
|
|
314
372
|
for (const notice of recipes.warnings) warning(notice);
|
|
315
373
|
|
|
316
|
-
const
|
|
374
|
+
const withoutViews = withRecipeTargets(
|
|
317
375
|
resolveCompilation(settings, rootScope),
|
|
318
376
|
recipes,
|
|
319
377
|
settings,
|
|
320
378
|
rootScope
|
|
321
379
|
);
|
|
380
|
+
// The files each view lists (`#memories`), worked out once per build.
|
|
381
|
+
const config: CompilationConfig | null =
|
|
382
|
+
withoutViews === null || options.configContext === undefined
|
|
383
|
+
? withoutViews
|
|
384
|
+
: {
|
|
385
|
+
...withoutViews,
|
|
386
|
+
views: sharedHashNames().viewMap({
|
|
387
|
+
sousDir: options.configContext.sousDir,
|
|
388
|
+
settings,
|
|
389
|
+
}),
|
|
390
|
+
};
|
|
322
391
|
|
|
323
392
|
if (config) {
|
|
324
393
|
let effectiveConfig: CompilationConfig = config;
|
|
@@ -351,6 +420,15 @@ export class BuildService {
|
|
|
351
420
|
});
|
|
352
421
|
const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
|
|
353
422
|
if (!compileOk) success = false;
|
|
423
|
+
|
|
424
|
+
// Only a full build knows every route an include can take, so only a
|
|
425
|
+
// full build that compiled cleanly checks that every memory got in.
|
|
426
|
+
if (compileOk && options.configContext !== undefined) {
|
|
427
|
+
const notice = unincludedMemoriesWarning(
|
|
428
|
+
unincludedMemories(settings, options.configContext.sousDir, compiler.includedFiles())
|
|
429
|
+
);
|
|
430
|
+
if (notice !== undefined) warning(notice);
|
|
431
|
+
}
|
|
354
432
|
}
|
|
355
433
|
}
|
|
356
434
|
}
|