@sous-io/sous 0.2.37 → 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 -245
- 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 -410
- 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 -413
- 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 -1107
- 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
package/src/lib/build-service.ts
DELETED
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import type { ConfigContext, Settings } from "./settings.js";
|
|
4
|
-
import { resolveAliases, resolveCompilation, resolveRootScope } from "./settings.js";
|
|
5
|
-
import { resolveIncludeFiles, type ViewMap } from "./include-resolver.js";
|
|
6
|
-
import type { NamespaceResolver } from "./repos/namespace-resolver.js";
|
|
7
|
-
import { createProjectNamespaceResolver } from "./repos/locked-namespace-resolver.js";
|
|
8
|
-
import { buildRecipeTargets, type RecipeTargets } from "./repos/recipe-targets.js";
|
|
9
|
-
import { CompilationService, INCLUDE_LINE_PATTERN, resolveOutputPath } from "./markdown-compiler.js";
|
|
10
|
-
import type { CompilationConfig, CompilationTarget } from "./markdown-compiler.js";
|
|
11
|
-
import { StateService } from "./state.js";
|
|
12
|
-
import { isProtectedPath } from "./state.js";
|
|
13
|
-
import { describeLinkedRepos, protectedRepoPaths } from "./repos/links.js";
|
|
14
|
-
import { subscriptionServiceFor } from "./repos/subscription-service.js";
|
|
15
|
-
import { prepareRepositoriesForBuild } from "./build-preparation.js";
|
|
16
|
-
import {
|
|
17
|
-
noRecipeAnswers,
|
|
18
|
-
resolveRecipeAnswers,
|
|
19
|
-
unansweredWarning,
|
|
20
|
-
type RecipeAnswers,
|
|
21
|
-
} from "./vars/answers.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";
|
|
25
|
-
|
|
26
|
-
export type BuildOptions = {
|
|
27
|
-
strict?: boolean;
|
|
28
|
-
rebuild?: boolean;
|
|
29
|
-
dryRun?: boolean;
|
|
30
|
-
noCompile?: boolean;
|
|
31
|
-
noPrune?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* When set, only targets that transitively include this file are compiled.
|
|
34
|
-
* All other targets are skipped. If no targets include this file, compilation
|
|
35
|
-
* is skipped entirely.
|
|
36
|
-
*/
|
|
37
|
-
changedFile?: string;
|
|
38
|
-
/**
|
|
39
|
-
* Where the active config was discovered. Threaded into variable resolution so
|
|
40
|
-
* `${sousDir}` resolves and so state/PID paths default into `.sous/`.
|
|
41
|
-
*/
|
|
42
|
-
configContext?: ConfigContext;
|
|
43
|
-
/**
|
|
44
|
-
* Resolves `~namespace` include and render paths against recipe namespaces.
|
|
45
|
-
* Passed straight through to the compiler and to the include-graph walk, so a
|
|
46
|
-
* partial rebuild follows namespace includes too.
|
|
47
|
-
*
|
|
48
|
-
* When it is omitted and `configContext` is given, the build builds the
|
|
49
|
-
* project's own resolver from its lockfile, links map and store. Pass one
|
|
50
|
-
* explicitly to override that, which is what tests do.
|
|
51
|
-
*/
|
|
52
|
-
namespaceResolver?: NamespaceResolver;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** An empty recipe-target result, for a build with no config context. */
|
|
56
|
-
const NO_RECIPE_TARGETS: RecipeTargets = {
|
|
57
|
-
targets: [],
|
|
58
|
-
destinations: [],
|
|
59
|
-
watchDirs: [],
|
|
60
|
-
warnings: [],
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The answers to every recipe variable in play for the project the context
|
|
65
|
-
* describes, resolved once so a build can render with them, hand them to every
|
|
66
|
-
* scope it builds, and report what is still unanswered. Empty when the caller
|
|
67
|
-
* gave no config context.
|
|
68
|
-
*
|
|
69
|
-
* @param settings - The merged project config.
|
|
70
|
-
* @param configContext - Where the active config was discovered.
|
|
71
|
-
*/
|
|
72
|
-
export function resolveProjectAnswers(
|
|
73
|
-
settings: Settings,
|
|
74
|
-
configContext?: ConfigContext
|
|
75
|
-
): RecipeAnswers {
|
|
76
|
-
if (configContext === undefined) return noRecipeAnswers();
|
|
77
|
-
return resolveRecipeAnswers({ settings, sousDir: configContext.sousDir });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The compile targets a project's subscribed recipes contribute, for the project
|
|
82
|
-
* the options describe. Empty when the caller gave no config context, which is
|
|
83
|
-
* the case only in tests that build a settings object by hand.
|
|
84
|
-
*
|
|
85
|
-
* Each recipe's files render with that recipe's own view of the answers, and a
|
|
86
|
-
* required variable nothing answered is reported through `warnings`, so every
|
|
87
|
-
* caller that prints those tells the user what the build could not fill in.
|
|
88
|
-
*
|
|
89
|
-
* @param settings - The merged project config.
|
|
90
|
-
* @param rootScope - The resolved settings scope, for `${var}` in destinations.
|
|
91
|
-
* @param configContext - Where the active config was discovered.
|
|
92
|
-
* @param answers - The project's recipe answers, when the caller resolved them
|
|
93
|
-
* already; resolved here otherwise.
|
|
94
|
-
*/
|
|
95
|
-
export function resolveRecipeTargets(
|
|
96
|
-
settings: Settings,
|
|
97
|
-
rootScope: Record<string, string>,
|
|
98
|
-
configContext?: ConfigContext,
|
|
99
|
-
answers?: RecipeAnswers
|
|
100
|
-
): RecipeTargets {
|
|
101
|
-
if (configContext === undefined) return NO_RECIPE_TARGETS;
|
|
102
|
-
const resolved = answers ?? resolveProjectAnswers(settings, configContext);
|
|
103
|
-
const scopes = new Map<string, Record<string, string>>();
|
|
104
|
-
|
|
105
|
-
const recipes = buildRecipeTargets({
|
|
106
|
-
sousDir: configContext.sousDir,
|
|
107
|
-
settings,
|
|
108
|
-
scope: rootScope,
|
|
109
|
-
scopeFor: (recipe) => {
|
|
110
|
-
let scope = scopes.get(recipe.key);
|
|
111
|
-
if (scope === undefined) {
|
|
112
|
-
scope = resolveRootScope(settings, configContext, { answers: resolved, recipe: recipe.key });
|
|
113
|
-
scopes.set(recipe.key, scope);
|
|
114
|
-
}
|
|
115
|
-
return scope;
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const missing = unansweredWarning(resolved, rootScope);
|
|
120
|
-
if (missing !== undefined) recipes.warnings.push(missing);
|
|
121
|
-
return { ...recipes, secretVariables: [...resolved.secrets] };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Adds the recipe targets to a project's own compilation config. A project with
|
|
126
|
-
* no compilation block of its own still compiles its recipes, so the config is
|
|
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.
|
|
130
|
-
*
|
|
131
|
-
* @param config - The project's own compilation config, or null when it has none.
|
|
132
|
-
* @param recipes - The targets the subscribed recipes contribute.
|
|
133
|
-
* @param settings - The merged project config, for its aliases.
|
|
134
|
-
* @param rootScope - The resolved settings scope.
|
|
135
|
-
*/
|
|
136
|
-
export function withRecipeTargets(
|
|
137
|
-
config: CompilationConfig | null,
|
|
138
|
-
recipes: RecipeTargets,
|
|
139
|
-
settings: Settings,
|
|
140
|
-
rootScope: Record<string, string>
|
|
141
|
-
): CompilationConfig | null {
|
|
142
|
-
const secrets = recipes.secretVariables?.length ? { secretVariables: recipes.secretVariables } : {};
|
|
143
|
-
if (recipes.targets.length === 0) return config === null ? null : { ...config, ...secrets };
|
|
144
|
-
if (config === null) {
|
|
145
|
-
return {
|
|
146
|
-
targets: recipes.targets,
|
|
147
|
-
aliases: resolveAliases(settings, rootScope),
|
|
148
|
-
includeScope: rootScope,
|
|
149
|
-
...secrets,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
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
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* The directories a build's deletions must never reach into, for the project the
|
|
205
|
-
* options describe. Empty when the caller gave no config context, which is the
|
|
206
|
-
* case only in tests that build a settings object by hand.
|
|
207
|
-
*
|
|
208
|
-
* @param options - The build options, for the config context.
|
|
209
|
-
*/
|
|
210
|
-
function protectedPathsFor(options: BuildOptions): string[] {
|
|
211
|
-
if (options.configContext === undefined) return [];
|
|
212
|
-
return protectedRepoPaths(options.configContext.sousDir);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* The namespace resolver a build should use: the one the caller supplied, or the
|
|
217
|
-
* project's own, built from its lockfile. A project that locks no recipes gets
|
|
218
|
-
* undefined, which leaves `~` in an include line meaning an alias and nothing
|
|
219
|
-
* else.
|
|
220
|
-
*
|
|
221
|
-
* @param settings - The merged project config.
|
|
222
|
-
* @param options - The build options, for the config context and any override.
|
|
223
|
-
*/
|
|
224
|
-
function resolveNamespaceResolver(
|
|
225
|
-
settings: Settings,
|
|
226
|
-
options: BuildOptions
|
|
227
|
-
): NamespaceResolver | undefined {
|
|
228
|
-
if (options.namespaceResolver !== undefined) return options.namespaceResolver;
|
|
229
|
-
if (options.configContext === undefined) return undefined;
|
|
230
|
-
return createProjectNamespaceResolver({
|
|
231
|
-
sousDir: options.configContext.sousDir,
|
|
232
|
-
settings,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Recursively collects all file paths reachable from `filePath` via @include chains.
|
|
238
|
-
* Returns a Set of absolute paths. The `visited` set prevents infinite loops.
|
|
239
|
-
*
|
|
240
|
-
* Matches the same @<path>.md include lines as CompilationService.processIncludes,
|
|
241
|
-
* including alias/`${var}` paths, resolving each via the same candidate logic
|
|
242
|
-
* (first existing wins).
|
|
243
|
-
*/
|
|
244
|
-
function collectIncludeGraph(
|
|
245
|
-
filePath: string,
|
|
246
|
-
resolveOpts: {
|
|
247
|
-
aliases?: Record<string, string[]>;
|
|
248
|
-
scope?: Record<string, string>;
|
|
249
|
-
namespaceResolver?: NamespaceResolver;
|
|
250
|
-
views?: ViewMap;
|
|
251
|
-
} = {},
|
|
252
|
-
visited: Set<string> = new Set()
|
|
253
|
-
): Set<string> {
|
|
254
|
-
if (visited.has(filePath)) return visited;
|
|
255
|
-
visited.add(filePath);
|
|
256
|
-
|
|
257
|
-
if (!fs.existsSync(filePath)) return visited;
|
|
258
|
-
|
|
259
|
-
let content: string;
|
|
260
|
-
try {
|
|
261
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
262
|
-
} catch {
|
|
263
|
-
return visited;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const baseDir = path.dirname(filePath);
|
|
267
|
-
|
|
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(), {
|
|
272
|
-
aliases: resolveOpts.aliases,
|
|
273
|
-
views: resolveOpts.views,
|
|
274
|
-
scope: resolveOpts.scope,
|
|
275
|
-
baseDir,
|
|
276
|
-
namespaceResolver: resolveOpts.namespaceResolver,
|
|
277
|
-
fromFile: filePath,
|
|
278
|
-
});
|
|
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
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return visited;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Returns the subset of compilation targets that transitively include `filePath`.
|
|
290
|
-
* A target is affected if its `rootInputPath` equals `filePath`, or if `filePath`
|
|
291
|
-
* is reachable via @include chains from `rootInputPath`.
|
|
292
|
-
*
|
|
293
|
-
* Uses a simple recursive file scan — reads each .md file and checks for
|
|
294
|
-
* @<path> include lines. Does not compile; just walks the include graph.
|
|
295
|
-
*
|
|
296
|
-
* @param filePath - The changed file.
|
|
297
|
-
* @param config - The resolved compilation config.
|
|
298
|
-
* @param namespaceResolver - Optional resolver so `~namespace` includes are followed too.
|
|
299
|
-
*/
|
|
300
|
-
export function findAffectedTargets(
|
|
301
|
-
filePath: string,
|
|
302
|
-
config: CompilationConfig,
|
|
303
|
-
namespaceResolver?: NamespaceResolver
|
|
304
|
-
): CompilationTarget[] {
|
|
305
|
-
return config.targets.filter(target => {
|
|
306
|
-
const graph = collectIncludeGraph(target.rootInputPath, {
|
|
307
|
-
aliases: config.aliases,
|
|
308
|
-
views: config.views,
|
|
309
|
-
scope: config.includeScope,
|
|
310
|
-
namespaceResolver,
|
|
311
|
-
});
|
|
312
|
-
return graph.has(filePath);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Resolves the state file path for a config.
|
|
318
|
-
*
|
|
319
|
-
* The path is derived from the resolved settings scope, so a `stateFilePath`
|
|
320
|
-
* or `sousDir` defined in `_vars` (or injected by discovery) is honoured.
|
|
321
|
-
*
|
|
322
|
-
* @param settings - The loaded settings.
|
|
323
|
-
* @param configContext - Where the config was discovered (supplies `sousDir`).
|
|
324
|
-
* @returns Absolute path to the state file.
|
|
325
|
-
*/
|
|
326
|
-
export function resolveStateFilePath(
|
|
327
|
-
settings: Settings,
|
|
328
|
-
configContext?: ConfigContext
|
|
329
|
-
): string {
|
|
330
|
-
const scope = resolveRootScope(settings, configContext);
|
|
331
|
-
return new StateService().getFilePath(scope);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export class BuildService {
|
|
335
|
-
/**
|
|
336
|
-
* Runs compile + prune for the configured project.
|
|
337
|
-
* Returns true if all steps succeeded.
|
|
338
|
-
*/
|
|
339
|
-
async build(settings: Settings, options: BuildOptions = {}): Promise<boolean> {
|
|
340
|
-
// The recipe answers are resolved once here and handed to every scope the
|
|
341
|
-
// build assembles, so the lockfile and the manifests are read one time.
|
|
342
|
-
const answers = resolveProjectAnswers(settings, options.configContext);
|
|
343
|
-
const rootScope = resolveRootScope(settings, options.configContext, { answers });
|
|
344
|
-
const namespaceResolver = resolveNamespaceResolver(settings, options);
|
|
345
|
-
const protectedPaths = protectedPathsFor(options);
|
|
346
|
-
|
|
347
|
-
const stateService = new StateService();
|
|
348
|
-
const stateFilePath = resolveStateFilePath(settings, options.configContext);
|
|
349
|
-
|
|
350
|
-
let success = true;
|
|
351
|
-
|
|
352
|
-
// When --rebuild, clear all previously written files before compiling so the
|
|
353
|
-
// rebuild starts from a physically clean slate. Orphaned outputs are also
|
|
354
|
-
// caught by prune (compile carries prior state entries forward), so this is
|
|
355
|
-
// the aggressive path: delete everything up front rather than prune after.
|
|
356
|
-
if (options.rebuild && !options.dryRun && !options.noCompile) {
|
|
357
|
-
const existingState = await stateService.load(stateFilePath);
|
|
358
|
-
if (existingState?.files.length) {
|
|
359
|
-
stateService.deleteTrackedFiles(
|
|
360
|
-
existingState.files,
|
|
361
|
-
existingState.dirs,
|
|
362
|
-
protectedPaths
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Compile step. The recipes this project subscribes to contribute compile
|
|
368
|
-
// targets alongside its own, so a recipe's files are compiled by exactly the
|
|
369
|
-
// same machinery as everything else, and are pruned and cleared by it too.
|
|
370
|
-
if (!options.noCompile) {
|
|
371
|
-
const recipes = resolveRecipeTargets(settings, rootScope, options.configContext, answers);
|
|
372
|
-
for (const notice of recipes.warnings) warning(notice);
|
|
373
|
-
|
|
374
|
-
const withoutViews = withRecipeTargets(
|
|
375
|
-
resolveCompilation(settings, rootScope),
|
|
376
|
-
recipes,
|
|
377
|
-
settings,
|
|
378
|
-
rootScope
|
|
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
|
-
};
|
|
391
|
-
|
|
392
|
-
if (config) {
|
|
393
|
-
let effectiveConfig: CompilationConfig = config;
|
|
394
|
-
|
|
395
|
-
if (options.changedFile) {
|
|
396
|
-
const affectedTargets = findAffectedTargets(
|
|
397
|
-
options.changedFile,
|
|
398
|
-
config,
|
|
399
|
-
namespaceResolver
|
|
400
|
-
);
|
|
401
|
-
if (affectedTargets.length === 0) {
|
|
402
|
-
log(` ⊘ No targets affected by change to ${options.changedFile} — skipping compilation`);
|
|
403
|
-
} else {
|
|
404
|
-
effectiveConfig = { ...config, targets: affectedTargets };
|
|
405
|
-
const compiler = new CompilationService({
|
|
406
|
-
strict: options.strict,
|
|
407
|
-
rebuild: options.rebuild,
|
|
408
|
-
dryRun: options.dryRun,
|
|
409
|
-
namespaceResolver,
|
|
410
|
-
});
|
|
411
|
-
const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
|
|
412
|
-
if (!compileOk) success = false;
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
const compiler = new CompilationService({
|
|
416
|
-
strict: options.strict,
|
|
417
|
-
rebuild: options.rebuild,
|
|
418
|
-
dryRun: options.dryRun,
|
|
419
|
-
namespaceResolver,
|
|
420
|
-
});
|
|
421
|
-
const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
|
|
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
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Prune step
|
|
437
|
-
if (!options.noPrune && success) {
|
|
438
|
-
await this.prune(settings, stateFilePath, options.dryRun, options.configContext);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return success;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Removes output files that are tracked in state but no longer in the current config.
|
|
446
|
-
* Also removes Sous-created directories that are now empty.
|
|
447
|
-
*/
|
|
448
|
-
async prune(
|
|
449
|
-
settings: Settings,
|
|
450
|
-
stateFilePath: string,
|
|
451
|
-
dryRun = false,
|
|
452
|
-
configContext?: ConfigContext
|
|
453
|
-
): Promise<void> {
|
|
454
|
-
const stateService = new StateService();
|
|
455
|
-
const state = await stateService.load(stateFilePath);
|
|
456
|
-
if (!state || state.files.length === 0) return;
|
|
457
|
-
|
|
458
|
-
const protectedPaths = configContext ? protectedRepoPaths(configContext.sousDir) : [];
|
|
459
|
-
const rootScope = resolveRootScope(settings, configContext);
|
|
460
|
-
const recipes = resolveRecipeTargets(settings, rootScope, configContext);
|
|
461
|
-
const config = resolveCompilation(settings, rootScope);
|
|
462
|
-
|
|
463
|
-
// Collect the current output set: explicit files and active destinationDir prefixes
|
|
464
|
-
const currentOutputFiles = new Set<string>();
|
|
465
|
-
const currentOutputDirs = new Set<string>();
|
|
466
|
-
if (config) {
|
|
467
|
-
for (const target of config.targets) {
|
|
468
|
-
for (const output of target.outputs) {
|
|
469
|
-
if (output.destinationFile) currentOutputFiles.add(output.destinationFile);
|
|
470
|
-
if (output.destinationDir) currentOutputDirs.add(output.destinationDir);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Recipe targets are counted file by file rather than by their destination
|
|
476
|
-
// directory. Every subscribed recipe writes into the same directory, so a
|
|
477
|
-
// directory prefix would make everything ever written there look current and
|
|
478
|
-
// an unsubscribed recipe's files would stay forever.
|
|
479
|
-
for (const target of recipes.targets) {
|
|
480
|
-
for (const output of target.outputs) {
|
|
481
|
-
const dest = resolveOutputPath(target, output);
|
|
482
|
-
if (dest !== undefined) currentOutputFiles.add(dest);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// A state entry is current if it matches an explicit destinationFile, or if its dest
|
|
487
|
-
// path falls under an active destinationDir (glob target output).
|
|
488
|
-
function isCurrentOutput(dest: string): boolean {
|
|
489
|
-
if (currentOutputFiles.has(dest)) return true;
|
|
490
|
-
for (const dir of currentOutputDirs) {
|
|
491
|
-
if (dest.startsWith(dir + path.sep) || dest.startsWith(dir + "/")) return true;
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Find files to prune. Anything inside a linked checkout or the shared
|
|
497
|
-
// recipe store is never a prune candidate, whatever the state file says.
|
|
498
|
-
const toDelete = state.files.filter(
|
|
499
|
-
f => !isCurrentOutput(f.dest) && !isProtectedPath(f.dest, protectedPaths)
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
if (dryRun) {
|
|
503
|
-
for (const entry of toDelete) {
|
|
504
|
-
console.log(` ○ would prune: ${entry.dest}`);
|
|
505
|
-
}
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
stateService.deleteTrackedFiles(toDelete, state.dirs, protectedPaths);
|
|
510
|
-
for (const entry of toDelete) console.log(` ✗ pruned: ${entry.dest}`);
|
|
511
|
-
|
|
512
|
-
// Update state: remove pruned entries and any dirs that no longer exist
|
|
513
|
-
const deleted = new Set(toDelete.map(entry => entry.dest));
|
|
514
|
-
state.files = state.files.filter(f => !deleted.has(f.dest));
|
|
515
|
-
state.dirs = state.dirs.filter(d => fs.existsSync(d));
|
|
516
|
-
await stateService.save(stateFilePath, state);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** What {@link runProjectBuild} is given. */
|
|
521
|
-
export type ProjectBuildInput = {
|
|
522
|
-
/** The merged project config, reloaded after any change a command made. */
|
|
523
|
-
settings: Settings;
|
|
524
|
-
/** Where the active config was discovered. */
|
|
525
|
-
configContext: ConfigContext;
|
|
526
|
-
/** The shell environment as it was before the env files were injected. */
|
|
527
|
-
shellEnv?: NodeJS.ProcessEnv;
|
|
528
|
-
/** The build's own options; every one defaults to what a plain `sous build` uses. */
|
|
529
|
-
options?: Omit<BuildOptions, "configContext">;
|
|
530
|
-
/** The heading printed above the compile and prune step. Defaults to "Building". */
|
|
531
|
-
heading?: string;
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* The one build every command runs: `build`, `init`, `launch`, `prune`,
|
|
536
|
-
* `repo remove`, `repo unlink`, `subscription add`, `subscription remove` and
|
|
537
|
-
* `subscription update` all come through here, so a project is built the same
|
|
538
|
-
* way whichever of them the user typed.
|
|
539
|
-
*
|
|
540
|
-
* In order, it announces every linked repository, prepares the project's
|
|
541
|
-
* recipes (seeds the core recipe, locks new subscriptions, restores whatever
|
|
542
|
-
* the store is missing, checks upstream and reports newer versions), and then
|
|
543
|
-
* compiles and prunes. A dry run prepares nothing, because preparing writes the
|
|
544
|
-
* lockfile and the store. A partial rebuild in watch mode (one changed source
|
|
545
|
-
* file) prepares nothing either: the config did not change, so neither did the
|
|
546
|
-
* recipes it needs.
|
|
547
|
-
*
|
|
548
|
-
* @param input - The project, the build options and the heading.
|
|
549
|
-
* @returns True when compile and prune both succeeded; false after any compile error.
|
|
550
|
-
*/
|
|
551
|
-
export async function runProjectBuild(input: ProjectBuildInput): Promise<boolean> {
|
|
552
|
-
const options = input.options ?? {};
|
|
553
|
-
const partial = options.changedFile !== undefined;
|
|
554
|
-
|
|
555
|
-
// A linked repository is read from a working copy instead of a published
|
|
556
|
-
// version, so it is announced on every build; a build that silently produced
|
|
557
|
-
// something different would be far worse than a noisy one.
|
|
558
|
-
if (!partial) {
|
|
559
|
-
const linked = describeLinkedRepos(input.configContext.sousDir);
|
|
560
|
-
if (linked.length > 0) warning(linked.join("\n"));
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (!options.dryRun && !partial) {
|
|
564
|
-
await prepareRepositoriesForBuild(
|
|
565
|
-
subscriptionServiceFor({
|
|
566
|
-
configContext: input.configContext,
|
|
567
|
-
settings: input.settings,
|
|
568
|
-
...(input.shellEnv === undefined ? {} : { shellEnv: input.shellEnv }),
|
|
569
|
-
})
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
heading(input.heading ?? "Building");
|
|
574
|
-
const succeeded = await new BuildService().build(input.settings, {
|
|
575
|
-
...options,
|
|
576
|
-
configContext: input.configContext,
|
|
577
|
-
});
|
|
578
|
-
footer();
|
|
579
|
-
return succeeded;
|
|
580
|
-
}
|