@sous-io/sous 0.2.37 → 1.0.2
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 +1094 -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/settings.ts
DELETED
|
@@ -1,1107 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { globSync } from "glob";
|
|
7
|
-
import { inferGlobBase, type CompilationConfig, type CompilationTarget, type ResolvedRuntimeContext } from "./markdown-compiler.js";
|
|
8
|
-
import { buildAliasMap, resolveAliasPrefix, type AliasMap } from "./include-resolver.js";
|
|
9
|
-
import { toGlobPattern } from "./glob-pattern.js";
|
|
10
|
-
import {
|
|
11
|
-
CONFD_DIR_NAME,
|
|
12
|
-
ENV_DEFAULTS_NAME,
|
|
13
|
-
ENV_LOCAL_NAME,
|
|
14
|
-
SOUS_DIR_NAME,
|
|
15
|
-
type DiscoveredConfig,
|
|
16
|
-
} from "./config-discovery.js";
|
|
17
|
-
import { ConfigError } from "./errors.js";
|
|
18
|
-
import { sharedHashNames } from "../services/ref-resolver/index.js";
|
|
19
|
-
import { resolveSousHome } from "./sous-home.js";
|
|
20
|
-
import { validateSettings } from "./config-schema.js";
|
|
21
|
-
import { applyRepoDefaults } from "./repos/defaults.js";
|
|
22
|
-
import type { RecipeConfigLayer } from "./repos/recipe-config-layers.js";
|
|
23
|
-
import {
|
|
24
|
-
answersForRecipe,
|
|
25
|
-
resolveRecipeAnswers,
|
|
26
|
-
type RecipeAnswers,
|
|
27
|
-
} from "./vars/answers.js";
|
|
28
|
-
import { warning } from "../utils/formatting.js";
|
|
29
|
-
|
|
30
|
-
// Re-exported for backwards compatibility: ConfigError moved to ./errors.ts so
|
|
31
|
-
// config-discovery.ts can throw it without importing this module (cycle).
|
|
32
|
-
export { ConfigError, isConfigError } from "./errors.js";
|
|
33
|
-
|
|
34
|
-
// CLI_ROOT and SOUS_VERSION live in their own module so that modules this one
|
|
35
|
-
// imports can read them without importing this one back. Re-exported here under
|
|
36
|
-
// the names everything already uses.
|
|
37
|
-
import { CLI_ROOT, SOUS_VERSION } from "./package-info.js";
|
|
38
|
-
export { CLI_ROOT, SOUS_VERSION };
|
|
39
|
-
|
|
40
|
-
// --- Variable scope ------------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/** A flat map of resolved variable names to their string values. */
|
|
43
|
-
export type VarScope = Record<string, string>;
|
|
44
|
-
|
|
45
|
-
// --- Raw settings types (matching settings.local.js shape) --------------------------------------
|
|
46
|
-
|
|
47
|
-
type RawOutput = {
|
|
48
|
-
_if?: Record<string, { eq: string }>;
|
|
49
|
-
_vars?: Record<string, string>;
|
|
50
|
-
destinationFile?: string;
|
|
51
|
-
destinationDir?: string;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type RawRuntimeContext = {
|
|
55
|
-
/** ${var} path to the git repo root used for branch detection. */
|
|
56
|
-
gitRoot: string;
|
|
57
|
-
/** ${var} path where the generated session-context file is written. */
|
|
58
|
-
outputPath: string;
|
|
59
|
-
/** ${var} root directory for task files; branch name is appended to locate the active file. */
|
|
60
|
-
taskFileRoot: string;
|
|
61
|
-
/** Regex pattern string; branches matching this pattern trigger task file lookup. */
|
|
62
|
-
branchPattern?: string;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type RawTarget = {
|
|
66
|
-
_vars?: Record<string, string>;
|
|
67
|
-
/** Point-to-point source file path. Exactly one of entryPoint or entryGlob must be set. */
|
|
68
|
-
entryPoint?: string;
|
|
69
|
-
/** Glob pattern expanding to multiple source files. Exactly one of entryPoint or entryGlob must be set. */
|
|
70
|
-
entryGlob?: string;
|
|
71
|
-
/**
|
|
72
|
-
* Explicit base directory for computing relative output paths when using destinationDir.
|
|
73
|
-
* Only meaningful for entryGlob targets. When omitted, inferred from the glob pattern.
|
|
74
|
-
*/
|
|
75
|
-
globBase?: string;
|
|
76
|
-
/**
|
|
77
|
-
* When true, the compiler generates a runtime session context file (branch name, task file)
|
|
78
|
-
* alongside the entry point before compilation. Only meaningful for AGENTS-style entry points.
|
|
79
|
-
*/
|
|
80
|
-
generateRuntimeContext?: boolean;
|
|
81
|
-
outputs: RawOutput[];
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
type RawProjectCompilation = {
|
|
85
|
-
_vars?: Record<string, string>;
|
|
86
|
-
includeSourceComments?: boolean;
|
|
87
|
-
targets: RawTarget[];
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* One trusted repository, keyed in `Settings.repos` by the short name refs use
|
|
92
|
-
* in their `repo:` qualifier. Adding a repo is what trusts it.
|
|
93
|
-
*/
|
|
94
|
-
export type RepoEntry = {
|
|
95
|
-
/** Where the repository lives. */
|
|
96
|
-
url: string;
|
|
97
|
-
/**
|
|
98
|
-
* Whether the repository takes part in anything. Defaults to true; setting it
|
|
99
|
-
* to false is how a project opts out of a repository sous provides itself,
|
|
100
|
-
* without deleting an entry it does not own.
|
|
101
|
-
*/
|
|
102
|
-
enabled?: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Which provider handles it. Inferred from the URL when omitted. `local` is a
|
|
105
|
-
* repository on this machine, for local development and tests.
|
|
106
|
-
*/
|
|
107
|
-
provider?: "github" | "gitlab" | "local";
|
|
108
|
-
/** Install a newer in-range version whenever one exists, rather than holding the lock. */
|
|
109
|
-
alwaysPull?: boolean;
|
|
110
|
-
/** When the repo was added. */
|
|
111
|
-
addedAt?: string;
|
|
112
|
-
/** Who required it: "user", or the ref of the recipe whose dependency pulled it in. */
|
|
113
|
-
addedBy?: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* One subscription, keyed in `Settings.subscriptions` by a ref key: a bare
|
|
118
|
-
* namespace, or `namespace/recipe`.
|
|
119
|
-
*/
|
|
120
|
-
export type SubscriptionEntry = {
|
|
121
|
-
/**
|
|
122
|
-
* Whether the subscription takes part in anything. Defaults to true; setting
|
|
123
|
-
* it to false is how a project opts out of the `core` namespace sous
|
|
124
|
-
* subscribes it to.
|
|
125
|
-
*/
|
|
126
|
-
enabled?: boolean;
|
|
127
|
-
/** The semantic version range to resolve within. Defaults to "*". */
|
|
128
|
-
range?: string;
|
|
129
|
-
/** Let prerelease versions take part in range matching. */
|
|
130
|
-
prerelease?: boolean;
|
|
131
|
-
/** Per-subscription form of the repo-level always-pull flag. */
|
|
132
|
-
alwaysPull?: boolean;
|
|
133
|
-
/** When the subscription was added. */
|
|
134
|
-
addedAt?: string;
|
|
135
|
-
/** Who required it: "user", or the ref of the recipe that co-subscribed it. */
|
|
136
|
-
addedBy?: string;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Knobs for the machine-wide recipe store. Defaults are applied by the store
|
|
141
|
-
* itself, not here; see config-schema.ts for the values sous ships.
|
|
142
|
-
*/
|
|
143
|
-
type StoreConfig = {
|
|
144
|
-
/** Size cap, past which least-recently-used entries are collected. */
|
|
145
|
-
maxBytes?: number;
|
|
146
|
-
/** How long a fetched index stays fresh before sous re-checks upstream. */
|
|
147
|
-
freshnessSeconds?: number;
|
|
148
|
-
/** How often watch mode polls upstream. */
|
|
149
|
-
watchPollSeconds?: number;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* What a project does with each content kind its subscribed recipes publish.
|
|
154
|
-
*
|
|
155
|
-
* `skills.outputs` lists the destination directories for recipe skills, each
|
|
156
|
-
* `${var}` substituted; the default is `<project root>/.claude/skills`, the
|
|
157
|
-
* project root being the parent of `.sous/`. `memories.first` and
|
|
158
|
-
* `memories.exclude` are lists of globs, or `/.../` regular expressions, over
|
|
159
|
-
* `namespace/recipe` keys.
|
|
160
|
-
*/
|
|
161
|
-
type RecipesConfig = {
|
|
162
|
-
skills?: {
|
|
163
|
-
/** Where recipe skill bundles are written. */
|
|
164
|
-
outputs?: string[];
|
|
165
|
-
};
|
|
166
|
-
memories?: {
|
|
167
|
-
/** Recipes whose memories come first. */
|
|
168
|
-
first?: string[];
|
|
169
|
-
/** Recipes whose memories are left out. */
|
|
170
|
-
exclude?: string[];
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/** Configuration for a launchable tool (e.g. claude, codex). */
|
|
175
|
-
type ToolConfig = {
|
|
176
|
-
/** The executable command to run. */
|
|
177
|
-
command: string;
|
|
178
|
-
/** Arguments passed before the prompt file argument. */
|
|
179
|
-
args?: string[];
|
|
180
|
-
/**
|
|
181
|
-
* Path to a file whose contents are appended as the final argument to the command.
|
|
182
|
-
* Path is resolved through the project's var scope.
|
|
183
|
-
*/
|
|
184
|
-
promptFile?: string;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export type Settings = {
|
|
188
|
-
/** Config schema version. Optional; when present must be 1 (validated at load). */
|
|
189
|
-
version?: number;
|
|
190
|
-
_env?: Record<string, string>;
|
|
191
|
-
/**
|
|
192
|
-
* Config variables. A few names are read by Sous itself:
|
|
193
|
-
* `stateFilePath` overrides where the build state file is written (see
|
|
194
|
-
* StateService.getFilePath), and `pidFilePath` does the same for the watcher
|
|
195
|
-
* PID file. Both resolve through the settings scope.
|
|
196
|
-
*/
|
|
197
|
-
_vars?: Record<string, string>;
|
|
198
|
-
_aliases?: Record<string, string | string[]>;
|
|
199
|
-
/** Optional display name for the configured project. */
|
|
200
|
-
name?: string;
|
|
201
|
-
compilation?: RawProjectCompilation;
|
|
202
|
-
runtimeContext?: RawRuntimeContext;
|
|
203
|
-
tools?: Record<string, ToolConfig>;
|
|
204
|
-
/** Trusted repositories, keyed by the short name refs use. */
|
|
205
|
-
repos?: Record<string, RepoEntry>;
|
|
206
|
-
/** Subscriptions, keyed by ref key (`namespace` or `namespace/recipe`). */
|
|
207
|
-
subscriptions?: Record<string, SubscriptionEntry>;
|
|
208
|
-
/** Knobs for the machine-wide recipe store. */
|
|
209
|
-
store?: StoreConfig;
|
|
210
|
-
/**
|
|
211
|
-
* What the project does with each content kind its subscribed recipes publish.
|
|
212
|
-
*/
|
|
213
|
-
recipes?: RecipesConfig;
|
|
214
|
-
/**
|
|
215
|
-
* Variable mapping records, keyed by environment variable name, each bound to
|
|
216
|
-
* one recipe variable written as `namespace/recipe/variableName` with an
|
|
217
|
-
* optional `repo:` qualifier. The top rung of the answer resolution ladder.
|
|
218
|
-
*/
|
|
219
|
-
varMappings?: Record<string, string>;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// --- Loader -------------------------------------------------------------------------------------
|
|
223
|
-
|
|
224
|
-
/** Options accepted by loadSettings / loadSettingsWithLayers. */
|
|
225
|
-
export type LoadSettingsOptions = {
|
|
226
|
-
/**
|
|
227
|
-
* When true, the kernel returns one cumulative-config snapshot per top-level
|
|
228
|
-
* layer (the provenance seam behind `sous config get --layers`).
|
|
229
|
-
*/
|
|
230
|
-
trace?: boolean;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/** One trace-mode snapshot: the cumulative config AFTER `path` was merged. */
|
|
234
|
-
export type SettingsLayer = {
|
|
235
|
-
path: string;
|
|
236
|
-
config: unknown;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
/** Absolute path to the config kernel subprocess entry (plain .mjs, ships in src/). */
|
|
240
|
-
const CONFIG_KERNEL_PATH = path.join(CLI_ROOT, "src", "lib", "config-kernel.mjs");
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Loads settings from a discovered config (primary file + conf.d layers) and
|
|
244
|
-
* returns the merged result together with any trace-mode layer snapshots.
|
|
245
|
-
*
|
|
246
|
-
* Every layer — .js, .mjs, .json and .yaml alike — is loaded by ONE kernel
|
|
247
|
-
* subprocess (src/lib/config-kernel.mjs), which parses/imports each file in
|
|
248
|
-
* order, JSON-forces it, deep-merges it into a live cumulative config, runs
|
|
249
|
-
* `configure(currentConfig, builder)` exports, and serialises the final JSON
|
|
250
|
-
* once. Uniform kernel semantics beat the spawn cost, so there is no
|
|
251
|
-
* parent-side shortcut for plain JSON.
|
|
252
|
-
*
|
|
253
|
-
* Two spawn attempts are made, in this order:
|
|
254
|
-
*
|
|
255
|
-
* 1. Plain Node, no loader. This is what a normal ESM config needs, and it is
|
|
256
|
-
* the only thing that works for a `.sous/sous.config.js` sitting in a repo
|
|
257
|
-
* whose package.json has no `"type": "module"` (under the tsx loader such a
|
|
258
|
-
* file is treated as CJS and dies with ERR_REQUIRE_CYCLE_MODULE).
|
|
259
|
-
* 2. The tsx loader, so a config may use TypeScript syntax and extensionless
|
|
260
|
-
* relative imports. (The kernel itself is plain .mjs and runs under both.)
|
|
261
|
-
*
|
|
262
|
-
* The subprocess (rather than a direct `import()`) avoids the require(esm) cycle
|
|
263
|
-
* that tsx triggers in the parent process. Because every layer is round-tripped
|
|
264
|
-
* through JSON, functions, RegExp, Date and undefined values are dropped.
|
|
265
|
-
*
|
|
266
|
-
* @param source - The full DiscoveredConfig, or a bare config file path. A bare
|
|
267
|
-
* string means exactly that one file: no conf.d scan is performed (back-compat
|
|
268
|
-
* for tests and direct callers).
|
|
269
|
-
* @param options - `{ trace }` — see LoadSettingsOptions.
|
|
270
|
-
*/
|
|
271
|
-
/* c8 ignore next 75 */
|
|
272
|
-
export async function loadSettingsWithLayers(
|
|
273
|
-
source: DiscoveredConfig | string,
|
|
274
|
-
options: LoadSettingsOptions = {}
|
|
275
|
-
): Promise<{ settings: Settings; layers: SettingsLayer[] }> {
|
|
276
|
-
let configPath: string;
|
|
277
|
-
let sousDir: string;
|
|
278
|
-
let confDir: string;
|
|
279
|
-
let layerPaths: string[];
|
|
280
|
-
let recipeLayers: RecipeConfigLayer[];
|
|
281
|
-
|
|
282
|
-
if (typeof source === "string") {
|
|
283
|
-
configPath = path.resolve(source);
|
|
284
|
-
sousDir = path.dirname(configPath);
|
|
285
|
-
confDir = path.join(sousDir, CONFD_DIR_NAME);
|
|
286
|
-
layerPaths = [configPath];
|
|
287
|
-
recipeLayers = [];
|
|
288
|
-
} else {
|
|
289
|
-
({ configPath, sousDir, confDir, layerPaths } = source);
|
|
290
|
-
recipeLayers = source.recipeLayers ?? [];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (!fs.existsSync(configPath)) {
|
|
294
|
-
throw new Error(`Settings file not found: ${configPath}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// A recipe layer is handed to the kernel as content, not as a path, because
|
|
298
|
-
// it has already been read and filtered down to the keys a recipe may set
|
|
299
|
-
// (see `repos/recipe-config-layers.ts`). The kernel never opens the file, so
|
|
300
|
-
// there is no second, unfiltered reading of it.
|
|
301
|
-
const recipeLayerByPath = new Map(recipeLayers.map((layer) => [layer.path, layer]));
|
|
302
|
-
const sources = layerPaths.map((layerPath) => {
|
|
303
|
-
const recipeLayer = recipeLayerByPath.get(layerPath);
|
|
304
|
-
if (recipeLayer === undefined) return layerPath;
|
|
305
|
-
return { path: recipeLayer.path, config: recipeLayer.config };
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const kernelInput = JSON.stringify({
|
|
309
|
-
sources,
|
|
310
|
-
context: {
|
|
311
|
-
sousDir,
|
|
312
|
-
confDir,
|
|
313
|
-
sousRootPath: CLI_ROOT,
|
|
314
|
-
sousVersion: SOUS_VERSION,
|
|
315
|
-
configPath,
|
|
316
|
-
},
|
|
317
|
-
trace: options.trace === true,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Resolve tsx via module resolution so this works when npm hoists the
|
|
321
|
-
// dependency (local install, npx) as well as when it nests it (global,
|
|
322
|
-
// repo clone).
|
|
323
|
-
let tsxPath: string;
|
|
324
|
-
try {
|
|
325
|
-
tsxPath = createRequire(import.meta.url).resolve("tsx/esm");
|
|
326
|
-
} catch {
|
|
327
|
-
tsxPath = path.resolve(CLI_ROOT, "node_modules/tsx/dist/esm/index.cjs");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const attempts: { label: string; args: string[] }[] = [
|
|
331
|
-
{ label: "node", args: [CONFIG_KERNEL_PATH] },
|
|
332
|
-
// `--import` takes a module specifier, so the path goes as a file:// URL:
|
|
333
|
-
// a bare Windows path reads as a URL whose scheme is the drive letter.
|
|
334
|
-
{ label: "tsx", args: ["--import", pathToFileURL(tsxPath).href, CONFIG_KERNEL_PATH] },
|
|
335
|
-
];
|
|
336
|
-
|
|
337
|
-
const failures: { label: string; message: string }[] = [];
|
|
338
|
-
|
|
339
|
-
for (const attempt of attempts) {
|
|
340
|
-
const result = spawnSync(process.execPath, attempt.args, {
|
|
341
|
-
input: kernelInput,
|
|
342
|
-
encoding: "utf8",
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
if (result.status === 0) {
|
|
346
|
-
let parsed: { config: unknown; layers?: SettingsLayer[] };
|
|
347
|
-
try {
|
|
348
|
-
parsed = JSON.parse(result.stdout) as { config: unknown; layers?: SettingsLayer[] };
|
|
349
|
-
} catch {
|
|
350
|
-
throw new Error(
|
|
351
|
-
`Config at ${configPath} did not produce valid JSON. Every layer must resolve ` +
|
|
352
|
-
`to a plain object (a \`config\`/default export, or a \`configure\` function).\n` +
|
|
353
|
-
` Got: ${result.stdout.slice(0, 300)}`
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
// assertFlatConfig runs FIRST: its multi-project migration message is more
|
|
357
|
-
// actionable than a generic unknown-key error. validateSettings then checks
|
|
358
|
-
// the MERGED config against the zod schema (version, strict keys, shapes).
|
|
359
|
-
const flat = assertFlatConfig(parsed.config, configPath);
|
|
360
|
-
// The built-in repository and the implicit `core` subscription are added
|
|
361
|
-
// UNDER whatever the layers produced, and BEFORE validation, so that the
|
|
362
|
-
// shortest opt-out a project can write (`{ enabled: false }`) is a
|
|
363
|
-
// complete, valid entry once the built-in fields are underneath it. See
|
|
364
|
-
// `repos/defaults.ts`.
|
|
365
|
-
return {
|
|
366
|
-
settings: validateSettings(applyRepoDefaults(flat), configPath),
|
|
367
|
-
layers: parsed.layers ?? [],
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
failures.push({ label: attempt.label, message: result.stderr?.trim() || "unknown error" });
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Kernel-side errors (bad JSON/YAML, old schema, configure() throw, cycle,
|
|
375
|
-
// bad builder var, non-object layer) are raised inside the .mjs kernel, which
|
|
376
|
-
// runs identically under both the `node` and `tsx` attempts — so both stderrs
|
|
377
|
-
// are the same. Collapse identical messages so a single config error is not
|
|
378
|
-
// printed twice as if it were two distinct failures.
|
|
379
|
-
//
|
|
380
|
-
// The tsx attempt re-imports an ESM `.js` config that the `node` attempt
|
|
381
|
-
// already loaded and reported a real error for; on the tsx attempt this
|
|
382
|
-
// surfaces as a spurious "Cannot require() ES Module … in a cycle"
|
|
383
|
-
// (ERR_REQUIRE_CYCLE_MODULE) that is never a real user-config problem. Drop it
|
|
384
|
-
// whenever another attempt produced a genuine error, so the real message is
|
|
385
|
-
// not buried under an unrelated require(esm)-cycle warning.
|
|
386
|
-
const isTsxCycleArtifact = (message: string): boolean =>
|
|
387
|
-
/ERR_REQUIRE_CYCLE_MODULE/.test(message) ||
|
|
388
|
-
/Cannot require\(\) ES Module .* in a cycle/.test(message);
|
|
389
|
-
const meaningfulFailures = failures.filter((f) => !isTsxCycleArtifact(f.message));
|
|
390
|
-
const effectiveFailures = meaningfulFailures.length > 0 ? meaningfulFailures : failures;
|
|
391
|
-
|
|
392
|
-
const uniqueMessages = [...new Set(effectiveFailures.map((f) => f.message))];
|
|
393
|
-
const detail =
|
|
394
|
-
uniqueMessages.length === 1
|
|
395
|
-
? ` ${uniqueMessages[0]}`
|
|
396
|
-
: effectiveFailures.map((f) => ` [via ${f.label}] ${f.message}`).join("\n\n");
|
|
397
|
-
|
|
398
|
-
throw new ConfigError(`Failed to load config from ${configPath}\n${detail}`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Loads the merged settings for a discovered config (or a bare config file
|
|
403
|
-
* path). Thin wrapper over loadSettingsWithLayers for callers that do not need
|
|
404
|
-
* the trace-mode layer snapshots.
|
|
405
|
-
*/
|
|
406
|
-
export async function loadSettings(
|
|
407
|
-
source: DiscoveredConfig | string,
|
|
408
|
-
options: LoadSettingsOptions = {}
|
|
409
|
-
): Promise<Settings> {
|
|
410
|
-
const { settings } = await loadSettingsWithLayers(source, options);
|
|
411
|
-
return settings;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Rejects configs written in the removed multi-project schema. One config now
|
|
416
|
-
* describes exactly one project; the fields that used to live inside a
|
|
417
|
-
* `projects.<key>` entry sit at the top level instead.
|
|
418
|
-
*/
|
|
419
|
-
function assertFlatConfig(raw: unknown, configPath: string): Settings {
|
|
420
|
-
if (
|
|
421
|
-
raw !== null &&
|
|
422
|
-
typeof raw === "object" &&
|
|
423
|
-
("projects" in raw || "defaultProject" in raw)
|
|
424
|
-
) {
|
|
425
|
-
throw new ConfigError(
|
|
426
|
-
`Config at ${configPath} uses the removed multi-project schema ` +
|
|
427
|
-
`('projects' / 'defaultProject').\n` +
|
|
428
|
-
` A sous config now describes exactly one project. To migrate:\n` +
|
|
429
|
-
` 1. Move your single project's fields (name, _vars, _aliases, compilation,\n` +
|
|
430
|
-
` runtimeContext, tools) to the top level of the config.\n` +
|
|
431
|
-
` 2. Delete the 'projects' and 'defaultProject' keys.\n` +
|
|
432
|
-
` A config with several projects must be split into one config per project.`
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
return raw as Settings;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// --- Variable Resolution -------------------------------------------------------------------------
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Substitutes ${varName} references in a string using the provided scope.
|
|
442
|
-
* Unknown variable references are left as-is.
|
|
443
|
-
*/
|
|
444
|
-
export function substituteVars(str: string, scope: VarScope): string {
|
|
445
|
-
return str.replace(/\$\{([^}]+)\}/g, (match, name: string) => scope[name] ?? match);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** Returns the names of every `${var}` reference left unresolved in a string. */
|
|
449
|
-
export function findUnresolvedVars(str: string): string[] {
|
|
450
|
-
return [...new Set([...str.matchAll(/\$\{([^}]+)\}/g)].map((m) => m[1]))];
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Collapses `.` and `..` segments in an already-substituted absolute path.
|
|
455
|
-
*
|
|
456
|
-
* Output destinations must be normalized at resolution time, not left as written.
|
|
457
|
-
* A config that names a directory as `${sousDir}/..` (the natural way to reach the
|
|
458
|
-
* repo root from a discovered `.sous/`) otherwise produces a `destinationDir` of
|
|
459
|
-
* `/repo/.sous/../.claude/skills`, while the file paths Sous actually writes go
|
|
460
|
-
* through `path.join` and come out as `/repo/.claude/skills/...`. Prune compares
|
|
461
|
-
* tracked destinations against `destinationDir` by string prefix, so the two forms
|
|
462
|
-
* never match and prune deletes every file compile had just written.
|
|
463
|
-
*
|
|
464
|
-
* Relative values are left alone: they are resolved later against a context this
|
|
465
|
-
* function does not have.
|
|
466
|
-
*
|
|
467
|
-
* @param value - A substituted path from the config.
|
|
468
|
-
* @returns The normalized path, or `value` unchanged when it is not absolute.
|
|
469
|
-
*/
|
|
470
|
-
export function normalizeConfigPath(value: string): string {
|
|
471
|
-
return path.isAbsolute(value) ? path.normalize(value) : value;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Substitutes `${varName}` references and throws when any reference cannot be
|
|
476
|
-
* resolved from the scope. Used for every value Sous acts on (entry points,
|
|
477
|
-
* destinations, prompt files, runtime-context paths) so a typo'd or missing
|
|
478
|
-
* variable fails loudly instead of silently producing a literal `${var}` path.
|
|
479
|
-
*
|
|
480
|
-
* @param str - The raw value from the config.
|
|
481
|
-
* @param scope - The resolved variable scope.
|
|
482
|
-
* @param context - Where the value came from, e.g.
|
|
483
|
-
* `compilation.targets[0].entryPoint`. Named in the error.
|
|
484
|
-
* @returns The fully substituted string.
|
|
485
|
-
* @throws When one or more `${var}` references are unresolved.
|
|
486
|
-
*/
|
|
487
|
-
export function substituteVarsStrict(str: string, scope: VarScope, context: string): string {
|
|
488
|
-
const result = substituteVars(str, scope);
|
|
489
|
-
const unresolved = findUnresolvedVars(result);
|
|
490
|
-
|
|
491
|
-
if (unresolved.length > 0) {
|
|
492
|
-
const names = unresolved.map((n) => `\${${n}}`).join(", ");
|
|
493
|
-
const plural = unresolved.length === 1 ? "variable" : "variables";
|
|
494
|
-
const available = Object.keys(scope).sort();
|
|
495
|
-
throw new ConfigError(
|
|
496
|
-
`Unresolved ${plural} ${names} in ${context}\n` +
|
|
497
|
-
` raw value: ${str}\n` +
|
|
498
|
-
` Define ${unresolved.length === 1 ? "it" : "them"} in a _vars block, or map ` +
|
|
499
|
-
`${unresolved.length === 1 ? "it" : "them"} from the environment via the top-level ` +
|
|
500
|
-
`_env block (values can come from .sous/.env.local or .sous/.env).\n` +
|
|
501
|
-
` Variables in scope here: ${available.length > 0 ? available.join(", ") : "(none)"}`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Resolves a _vars block into a new scope with a FIXPOINT loop:
|
|
510
|
-
*
|
|
511
|
-
* 1. Start from the inherited scope. Every block entry begins unresolved.
|
|
512
|
-
* 2. Each round, substitute every still-unresolved entry against the current
|
|
513
|
-
* scope (inherited vars + entries already finalized this pass). An entry
|
|
514
|
-
* whose `${refs}` all resolve is finalized and added to the scope.
|
|
515
|
-
* 3. Repeat until a round finalizes nothing.
|
|
516
|
-
*
|
|
517
|
-
* Because each round re-scans every unresolved entry, declaration order does not
|
|
518
|
-
* matter: `{ file: "${root}/x", root: "/data" }` resolves as readily as the
|
|
519
|
-
* reverse. When progress stops with entries still unresolved, that is a hard
|
|
520
|
-
* error (see buildUnresolvedScopeError): a `${ref}` that never resolves — a typo,
|
|
521
|
-
* a cycle, or a name defined nowhere — is a config mistake, not a literal to be
|
|
522
|
-
* passed through silently.
|
|
523
|
-
*/
|
|
524
|
-
export function resolveScope(block: Record<string, string>, inherited: VarScope): VarScope {
|
|
525
|
-
const blockKeys = Object.keys(block);
|
|
526
|
-
|
|
527
|
-
// Warn if user defines vars in the reserved 'sous*' namespace
|
|
528
|
-
for (const key of blockKeys) {
|
|
529
|
-
if (key.startsWith("sous")) {
|
|
530
|
-
console.warn(`Warning: variable '${key}' uses the reserved 'sous*' namespace and may conflict with auto-injected variables.`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const scope: VarScope = { ...inherited };
|
|
535
|
-
const unresolved = new Set(blockKeys);
|
|
536
|
-
|
|
537
|
-
let progressed = true;
|
|
538
|
-
while (progressed && unresolved.size > 0) {
|
|
539
|
-
progressed = false;
|
|
540
|
-
for (const key of unresolved) {
|
|
541
|
-
const substituted = substituteVars(block[key], scope);
|
|
542
|
-
if (findUnresolvedVars(substituted).length === 0) {
|
|
543
|
-
scope[key] = substituted;
|
|
544
|
-
unresolved.delete(key);
|
|
545
|
-
progressed = true;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (unresolved.size > 0) {
|
|
551
|
-
throw buildUnresolvedScopeError(block, scope, unresolved);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return scope;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Builds the ConfigError thrown when resolveScope's fixpoint stops with entries
|
|
559
|
-
* still unresolved. The message names each unresolved entry and the exact
|
|
560
|
-
* `${names}` it still needs, then separates the two failure modes: reference
|
|
561
|
-
* CYCLES (entries that depend on each other, no starting point) and UNDEFINED
|
|
562
|
-
* references (names defined nowhere). It closes with the variables that ARE in
|
|
563
|
-
* scope, so a typo is obvious at a glance.
|
|
564
|
-
*
|
|
565
|
-
* @param block - The raw _vars block being resolved.
|
|
566
|
-
* @param scope - The working scope (inherited vars + every entry that DID
|
|
567
|
-
* resolve); its keys are the "in scope" list.
|
|
568
|
-
* @param unresolved - The block keys that never resolved.
|
|
569
|
-
*/
|
|
570
|
-
function buildUnresolvedScopeError(
|
|
571
|
-
block: Record<string, string>,
|
|
572
|
-
scope: VarScope,
|
|
573
|
-
unresolved: Set<string>
|
|
574
|
-
): ConfigError {
|
|
575
|
-
// For each unresolved entry, the ${names} still missing after fixpoint. Every
|
|
576
|
-
// such name is either another unresolved block key (an intra-block edge) or a
|
|
577
|
-
// name defined nowhere (resolvable block keys and inherited vars are already
|
|
578
|
-
// in `scope`, so they never appear here).
|
|
579
|
-
const needs = new Map<string, string[]>();
|
|
580
|
-
for (const key of unresolved) {
|
|
581
|
-
needs.set(key, findUnresolvedVars(substituteVars(block[key], scope)));
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Dependency graph among unresolved entries: u -> v when u still needs the
|
|
585
|
-
// still-unresolved block key v. SCCs of this graph are the reference cycles.
|
|
586
|
-
const edges = new Map<string, string[]>();
|
|
587
|
-
for (const key of unresolved) {
|
|
588
|
-
edges.set(key, (needs.get(key) ?? []).filter((n) => unresolved.has(n)));
|
|
589
|
-
}
|
|
590
|
-
const cycles = findCycles([...unresolved], edges);
|
|
591
|
-
|
|
592
|
-
// Undefined references: needed names that are not block keys at all.
|
|
593
|
-
const undefinedRefs = new Map<string, string[]>();
|
|
594
|
-
for (const key of unresolved) {
|
|
595
|
-
for (const name of needs.get(key) ?? []) {
|
|
596
|
-
if (!unresolved.has(name)) {
|
|
597
|
-
const referrers = undefinedRefs.get(name) ?? [];
|
|
598
|
-
referrers.push(key);
|
|
599
|
-
undefinedRefs.set(name, referrers);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const lines: string[] = [
|
|
605
|
-
"Unresolved variables after fixpoint resolution of a _vars block:",
|
|
606
|
-
];
|
|
607
|
-
for (const key of [...unresolved].sort()) {
|
|
608
|
-
const names = (needs.get(key) ?? []).map((n) => `\${${n}}`).join(", ");
|
|
609
|
-
lines.push(` - ${key} still needs ${names || "(nothing resolvable)"}`);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (cycles.length > 0) {
|
|
613
|
-
lines.push("");
|
|
614
|
-
lines.push("Reference cycles (these variables reference each other):");
|
|
615
|
-
for (const cycle of cycles) {
|
|
616
|
-
const sorted = [...cycle].sort();
|
|
617
|
-
lines.push(` - ${[...sorted, sorted[0]].join(" -> ")}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (undefinedRefs.size > 0) {
|
|
622
|
-
lines.push("");
|
|
623
|
-
lines.push("Undefined references (names defined nowhere — no _vars, _env, or auto-var):");
|
|
624
|
-
for (const name of [...undefinedRefs.keys()].sort()) {
|
|
625
|
-
const referrers = [...new Set(undefinedRefs.get(name) ?? [])].sort();
|
|
626
|
-
lines.push(` - \${${name}} (needed by ${referrers.join(", ")})`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const inScope = Object.keys(scope).sort();
|
|
631
|
-
lines.push("");
|
|
632
|
-
lines.push(`Variables in scope here: ${inScope.length > 0 ? inScope.join(", ") : "(none)"}`);
|
|
633
|
-
|
|
634
|
-
return new ConfigError(lines.join("\n"));
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Finds reference cycles in a directed graph via Tarjan's strongly-connected-
|
|
639
|
-
* component algorithm. A cycle is an SCC with more than one member, or a single
|
|
640
|
-
* node that references itself. Returns each cycle as its member list.
|
|
641
|
-
*/
|
|
642
|
-
function findCycles(nodes: string[], edges: Map<string, string[]>): string[][] {
|
|
643
|
-
const index = new Map<string, number>();
|
|
644
|
-
const low = new Map<string, number>();
|
|
645
|
-
const onStack = new Set<string>();
|
|
646
|
-
const stack: string[] = [];
|
|
647
|
-
const sccs: string[][] = [];
|
|
648
|
-
let counter = 0;
|
|
649
|
-
|
|
650
|
-
function strongconnect(v: string): void {
|
|
651
|
-
index.set(v, counter);
|
|
652
|
-
low.set(v, counter);
|
|
653
|
-
counter++;
|
|
654
|
-
stack.push(v);
|
|
655
|
-
onStack.add(v);
|
|
656
|
-
|
|
657
|
-
for (const w of edges.get(v) ?? []) {
|
|
658
|
-
if (!index.has(w)) {
|
|
659
|
-
strongconnect(w);
|
|
660
|
-
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
|
661
|
-
} else if (onStack.has(w)) {
|
|
662
|
-
low.set(v, Math.min(low.get(v)!, index.get(w)!));
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (low.get(v) === index.get(v)) {
|
|
667
|
-
const component: string[] = [];
|
|
668
|
-
let w: string;
|
|
669
|
-
do {
|
|
670
|
-
w = stack.pop()!;
|
|
671
|
-
onStack.delete(w);
|
|
672
|
-
component.push(w);
|
|
673
|
-
} while (w !== v);
|
|
674
|
-
sccs.push(component);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
for (const v of nodes) {
|
|
679
|
-
if (!index.has(v)) strongconnect(v);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return sccs.filter(
|
|
683
|
-
(component) =>
|
|
684
|
-
component.length > 1 || (edges.get(component[0]) ?? []).includes(component[0])
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Where the active config came from. Threaded through variable resolution so
|
|
690
|
-
* configs can reference their own `.sous/` directory and so error messages can
|
|
691
|
-
* point at the right `.env.local` / `.env`.
|
|
692
|
-
*/
|
|
693
|
-
export type ConfigContext = {
|
|
694
|
-
/** Absolute path to the `.sous/` directory holding the config. */
|
|
695
|
-
sousDir: string;
|
|
696
|
-
/** Absolute path to the primary config file itself. */
|
|
697
|
-
configPath: string;
|
|
698
|
-
/** Absolute path to the `conf.d/` drop-in directory (may not exist). */
|
|
699
|
-
confDir?: string;
|
|
700
|
-
/** Ordered absolute paths of every loaded config layer (primary first). */
|
|
701
|
-
layerPaths?: string[];
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Builds the auto-injected variable scope. These vars are always available
|
|
706
|
-
* and injected first, before _env and _vars.
|
|
707
|
-
* The 'sous*' namespace is reserved — warns if user defines a var starting with 'sous'.
|
|
708
|
-
*
|
|
709
|
-
* `sousHome` is resolved from `process.env` on every call rather than captured
|
|
710
|
-
* once, because `SOUS_HOME` is file-settable: `.sous/.env.local` and
|
|
711
|
-
* `.sous/.env` are loaded into `process.env` before settings resolve.
|
|
712
|
-
*
|
|
713
|
-
* @param context - The discovered config location. When supplied, adds
|
|
714
|
-
* `sousDir` and `sousConfigPath` (plus `sousConfDir` when known) so configs
|
|
715
|
-
* can build paths relative to their own `.sous/` directory.
|
|
716
|
-
*/
|
|
717
|
-
export function buildAutoVars(context?: ConfigContext): VarScope {
|
|
718
|
-
return {
|
|
719
|
-
sousRootPath: CLI_ROOT,
|
|
720
|
-
sousVersion: SOUS_VERSION,
|
|
721
|
-
sousHome: resolveSousHome(),
|
|
722
|
-
...(context !== undefined && {
|
|
723
|
-
sousDir: context.sousDir,
|
|
724
|
-
sousConfigPath: context.configPath,
|
|
725
|
-
}),
|
|
726
|
-
...(context?.confDir !== undefined && {
|
|
727
|
-
sousConfDir: context.confDir,
|
|
728
|
-
}),
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Resolves the top-level _env block into a VarScope.
|
|
734
|
-
* Each entry maps a config var name (key) to an environment variable name (value).
|
|
735
|
-
* Throws a clear error if any referenced env var is not set.
|
|
736
|
-
* Only called on the root Settings object — _env is top-level only.
|
|
737
|
-
*
|
|
738
|
-
* @param settings - The root settings object.
|
|
739
|
-
* @param context - The discovered config location, used to name the
|
|
740
|
-
* `.sous/.env.local` and `.sous/.env` files in the error message.
|
|
741
|
-
*/
|
|
742
|
-
export function resolveEnvScope(settings: Settings, context?: ConfigContext): VarScope {
|
|
743
|
-
const env = settings._env ?? {};
|
|
744
|
-
const scope: VarScope = {};
|
|
745
|
-
|
|
746
|
-
for (const [varName, envVarName] of Object.entries(env)) {
|
|
747
|
-
const value = process.env[envVarName];
|
|
748
|
-
if (value === undefined) {
|
|
749
|
-
const envLocalPath = context
|
|
750
|
-
? path.join(context.sousDir, ENV_LOCAL_NAME)
|
|
751
|
-
: `${SOUS_DIR_NAME}/${ENV_LOCAL_NAME}`;
|
|
752
|
-
const envDefaultsPath = context
|
|
753
|
-
? path.join(context.sousDir, ENV_DEFAULTS_NAME)
|
|
754
|
-
: `${SOUS_DIR_NAME}/${ENV_DEFAULTS_NAME}`;
|
|
755
|
-
throw new ConfigError(
|
|
756
|
-
`_env resolution failed: environment variable '${envVarName}' (mapped to config ` +
|
|
757
|
-
`var '${varName}') is not set.\n` +
|
|
758
|
-
` Define it in ${envLocalPath} as:\n` +
|
|
759
|
-
` ${envVarName}=<value>\n` +
|
|
760
|
-
` ...or, if the value is shared by the whole team and is not a secret, in ` +
|
|
761
|
-
`${envDefaultsPath} (committed).\n` +
|
|
762
|
-
` ...or export it in your shell before running sous.`
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
scope[varName] = value;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return scope;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/** What else a root scope may be built from; see resolveRootScope. */
|
|
772
|
-
export type RootScopeOptions = {
|
|
773
|
-
/**
|
|
774
|
-
* The recipe answers already resolved for this project. When omitted and a
|
|
775
|
-
* config context is given, they are resolved here; pass them when a caller
|
|
776
|
-
* has them already, so the lockfile and the manifests are read once.
|
|
777
|
-
*/
|
|
778
|
-
answers?: RecipeAnswers;
|
|
779
|
-
/**
|
|
780
|
-
* The recipe whose own files this scope renders, as `namespace/recipe`. Its
|
|
781
|
-
* own answers are laid over the merged view, so a recipe sees the answer to
|
|
782
|
-
* its own question even when another recipe asks the same name.
|
|
783
|
-
*/
|
|
784
|
-
recipe?: string;
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Resolves the root-level _vars from a Settings object into a scope.
|
|
789
|
-
* Chains: auto-vars → recipe answers → env scope → root _vars.
|
|
790
|
-
*
|
|
791
|
-
* The recipe answers are the values the project's env files and shell hold for
|
|
792
|
-
* every variable its subscribed recipes publish, found through the ladder in
|
|
793
|
-
* `vars/ladder.ts`. They sit under `_env` and `_vars`, so an explicit config
|
|
794
|
-
* value always wins, and they are present only when a config context says which
|
|
795
|
-
* project this is; a settings object built by hand in a test has none.
|
|
796
|
-
*
|
|
797
|
-
* @param settings - The root settings object.
|
|
798
|
-
* @param context - The discovered config location (optional in tests).
|
|
799
|
-
* @param options - Answers already resolved, or the recipe the scope is for.
|
|
800
|
-
*/
|
|
801
|
-
export function resolveRootScope(
|
|
802
|
-
settings: Settings,
|
|
803
|
-
context?: ConfigContext,
|
|
804
|
-
options: RootScopeOptions = {}
|
|
805
|
-
): VarScope {
|
|
806
|
-
const autoVars = buildAutoVars(context);
|
|
807
|
-
const answers = resolveAnswerLayer(settings, context, options);
|
|
808
|
-
const envScope = resolveEnvScope(settings, context);
|
|
809
|
-
const baseScope = { ...autoVars, ...answers, ...envScope };
|
|
810
|
-
return resolveScope(settings._vars ?? {}, baseScope);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* The recipe-answer layer of a root scope: the merged view, or a recipe's own
|
|
815
|
-
* view of it. Empty without a config context.
|
|
816
|
-
*/
|
|
817
|
-
function resolveAnswerLayer(
|
|
818
|
-
settings: Settings,
|
|
819
|
-
context: ConfigContext | undefined,
|
|
820
|
-
options: RootScopeOptions
|
|
821
|
-
): VarScope {
|
|
822
|
-
if (context === undefined) return {};
|
|
823
|
-
const answers =
|
|
824
|
-
options.answers ?? resolveRecipeAnswers({ settings, sousDir: context.sousDir });
|
|
825
|
-
return options.recipe === undefined ? answers.merged : answersForRecipe(answers, options.recipe);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Built-in `@include` aliases: every `#` name the registry holds
|
|
830
|
-
* (`sharedHashNames`), each standing for its directories in this project. They
|
|
831
|
-
* are reserved (user `_aliases` may not start with `#`), so a user alias can
|
|
832
|
-
* never shadow one.
|
|
833
|
-
*
|
|
834
|
-
* - `#project` is the consuming project's root (`projectRoot`).
|
|
835
|
-
*
|
|
836
|
-
* Files that used to be reached through a built-in alias pointing inside the
|
|
837
|
-
* sous package are published as recipes now, and a recipe's files are addressed
|
|
838
|
-
* by its namespace (`@~workflow/task-files/_partials/resume-task.md`),
|
|
839
|
-
* resolved against what the project has pinned. A `~` reference is NOT an
|
|
840
|
-
* alias: it is resolved separately, after the alias map has been tried; see
|
|
841
|
-
* `locked-namespace-resolver.ts`.
|
|
842
|
-
*/
|
|
843
|
-
export function buildBuiltInAliases(scope: VarScope): AliasMap {
|
|
844
|
-
return sharedHashNames().aliasMap(scope as Record<string, string>);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Resolve the full `@include` alias map: built-ins, then the config's
|
|
849
|
-
* `_aliases` block (user entries prepend, so they are tried first and fall
|
|
850
|
-
* through to built-in bases of the same name). User alias names starting
|
|
851
|
-
* with `~` or `#` are a ConfigError (reserved).
|
|
852
|
-
*
|
|
853
|
-
* @param settings - The loaded settings (for the `_aliases` block).
|
|
854
|
-
* @param scope - The resolved settings scope (for ${var} substitution + projectRoot).
|
|
855
|
-
*/
|
|
856
|
-
export function resolveAliases(settings: Settings, scope: VarScope): AliasMap {
|
|
857
|
-
return buildAliasMap({
|
|
858
|
-
builtIns: buildBuiltInAliases(scope),
|
|
859
|
-
userAliases: [settings._aliases],
|
|
860
|
-
scope,
|
|
861
|
-
onError: (message) => {
|
|
862
|
-
throw new ConfigError(message);
|
|
863
|
-
},
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/** A resolved tool configuration with promptFile path substituted. */
|
|
868
|
-
export type ResolvedToolConfig = {
|
|
869
|
-
command: string;
|
|
870
|
-
args?: string[];
|
|
871
|
-
promptFile?: string;
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Resolves the config's tools block, substituting vars in promptFile paths.
|
|
876
|
-
* Returns an empty object if no tools are configured.
|
|
877
|
-
*
|
|
878
|
-
* @param settings - The loaded settings.
|
|
879
|
-
* @param scope - The resolved settings scope (from resolveRootScope).
|
|
880
|
-
*/
|
|
881
|
-
export function resolveTools(
|
|
882
|
-
settings: Settings,
|
|
883
|
-
scope: VarScope = {}
|
|
884
|
-
): Record<string, ResolvedToolConfig> {
|
|
885
|
-
if (!settings.tools) return {};
|
|
886
|
-
|
|
887
|
-
return Object.fromEntries(
|
|
888
|
-
Object.entries(settings.tools).map(([name, tool]) => [
|
|
889
|
-
name,
|
|
890
|
-
{
|
|
891
|
-
command: tool.command,
|
|
892
|
-
...(tool.args !== undefined && { args: tool.args }),
|
|
893
|
-
...(tool.promptFile !== undefined && {
|
|
894
|
-
promptFile: substituteVarsStrict(
|
|
895
|
-
tool.promptFile,
|
|
896
|
-
scope,
|
|
897
|
-
`tools.${name}.promptFile`
|
|
898
|
-
),
|
|
899
|
-
}),
|
|
900
|
-
},
|
|
901
|
-
])
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// --- Compilation Resolution ----------------------------------------------------------------------
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* Resolves a RawRuntimeContext into a ResolvedRuntimeContext by substituting
|
|
909
|
-
* ${var} references and converting branchPattern from string to RegExp.
|
|
910
|
-
*/
|
|
911
|
-
function resolveRuntimeContext(
|
|
912
|
-
raw: RawRuntimeContext,
|
|
913
|
-
scope: VarScope,
|
|
914
|
-
context: string
|
|
915
|
-
): ResolvedRuntimeContext {
|
|
916
|
-
return {
|
|
917
|
-
gitRoot: substituteVarsStrict(raw.gitRoot, scope, `${context}.gitRoot`),
|
|
918
|
-
outputPath: substituteVarsStrict(raw.outputPath, scope, `${context}.outputPath`),
|
|
919
|
-
taskFileRoot: substituteVarsStrict(raw.taskFileRoot, scope, `${context}.taskFileRoot`),
|
|
920
|
-
branchPattern: new RegExp(raw.branchPattern ?? "PT-"),
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Resolves the config's compilation block into the shape the compiler expects.
|
|
926
|
-
* Walks the config tree resolving _vars at each level (settings → compilation → target → output).
|
|
927
|
-
* Pass scope from resolveRootScope(settings) to thread the settings vars down.
|
|
928
|
-
* Returns null if the config has no compilation block.
|
|
929
|
-
*/
|
|
930
|
-
export function resolveCompilation(
|
|
931
|
-
settings: Settings,
|
|
932
|
-
scope: VarScope = {}
|
|
933
|
-
): CompilationConfig | null {
|
|
934
|
-
if (!settings.compilation) return null;
|
|
935
|
-
|
|
936
|
-
const compilationScope = resolveScope(settings.compilation._vars ?? {}, scope);
|
|
937
|
-
const aliases = resolveAliases(settings, scope);
|
|
938
|
-
|
|
939
|
-
return {
|
|
940
|
-
includeSourceComments: settings.compilation.includeSourceComments,
|
|
941
|
-
aliases,
|
|
942
|
-
includeScope: scope,
|
|
943
|
-
targets: settings.compilation.targets.flatMap((target, targetIndex): CompilationTarget[] => {
|
|
944
|
-
const targetScope = resolveScope(target._vars ?? {}, compilationScope);
|
|
945
|
-
const where = `compilation.targets[${targetIndex}]`;
|
|
946
|
-
const hasSingle = target.entryPoint !== undefined;
|
|
947
|
-
const hasGlob = target.entryGlob !== undefined;
|
|
948
|
-
|
|
949
|
-
if (hasSingle && hasGlob) {
|
|
950
|
-
throw new Error("Target cannot have both entryPoint and entryGlob");
|
|
951
|
-
}
|
|
952
|
-
if (!hasSingle && !hasGlob) {
|
|
953
|
-
throw new Error("Target must have either entryPoint or entryGlob");
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Resolves the outputs array for a target, filtering by _if conditions and substituting vars.
|
|
958
|
-
*/
|
|
959
|
-
function resolveOutputs(scope: VarScope) {
|
|
960
|
-
return target.outputs
|
|
961
|
-
.filter(output => {
|
|
962
|
-
if (!output._if) return true;
|
|
963
|
-
const outputScope = resolveScope(output._vars ?? {}, scope);
|
|
964
|
-
return Object.entries(output._if).every(([varName, condition]) => {
|
|
965
|
-
const val = outputScope[varName] ?? scope[varName] ?? compilationScope[varName];
|
|
966
|
-
return val === condition.eq;
|
|
967
|
-
});
|
|
968
|
-
})
|
|
969
|
-
.map((output, outputIndex) => {
|
|
970
|
-
const outputScope = resolveScope(output._vars ?? {}, scope);
|
|
971
|
-
const outputWhere = `${where}.outputs[${outputIndex}]`;
|
|
972
|
-
return {
|
|
973
|
-
...(output.destinationFile !== undefined && {
|
|
974
|
-
destinationFile: normalizeConfigPath(
|
|
975
|
-
substituteVarsStrict(
|
|
976
|
-
output.destinationFile,
|
|
977
|
-
outputScope,
|
|
978
|
-
`${outputWhere}.destinationFile`
|
|
979
|
-
)
|
|
980
|
-
),
|
|
981
|
-
}),
|
|
982
|
-
...(output.destinationDir !== undefined && {
|
|
983
|
-
destinationDir: normalizeConfigPath(
|
|
984
|
-
substituteVarsStrict(
|
|
985
|
-
output.destinationDir,
|
|
986
|
-
outputScope,
|
|
987
|
-
`${outputWhere}.destinationDir`
|
|
988
|
-
)
|
|
989
|
-
),
|
|
990
|
-
}),
|
|
991
|
-
vars: outputScope,
|
|
992
|
-
};
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (hasSingle) {
|
|
997
|
-
const runtimeContext =
|
|
998
|
-
target.generateRuntimeContext && settings.runtimeContext
|
|
999
|
-
? resolveRuntimeContext(settings.runtimeContext, scope, "runtimeContext")
|
|
1000
|
-
: undefined;
|
|
1001
|
-
return [{
|
|
1002
|
-
rootInputPath: normalizeConfigPath(
|
|
1003
|
-
substituteVarsStrict(target.entryPoint!, targetScope, `${where}.entryPoint`)
|
|
1004
|
-
),
|
|
1005
|
-
...(runtimeContext !== undefined && { runtimeContext }),
|
|
1006
|
-
outputs: resolveOutputs(targetScope),
|
|
1007
|
-
}];
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/* c8 ignore start */
|
|
1011
|
-
// Glob target: expand pattern into one CompilationTarget per matched file, skipping dirs.
|
|
1012
|
-
// A leading alias (`#project/skills/**`) expands to one candidate pattern per alias
|
|
1013
|
-
// base; the first base that matches any files wins, mirroring the first-existing-wins
|
|
1014
|
-
// rule of @include resolution.
|
|
1015
|
-
const rawPattern = substituteVarsStrict(target.entryGlob!, targetScope, `${where}.entryGlob`);
|
|
1016
|
-
const patternCandidates = resolveAliasPrefix(rawPattern, aliases).map(toGlobPattern);
|
|
1017
|
-
let pattern = patternCandidates[0];
|
|
1018
|
-
let matchedFiles: string[] = [];
|
|
1019
|
-
for (const candidate of patternCandidates) {
|
|
1020
|
-
const matches = globSync(candidate, { absolute: true })
|
|
1021
|
-
.filter(filePath => fs.statSync(filePath).isFile());
|
|
1022
|
-
if (matches.length > 0) {
|
|
1023
|
-
pattern = candidate;
|
|
1024
|
-
matchedFiles = matches;
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (matchedFiles.length === 0) {
|
|
1030
|
-
warning(`Glob pattern matched no files:\n${patternCandidates.join("\n")}`);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const globBase = normalizeConfigPath(
|
|
1034
|
-
target.globBase
|
|
1035
|
-
? substituteVarsStrict(target.globBase, targetScope, `${where}.globBase`)
|
|
1036
|
-
: inferGlobBase(pattern)
|
|
1037
|
-
);
|
|
1038
|
-
return matchedFiles.map(filePath => ({
|
|
1039
|
-
rootInputPath: filePath,
|
|
1040
|
-
globBase,
|
|
1041
|
-
outputs: resolveOutputs(targetScope),
|
|
1042
|
-
}));
|
|
1043
|
-
/* c8 ignore stop */
|
|
1044
|
-
}),
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/** Resolved watch configuration for a project. */
|
|
1049
|
-
export type WatchConfig = {
|
|
1050
|
-
/** Exact file paths — watched directly by chokidar. Trigger partial rebuilds. */
|
|
1051
|
-
files: string[];
|
|
1052
|
-
/**
|
|
1053
|
-
* Glob patterns — chokidar watches their base directories; incoming events are
|
|
1054
|
-
* filtered against these patterns before triggering a partial rebuild.
|
|
1055
|
-
*/
|
|
1056
|
-
globs: string[];
|
|
1057
|
-
/**
|
|
1058
|
-
* Additional paths (files or directories) that trigger a full rebuild when changed.
|
|
1059
|
-
* Used for the settings file, templating directory, and config imports.
|
|
1060
|
-
*/
|
|
1061
|
-
fullRebuildPaths?: string[];
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Returns the watch configuration for the config's compilation targets.
|
|
1066
|
-
*
|
|
1067
|
-
* - entryPoint targets → exact file path in `files`.
|
|
1068
|
-
* - entryGlob targets → resolved glob string in `globs`.
|
|
1069
|
-
*
|
|
1070
|
-
* Alias prefixes in entryGlob patterns are expanded (every base of the alias
|
|
1071
|
-
* is watched, matching compile's fall-through resolution).
|
|
1072
|
-
*/
|
|
1073
|
-
export function resolveWatchConfig(settings: Settings, scope: VarScope = {}): WatchConfig {
|
|
1074
|
-
if (!settings.compilation) return { files: [], globs: [] };
|
|
1075
|
-
|
|
1076
|
-
const compilationScope = resolveScope(settings.compilation._vars ?? {}, scope);
|
|
1077
|
-
const aliases = resolveAliases(settings, scope);
|
|
1078
|
-
const files: string[] = [];
|
|
1079
|
-
const globs: string[] = [];
|
|
1080
|
-
|
|
1081
|
-
for (const [targetIndex, target] of settings.compilation.targets.entries()) {
|
|
1082
|
-
const targetScope = resolveScope(target._vars ?? {}, compilationScope);
|
|
1083
|
-
const where = `compilation.targets[${targetIndex}]`;
|
|
1084
|
-
|
|
1085
|
-
if (target.entryPoint) {
|
|
1086
|
-
// Normalize to match the compilation path (rootInputPath) and, more
|
|
1087
|
-
// importantly, chokidar's change events: chokidar reports the OS-normalized
|
|
1088
|
-
// path, so an unnormalized `${sousDir}/../prompts/A.md` entry would never
|
|
1089
|
-
// string-match the `/prompts/A.md` event and the partial rebuild would never fire.
|
|
1090
|
-
files.push(
|
|
1091
|
-
normalizeConfigPath(
|
|
1092
|
-
substituteVarsStrict(target.entryPoint, targetScope, `${where}.entryPoint`)
|
|
1093
|
-
)
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (target.entryGlob) {
|
|
1098
|
-
const pattern = substituteVarsStrict(target.entryGlob, targetScope, `${where}.entryGlob`);
|
|
1099
|
-
globs.push(...resolveAliasPrefix(pattern, aliases).map(normalizeConfigPath));
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
return {
|
|
1104
|
-
files: [...new Set(files)],
|
|
1105
|
-
globs: [...new Set(globs)],
|
|
1106
|
-
};
|
|
1107
|
-
}
|