@sous-io/sous 0.2.36 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -237
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -391
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -412
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1105
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
package/src/lib/vars/validate.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validating an answer against the definition that asked for it.
|
|
3
|
-
*
|
|
4
|
-
* A recipe declares a small, deliberately boring constraint vocabulary (a type,
|
|
5
|
-
* plus `pattern`, `minLength`, `maxLength`, `min`, `max` and `enum`), all of it
|
|
6
|
-
* expressible in plain JSON so a manifest never has to carry code. This module
|
|
7
|
-
* turns that vocabulary into a zod schema and reports a failure in the same
|
|
8
|
-
* plain language the question was asked in, naming the constraint that was
|
|
9
|
-
* violated rather than dumping a schema error.
|
|
10
|
-
*
|
|
11
|
-
* Answers are stored as text in env files, so validation always starts from a
|
|
12
|
-
* string and hands back the coerced value.
|
|
13
|
-
*
|
|
14
|
-
* A `pattern` comes from a recipe, which is code from somewhere else, so it is
|
|
15
|
-
* never run on this thread without a limit; see `safe-regex.ts`. A pattern that
|
|
16
|
-
* exceeds its budget fails validation, and the failure says so plainly rather
|
|
17
|
-
* than telling the person that their answer is wrong.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
|
|
22
|
-
import { DEFAULT_PATTERN_BUDGET_MS, matchWithBudget } from "./safe-regex.js";
|
|
23
|
-
|
|
24
|
-
/** The value an answer coerces to, once it has been validated. */
|
|
25
|
-
export type AnswerValue = string | number | boolean;
|
|
26
|
-
|
|
27
|
-
/** What the caller knows about where a definition came from, for messages. */
|
|
28
|
-
export interface ValidationContext {
|
|
29
|
-
/**
|
|
30
|
-
* The recipe that published the definition, named the way it should read in a
|
|
31
|
-
* message (for example 'acme/web-app'). Omitted when the caller does not know
|
|
32
|
-
* it, in which case messages describe it in words instead.
|
|
33
|
-
*/
|
|
34
|
-
recipe?: string;
|
|
35
|
-
/** How long a declared pattern may run, in milliseconds. */
|
|
36
|
-
patternBudgetMs?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** A validated answer, or the reason it was refused. */
|
|
40
|
-
export type AnswerValidation =
|
|
41
|
-
| { ok: true; value: AnswerValue }
|
|
42
|
-
| { ok: false; message: string };
|
|
43
|
-
|
|
44
|
-
/** The strings accepted as a true answer for a boolean variable. */
|
|
45
|
-
const TRUE_WORDS = new Set(["true", "yes", "y", "on", "1"]);
|
|
46
|
-
|
|
47
|
-
/** The strings accepted as a false answer for a boolean variable. */
|
|
48
|
-
const FALSE_WORDS = new Set(["false", "no", "n", "off", "0"]);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Builds the zod schema for one definition. The schema's input is the trimmed
|
|
52
|
-
* answer text and its output is the coerced value, so callers get the number or
|
|
53
|
-
* boolean a `number` or `boolean` variable promised.
|
|
54
|
-
*
|
|
55
|
-
* @param definition - The variable definition to build a schema for.
|
|
56
|
-
* @param context - What is known about the recipe, for the pattern message.
|
|
57
|
-
*/
|
|
58
|
-
export function validationSchemaFor(
|
|
59
|
-
definition: VariableDefinition,
|
|
60
|
-
context: ValidationContext = {}
|
|
61
|
-
): z.ZodType<AnswerValue> {
|
|
62
|
-
const rules = definition.validate;
|
|
63
|
-
|
|
64
|
-
switch (definition.type) {
|
|
65
|
-
case "number": {
|
|
66
|
-
const schema = z
|
|
67
|
-
.string()
|
|
68
|
-
.refine((value) => value.length > 0 && Number.isFinite(Number(value)), {
|
|
69
|
-
message: "must be a number",
|
|
70
|
-
})
|
|
71
|
-
.transform((value) => Number(value))
|
|
72
|
-
.pipe(numberRules());
|
|
73
|
-
return schema as unknown as z.ZodType<AnswerValue>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
case "boolean": {
|
|
77
|
-
const schema = z
|
|
78
|
-
.string()
|
|
79
|
-
.refine((value) => TRUE_WORDS.has(value.toLowerCase()) || FALSE_WORDS.has(value.toLowerCase()), {
|
|
80
|
-
message: "must be 'true' or 'false'",
|
|
81
|
-
})
|
|
82
|
-
.transform((value) => TRUE_WORDS.has(value.toLowerCase()));
|
|
83
|
-
return schema as unknown as z.ZodType<AnswerValue>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case "enum": {
|
|
87
|
-
const options = rules?.enum ?? [];
|
|
88
|
-
const schema = z.string().refine((value) => options.includes(value), {
|
|
89
|
-
message: `must be one of: ${options.join(", ")}`,
|
|
90
|
-
});
|
|
91
|
-
return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
case "url": {
|
|
95
|
-
const schema = z.string().refine(
|
|
96
|
-
(value) => {
|
|
97
|
-
try {
|
|
98
|
-
const parsed = new URL(value);
|
|
99
|
-
return parsed.protocol.length > 1;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
{ message: "must be a URL, including its scheme (for example 'https://example.com')" }
|
|
105
|
-
);
|
|
106
|
-
return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
case "path": {
|
|
110
|
-
const schema = z.string().refine((value) => !value.includes("\0"), {
|
|
111
|
-
message: "must be a filesystem path, with no null characters in it",
|
|
112
|
-
});
|
|
113
|
-
return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
case "string":
|
|
117
|
-
default:
|
|
118
|
-
return stringRules(z.string()) as unknown as z.ZodType<AnswerValue>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Applies the numeric constraints to a number schema. */
|
|
122
|
-
function numberRules(): z.ZodType<number, number> {
|
|
123
|
-
let schema = z.number();
|
|
124
|
-
if (rules?.min !== undefined) schema = schema.min(rules.min, `must be at least ${rules.min}`);
|
|
125
|
-
if (rules?.max !== undefined) schema = schema.max(rules.max, `must be at most ${rules.max}`);
|
|
126
|
-
return schema;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Applies the string constraints, including the required-means-non-empty rule. */
|
|
130
|
-
function stringRules(base: z.ZodType<string>): z.ZodType<string> {
|
|
131
|
-
let schema: z.ZodType<string> = base;
|
|
132
|
-
|
|
133
|
-
if (definition.required) {
|
|
134
|
-
schema = schema.refine((value) => value.length > 0, { message: "must not be empty" });
|
|
135
|
-
}
|
|
136
|
-
if (rules?.minLength !== undefined) {
|
|
137
|
-
schema = schema.refine((value) => value.length >= rules.minLength!, {
|
|
138
|
-
message: `must be at least ${rules.minLength} characters long`,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
if (rules?.maxLength !== undefined) {
|
|
142
|
-
schema = schema.refine((value) => value.length <= rules.maxLength!, {
|
|
143
|
-
message: `must be at most ${rules.maxLength} characters long`,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (rules?.pattern !== undefined) {
|
|
147
|
-
const pattern = rules.pattern;
|
|
148
|
-
const budget = context.patternBudgetMs ?? DEFAULT_PATTERN_BUDGET_MS;
|
|
149
|
-
schema = schema.superRefine((value, ctx) => {
|
|
150
|
-
const outcome = matchWithBudget(pattern, value, budget);
|
|
151
|
-
if (outcome === "match") return;
|
|
152
|
-
ctx.addIssue({
|
|
153
|
-
code: "custom",
|
|
154
|
-
message:
|
|
155
|
-
outcome === "timeout"
|
|
156
|
-
? patternTooSlowMessage(pattern, budget, context.recipe)
|
|
157
|
-
: `must match the pattern ${pattern}`,
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return schema;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The failure for a pattern that ran out of time. It names the pattern and the
|
|
168
|
-
* recipe that published it, and says plainly that the pattern is the problem;
|
|
169
|
-
* the person answering has no way to write an answer that a runaway pattern
|
|
170
|
-
* would finish on.
|
|
171
|
-
*
|
|
172
|
-
* @param pattern - The regular expression source the recipe declared.
|
|
173
|
-
* @param budgetMs - The budget it exceeded, in milliseconds.
|
|
174
|
-
* @param recipe - The recipe that published it, when the caller knows it.
|
|
175
|
-
*/
|
|
176
|
-
function patternTooSlowMessage(
|
|
177
|
-
pattern: string,
|
|
178
|
-
budgetMs: number,
|
|
179
|
-
recipe?: string
|
|
180
|
-
): string {
|
|
181
|
-
const publisher = recipe === undefined ? "the recipe that defines it" : `the recipe ${recipe}`;
|
|
182
|
-
return (
|
|
183
|
-
`could not be checked: the pattern ${pattern}, published by ${publisher}, ` +
|
|
184
|
-
`took longer than ${budgetMs} milliseconds to run, so sous stopped waiting ` +
|
|
185
|
-
"for it. The pattern is too slow to run, and the answer was not the problem; " +
|
|
186
|
-
"this needs to be reported to whoever publishes the recipe"
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Validates one answer against its definition.
|
|
192
|
-
*
|
|
193
|
-
* @param definition - The variable definition the answer is for.
|
|
194
|
-
* @param raw - The answer as text, as typed or as read from an env file.
|
|
195
|
-
* @param context - What is known about the recipe, for the pattern message.
|
|
196
|
-
* @returns The coerced value, or a plain-language message naming what is wrong.
|
|
197
|
-
*/
|
|
198
|
-
export function validateAnswer(
|
|
199
|
-
definition: VariableDefinition,
|
|
200
|
-
raw: string,
|
|
201
|
-
context: ValidationContext = {}
|
|
202
|
-
): AnswerValidation {
|
|
203
|
-
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
204
|
-
|
|
205
|
-
if (trimmed.length === 0 && !definition.required) {
|
|
206
|
-
return { ok: true, value: "" };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const result = validationSchemaFor(definition, context).safeParse(trimmed);
|
|
210
|
-
if (result.success) return { ok: true, value: result.data };
|
|
211
|
-
|
|
212
|
-
const first = result.error.issues[0];
|
|
213
|
-
const reason = first?.message ?? "is not valid";
|
|
214
|
-
return { ok: false, message: `${definition.name} ${reason}.` };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* The form an answer is STORED in. Env files hold text, so a coerced number or
|
|
219
|
-
* boolean goes back to its canonical string.
|
|
220
|
-
*
|
|
221
|
-
* @param value - The coerced answer.
|
|
222
|
-
*/
|
|
223
|
-
export function storedForm(value: AnswerValue): string {
|
|
224
|
-
return typeof value === "boolean" ? String(value) : String(value);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* A short, plain-language summary of a definition's constraints, shown beside
|
|
229
|
-
* the question and by `sous vars <name>`. Returns an empty array when the
|
|
230
|
-
* definition constrains nothing beyond its type.
|
|
231
|
-
*
|
|
232
|
-
* @param definition - The variable definition.
|
|
233
|
-
*/
|
|
234
|
-
export function constraintHints(definition: VariableDefinition): string[] {
|
|
235
|
-
const hints: string[] = [];
|
|
236
|
-
const rules = definition.validate;
|
|
237
|
-
|
|
238
|
-
hints.push(`type: ${definition.type}`);
|
|
239
|
-
if (definition.type === "enum" && rules?.enum !== undefined) {
|
|
240
|
-
hints.push(`one of: ${rules.enum.join(", ")}`);
|
|
241
|
-
}
|
|
242
|
-
if (rules?.minLength !== undefined) hints.push(`at least ${rules.minLength} characters`);
|
|
243
|
-
if (rules?.maxLength !== undefined) hints.push(`at most ${rules.maxLength} characters`);
|
|
244
|
-
if (rules?.min !== undefined) hints.push(`no less than ${rules.min}`);
|
|
245
|
-
if (rules?.max !== undefined) hints.push(`no more than ${rules.max}`);
|
|
246
|
-
if (rules?.pattern !== undefined) hints.push(`matching ${rules.pattern}`);
|
|
247
|
-
if (!definition.required) hints.push("optional");
|
|
248
|
-
|
|
249
|
-
return hints;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* A character count with the noun in the right number.
|
|
254
|
-
*
|
|
255
|
-
* @param count - How many characters.
|
|
256
|
-
*/
|
|
257
|
-
function characters(count: number): string {
|
|
258
|
-
return count === 1 ? "1 character" : `${count} characters`;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* One sentence per constraint, in plain words with the raw form the manifest
|
|
263
|
-
* declared in parentheses, so a reader can both understand the rule and find it
|
|
264
|
-
* in the recipe that published it. This is what the advanced view and
|
|
265
|
-
* `sous vars show` list under `@constraints`.
|
|
266
|
-
*
|
|
267
|
-
* @param definition - The variable definition.
|
|
268
|
-
* @returns One sentence per constraint, type first.
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* constraintBullets(definition);
|
|
272
|
-
* // -> ["must be a value of the type url (type: url)", "must match the pattern /^https/ (pattern: ^https)"]
|
|
273
|
-
*/
|
|
274
|
-
export function constraintBullets(definition: VariableDefinition): string[] {
|
|
275
|
-
const rules = definition.validate;
|
|
276
|
-
const options = rules?.enum;
|
|
277
|
-
|
|
278
|
-
// The type sentence never puts an article in front of the type name, because
|
|
279
|
-
// the article would have to change with the name ('a path', but 'an enum').
|
|
280
|
-
// An enum is described by the options it allows, which says more than the
|
|
281
|
-
// word 'enum' does and reads as one sentence rather than two.
|
|
282
|
-
const bullets: string[] =
|
|
283
|
-
definition.type === "enum" && options !== undefined
|
|
284
|
-
? [`must be one of: ${options.join(", ")} (type: enum)`]
|
|
285
|
-
: [`must be a value of the type ${definition.type} (type: ${definition.type})`];
|
|
286
|
-
|
|
287
|
-
if (options !== undefined && definition.type !== "enum") {
|
|
288
|
-
bullets.push(`must be one of: ${options.join(", ")} (enum: ${options.join(", ")})`);
|
|
289
|
-
}
|
|
290
|
-
if (rules?.minLength !== undefined) {
|
|
291
|
-
bullets.push(
|
|
292
|
-
`must be at least ${characters(rules.minLength)} long (minLength: ${rules.minLength})`
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
if (rules?.maxLength !== undefined) {
|
|
296
|
-
bullets.push(
|
|
297
|
-
`must be at most ${characters(rules.maxLength)} long (maxLength: ${rules.maxLength})`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
if (rules?.min !== undefined) {
|
|
301
|
-
bullets.push(`must be no less than ${rules.min} (min: ${rules.min})`);
|
|
302
|
-
}
|
|
303
|
-
if (rules?.max !== undefined) {
|
|
304
|
-
bullets.push(`must be no more than ${rules.max} (max: ${rules.max})`);
|
|
305
|
-
}
|
|
306
|
-
if (rules?.pattern !== undefined) {
|
|
307
|
-
bullets.push(`must match the pattern /${rules.pattern}/ (pattern: ${rules.pattern})`);
|
|
308
|
-
}
|
|
309
|
-
if (!definition.required) bullets.push("an answer is optional");
|
|
310
|
-
|
|
311
|
-
return bullets;
|
|
312
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What `sous --version` prints.
|
|
3
|
-
*
|
|
4
|
-
* oclif answers `--version` with its user agent string (package, version,
|
|
5
|
-
* platform and Node build on one line). Sous answers it itself, in bin/run.js,
|
|
6
|
-
* before oclif is loaded: the version alone, or with `--verbose` the same facts
|
|
7
|
-
* as a key and value list, so a person asking "which sous is this?" gets one
|
|
8
|
-
* short line and a person asking "where is it?" gets the rest.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from "node:fs";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import { log, showVariables } from "../utils/formatting.js";
|
|
14
|
-
|
|
15
|
-
/** The one flag oclif treats as a version request, and so does sous. */
|
|
16
|
-
export const VERSION_FLAG = "--version";
|
|
17
|
-
|
|
18
|
-
/** The flag that adds where the install is, the platform and the Node build. */
|
|
19
|
-
export const VERBOSE_FLAG = "--verbose";
|
|
20
|
-
|
|
21
|
-
/** The facts a version report is made of. */
|
|
22
|
-
export type VersionFacts = {
|
|
23
|
-
/** The npm package name. */
|
|
24
|
-
name: string;
|
|
25
|
-
/** The package version. */
|
|
26
|
-
version: string;
|
|
27
|
-
/** The absolute path of the install answering. */
|
|
28
|
-
root: string;
|
|
29
|
-
/** The platform and architecture, the way oclif spells them. */
|
|
30
|
-
platform: string;
|
|
31
|
-
/** The Node build, with its leading `v`. */
|
|
32
|
-
node: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** Whether the command line is a version request: `--version` as its first word. */
|
|
36
|
-
export function isVersionRequest(argv: readonly string[]): boolean {
|
|
37
|
-
return argv[0] === VERSION_FLAG;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Reads the facts about the install rooted at `packageRoot`. */
|
|
41
|
-
export function readVersionFacts(packageRoot: string): VersionFacts {
|
|
42
|
-
const pkg = JSON.parse(fs.readFileSync(`${packageRoot}/package.json`, "utf8")) as {
|
|
43
|
-
name?: unknown;
|
|
44
|
-
version?: unknown;
|
|
45
|
-
};
|
|
46
|
-
return {
|
|
47
|
-
name: typeof pkg.name === "string" ? pkg.name : "unknown",
|
|
48
|
-
version: typeof pkg.version === "string" ? pkg.version : "unknown",
|
|
49
|
-
root: packageRoot,
|
|
50
|
-
platform: `${os.platform()}-${os.arch()}`,
|
|
51
|
-
node: process.version,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Prints the report: the version on its own line, and with `verbose` the
|
|
57
|
-
* facts under it as a key and value list.
|
|
58
|
-
*
|
|
59
|
-
* @param facts - What to report.
|
|
60
|
-
* @param verbose - Whether to print the facts under the version.
|
|
61
|
-
* @param write - Where each line goes; standard output by default.
|
|
62
|
-
*/
|
|
63
|
-
export function printVersionReport(
|
|
64
|
-
facts: VersionFacts,
|
|
65
|
-
verbose: boolean,
|
|
66
|
-
write: (line: string) => void = log
|
|
67
|
-
): void {
|
|
68
|
-
write(`v${facts.version}`);
|
|
69
|
-
if (!verbose) return;
|
|
70
|
-
showVariables(
|
|
71
|
-
{
|
|
72
|
-
Package: facts.name,
|
|
73
|
-
Install: facts.root,
|
|
74
|
-
Platform: facts.platform,
|
|
75
|
-
Node: facts.node,
|
|
76
|
-
},
|
|
77
|
-
{ write }
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Answers a version request from the command line: `sous --version`, or
|
|
83
|
-
* `sous --version --verbose`.
|
|
84
|
-
*
|
|
85
|
-
* @param argv - The command line after the program name.
|
|
86
|
-
* @param packageRoot - The root of the install answering.
|
|
87
|
-
*/
|
|
88
|
-
export function printVersion(argv: readonly string[], packageRoot: string): void {
|
|
89
|
-
printVersionReport(readVersionFacts(packageRoot), argv.includes(VERBOSE_FLAG));
|
|
90
|
-
}
|
package/src/lib/watch-loop.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
CLI_ROOT,
|
|
4
|
-
resolveRootScope,
|
|
5
|
-
resolveWatchConfig,
|
|
6
|
-
type ConfigContext,
|
|
7
|
-
type Settings,
|
|
8
|
-
type WatchConfig,
|
|
9
|
-
} from "./settings.js";
|
|
10
|
-
import type { WatchHandle, WatchService } from "./watch-service.js";
|
|
11
|
-
import { readEffectiveLinks } from "./repos/links.js";
|
|
12
|
-
import { log } from "../utils/formatting.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Builds a WatchConfig from current settings, injecting the primary config
|
|
16
|
-
* file, the conf.d/ drop-in DIRECTORY, the templating directory and every linked
|
|
17
|
-
* repository's working copy into fullRebuildPaths. Watching a directory (not
|
|
18
|
-
* each file inside it) covers files appearing, changing, or disappearing at
|
|
19
|
-
* runtime, since full-rebuild matching is exact-or-directory-prefix.
|
|
20
|
-
*
|
|
21
|
-
* A linked repository is watched because that is the whole point of a link: the
|
|
22
|
-
* recipes are being edited right now, in that checkout, and a watch that ignored
|
|
23
|
-
* them would make the link useless.
|
|
24
|
-
*
|
|
25
|
-
* Shared by `build --watch` and `compile --watch` so both react to config,
|
|
26
|
-
* template and linked-recipe edits identically.
|
|
27
|
-
*/
|
|
28
|
-
export function buildReloadWatchConfig(
|
|
29
|
-
settings: Settings,
|
|
30
|
-
configContext: ConfigContext
|
|
31
|
-
): WatchConfig {
|
|
32
|
-
const rootScope = resolveRootScope(settings, configContext);
|
|
33
|
-
const config = resolveWatchConfig(settings, rootScope);
|
|
34
|
-
const links = readEffectiveLinks(configContext.sousDir);
|
|
35
|
-
const reloadPaths = [
|
|
36
|
-
configContext.configPath,
|
|
37
|
-
configContext.confDir,
|
|
38
|
-
path.join(CLI_ROOT, "src", "templating"),
|
|
39
|
-
...Object.values(links).map((link) => link.path),
|
|
40
|
-
].filter((p): p is string => typeof p === "string");
|
|
41
|
-
config.fullRebuildPaths = [...(config.fullRebuildPaths ?? []), ...reloadPaths];
|
|
42
|
-
return config;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Options for {@link startConfigReloadWatch}. */
|
|
46
|
-
export type ConfigReloadWatchOptions = {
|
|
47
|
-
/** The watcher factory used to (re)start chokidar. */
|
|
48
|
-
watchService: WatchService;
|
|
49
|
-
/**
|
|
50
|
-
* Produces a fresh WatchConfig from the CURRENT settings each time a watcher
|
|
51
|
-
* is (re)started; typically `() => buildReloadWatchConfig(this.settings, this.configContext)`.
|
|
52
|
-
*/
|
|
53
|
-
buildWatchConfig: () => WatchConfig;
|
|
54
|
-
/**
|
|
55
|
-
* Performs the actual work (build/compile) using the command's CURRENT
|
|
56
|
-
* settings. Called for partial rebuilds (with the changed file) and, after a
|
|
57
|
-
* successful reload, for full rebuilds (no argument). Owns its own
|
|
58
|
-
* heading/footer output.
|
|
59
|
-
*/
|
|
60
|
-
rebuild: (changedFile?: string) => Promise<void>;
|
|
61
|
-
/**
|
|
62
|
-
* Re-runs discovery + settings load and commits the result onto the command,
|
|
63
|
-
* but only if it loads cleanly (last-good semantics). Throws on failure.
|
|
64
|
-
*/
|
|
65
|
-
reloadConfig: () => Promise<void>;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** The running watch loop; exposes the live handle and a manual full-rebuild trigger. */
|
|
69
|
-
export type ConfigReloadWatchController = {
|
|
70
|
-
/** Mutable reference to the live watcher handle (swapped on every restart). */
|
|
71
|
-
handle: { current: WatchHandle | null };
|
|
72
|
-
/** Triggers a full rebuild immediately, bypassing the debounce (e.g. a keypress). */
|
|
73
|
-
triggerFullRebuild: (reason: string) => Promise<void>;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Runs the shared watch loop used by `build --watch` and `compile --watch`.
|
|
78
|
-
*
|
|
79
|
-
* - Partial events (a watched source file changed) call `rebuild(filePath)`.
|
|
80
|
-
* - Full events (config file, conf.d directory, or templating dir changed) stop
|
|
81
|
-
* the watcher, reload settings via `reloadConfig`, rebuild, and restart the
|
|
82
|
-
* watcher. A failed reload is reported without wedging the session: the
|
|
83
|
-
* last-good config stays in place and the watcher restarts so the next edit
|
|
84
|
-
* can recover.
|
|
85
|
-
*
|
|
86
|
-
* A single `isRebuilding` guard serialises overlapping triggers.
|
|
87
|
-
*/
|
|
88
|
-
export function startConfigReloadWatch(
|
|
89
|
-
options: ConfigReloadWatchOptions
|
|
90
|
-
): ConfigReloadWatchController {
|
|
91
|
-
const { watchService, buildWatchConfig, rebuild, reloadConfig } = options;
|
|
92
|
-
|
|
93
|
-
let isRebuilding = false;
|
|
94
|
-
const handle: { current: WatchHandle | null } = { current: null };
|
|
95
|
-
|
|
96
|
-
const startWatcher = () => {
|
|
97
|
-
const watchConfig = buildWatchConfig();
|
|
98
|
-
handle.current = watchService.watch(watchConfig, async (event) => {
|
|
99
|
-
if (event.type === "partial") {
|
|
100
|
-
if (isRebuilding) return;
|
|
101
|
-
isRebuilding = true;
|
|
102
|
-
log(`\nChange detected: ${event.filePath}`);
|
|
103
|
-
await rebuild(event.filePath);
|
|
104
|
-
isRebuilding = false;
|
|
105
|
-
} else {
|
|
106
|
-
// Full rebuild: stop current watcher, reload settings, restart
|
|
107
|
-
if (isRebuilding) return;
|
|
108
|
-
isRebuilding = true;
|
|
109
|
-
log(`\nConfig changed (${event.filePath}), reloading settings and restarting watcher...`);
|
|
110
|
-
await handle.current!.stop();
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
// Re-run discovery: conf.d layer files can appear or disappear while
|
|
114
|
-
// watching, so the ordered layer list must be rebuilt (and the
|
|
115
|
-
// duplicate-baseName check re-run) before reloading settings. Only
|
|
116
|
-
// committed by reloadConfig once it loads cleanly.
|
|
117
|
-
await reloadConfig();
|
|
118
|
-
await rebuild();
|
|
119
|
-
} catch (error) {
|
|
120
|
-
// A broken config edit (bad JSON/JS, colliding conf.d baseNames,
|
|
121
|
-
// configure() throw, etc.) must not wedge the session: report it,
|
|
122
|
-
// keep the last-good config, and fall through to restart the watcher
|
|
123
|
-
// so the next edit can recover.
|
|
124
|
-
log(
|
|
125
|
-
`\nConfig reload failed; keeping the last-good config. Fix the config and save again to retry.\n ${
|
|
126
|
-
error instanceof Error ? error.message : String(error)
|
|
127
|
-
}`
|
|
128
|
-
);
|
|
129
|
-
} finally {
|
|
130
|
-
isRebuilding = false;
|
|
131
|
-
startWatcher();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
startWatcher();
|
|
138
|
-
|
|
139
|
-
const triggerFullRebuild = async (reason: string) => {
|
|
140
|
-
if (isRebuilding) return;
|
|
141
|
-
isRebuilding = true;
|
|
142
|
-
log(`\n${reason}`);
|
|
143
|
-
await rebuild();
|
|
144
|
-
isRebuilding = false;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return { handle, triggerFullRebuild };
|
|
148
|
-
}
|
package/src/lib/watch-service.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import chokidar from "chokidar";
|
|
2
|
-
import { minimatch } from "minimatch";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { WatchConfig } from "./settings.js";
|
|
5
|
-
import { toGlobPattern } from "./glob-pattern.js";
|
|
6
|
-
import { log } from "../utils/formatting.js";
|
|
7
|
-
|
|
8
|
-
/** Milliseconds to wait after a change before triggering a rebuild. */
|
|
9
|
-
const DEBOUNCE_MS = 300;
|
|
10
|
-
|
|
11
|
-
/** A partial rebuild triggered by a change to a specific source file. */
|
|
12
|
-
export type PartialRebuildEvent = {
|
|
13
|
-
type: "partial";
|
|
14
|
-
filePath: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** A full rebuild triggered by a change to a high-impact path (config, templating, etc.). */
|
|
18
|
-
export type FullRebuildEvent = {
|
|
19
|
-
type: "full";
|
|
20
|
-
reason: string;
|
|
21
|
-
filePath: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type WatchEvent = PartialRebuildEvent | FullRebuildEvent;
|
|
25
|
-
|
|
26
|
-
/** A handle returned by WatchService.watch() that allows the caller to stop the watcher. */
|
|
27
|
-
export type WatchHandle = {
|
|
28
|
-
stop(): Promise<void>;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Extracts the longest leading non-glob segment of a pattern as a watchable
|
|
33
|
-
* directory path. Chokidar v5 does not accept glob patterns directly.
|
|
34
|
-
*
|
|
35
|
-
* Examples:
|
|
36
|
-
* "/foo/bar/**\/*" → "/foo/bar"
|
|
37
|
-
* "/foo/bar/*.md" → "/foo/bar"
|
|
38
|
-
*/
|
|
39
|
-
function globBaseDir(pattern: string): string {
|
|
40
|
-
const starIndex = pattern.indexOf("*");
|
|
41
|
-
if (starIndex === -1) return path.dirname(pattern);
|
|
42
|
-
const prefix = pattern.slice(0, starIndex);
|
|
43
|
-
return prefix.endsWith("/") || prefix.endsWith(path.sep)
|
|
44
|
-
? prefix.slice(0, -1)
|
|
45
|
-
: path.dirname(prefix);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Watches exact files, glob-backed directories, and full-rebuild paths,
|
|
50
|
-
* calling the provided callback (debounced) when a relevant change is detected.
|
|
51
|
-
*
|
|
52
|
-
* - Changes to `files` or `globs` entries fire a partial rebuild event.
|
|
53
|
-
* - Changes to `fullRebuildPaths` entries fire a full rebuild event.
|
|
54
|
-
*
|
|
55
|
-
* Returns a WatchHandle with a `stop()` method to close the watcher.
|
|
56
|
-
*/
|
|
57
|
-
export class WatchService {
|
|
58
|
-
watch(config: WatchConfig, onChange: (event: WatchEvent) => Promise<void>): WatchHandle {
|
|
59
|
-
const { files, globs, fullRebuildPaths = [] } = config;
|
|
60
|
-
const globBaseDirs = globs.map(globBaseDir);
|
|
61
|
-
const watchPaths = [...new Set([...files, ...globBaseDirs, ...fullRebuildPaths])];
|
|
62
|
-
|
|
63
|
-
const watcher = chokidar.watch(watchPaths, {
|
|
64
|
-
ignoreInitial: true,
|
|
65
|
-
persistent: true,
|
|
66
|
-
ignored: /sous\.state\.json$/,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
let debounceTimer: NodeJS.Timeout | null = null;
|
|
70
|
-
let pendingEvent: WatchEvent | null = null;
|
|
71
|
-
|
|
72
|
-
const schedule = (event: WatchEvent) => {
|
|
73
|
-
pendingEvent = event;
|
|
74
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
75
|
-
debounceTimer = setTimeout(async () => {
|
|
76
|
-
debounceTimer = null;
|
|
77
|
-
const evt = pendingEvent!;
|
|
78
|
-
pendingEvent = null;
|
|
79
|
-
try {
|
|
80
|
-
await onChange(evt);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
-
log(` Watch error: ${message}`);
|
|
84
|
-
}
|
|
85
|
-
}, DEBOUNCE_MS);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
watcher.on("all", (event, filePath) => {
|
|
89
|
-
if (!["add", "change", "unlink"].includes(event)) return;
|
|
90
|
-
|
|
91
|
-
// Full-rebuild paths take priority
|
|
92
|
-
const isFullRebuildPath = fullRebuildPaths.some(p =>
|
|
93
|
-
filePath === p || filePath.startsWith(p + path.sep) || filePath.startsWith(p + "/")
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (isFullRebuildPath) {
|
|
97
|
-
schedule({ type: "full", reason: filePath, filePath });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const isExactFile = files.includes(filePath);
|
|
102
|
-
const matchesGlob = globs.some(g => minimatch(filePath, toGlobPattern(g)));
|
|
103
|
-
|
|
104
|
-
if (isExactFile || matchesGlob) {
|
|
105
|
-
schedule({ type: "partial", filePath });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const totalPatterns = files.length + globs.length + fullRebuildPaths.length;
|
|
110
|
-
log(`\nWatching ${totalPatterns} pattern(s) for changes...\n`);
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
stop: () => watcher.close(),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|