@sous-io/sous 0.1.1 → 0.2.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 +115 -35
- package/bin/run.js +10 -1
- package/docs/markdown/README.md +27 -0
- package/docs/markdown/_sidebar.md +18 -0
- package/docs/markdown/commands.md +308 -0
- package/docs/markdown/config-discovery.md +74 -0
- package/docs/markdown/config-inspection.md +69 -0
- package/docs/markdown/config-layers.md +92 -0
- package/docs/markdown/config-variables.md +79 -0
- package/docs/markdown/configuration.md +71 -0
- package/docs/markdown/design-principles.md +59 -0
- package/docs/markdown/repositories-authoring.md +409 -0
- package/docs/markdown/repositories-consuming.md +580 -0
- package/docs/markdown/repositories-file-formats.md +1084 -0
- package/docs/markdown/repositories-variables.md +387 -0
- package/docs/markdown/repositories.md +303 -0
- package/docs/markdown/skill-categories.md +58 -0
- package/package.json +72 -8
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
- package/sous.config.schema.json +337 -0
- package/src/base-command.ts +220 -67
- package/src/commands/build.ts +150 -73
- package/src/commands/clear.ts +23 -15
- package/src/commands/compile.ts +74 -16
- package/src/commands/config/get.ts +110 -0
- package/src/commands/config/show.ts +32 -0
- package/src/commands/config/validate.ts +53 -0
- package/src/commands/help.ts +46 -0
- package/src/commands/launch.ts +36 -14
- package/src/commands/lock/rebuild.ts +241 -0
- package/src/commands/lock/show.ts +115 -0
- package/src/commands/namespace/list.ts +117 -0
- package/src/commands/namespace/show.ts +110 -0
- package/src/commands/prune.ts +3 -11
- package/src/commands/recipe/list.ts +95 -0
- package/src/commands/recipe/show.ts +301 -0
- package/src/commands/repo/add.ts +145 -0
- package/src/commands/repo/gc.ts +172 -0
- package/src/commands/repo/init.ts +136 -0
- package/src/commands/repo/link.ts +500 -0
- package/src/commands/repo/list.ts +179 -0
- package/src/commands/repo/release.ts +625 -0
- package/src/commands/repo/remove.ts +193 -0
- package/src/commands/repo/search.ts +189 -0
- package/src/commands/repo/submit.ts +133 -0
- package/src/commands/repo/unlink.ts +147 -0
- package/src/commands/subscription/add.ts +285 -0
- package/src/commands/subscription/list.ts +129 -0
- package/src/commands/subscription/remove.ts +181 -0
- package/src/commands/vars/ask.ts +374 -0
- package/src/commands/vars/index.ts +79 -0
- package/src/commands/vars/list.ts +67 -0
- package/src/commands/vars/show.ts +77 -0
- package/src/config-command.ts +30 -0
- package/src/lib/build-service.ts +206 -54
- package/src/lib/config-discovery.ts +220 -27
- package/src/lib/config-inspect.ts +145 -0
- package/src/lib/config-kernel.mjs +377 -0
- package/src/lib/config-schema.ts +361 -0
- package/src/lib/env-file.ts +328 -0
- package/src/lib/env-local.ts +18 -1
- package/src/lib/errors.ts +32 -0
- package/src/lib/include-resolver.ts +108 -15
- package/src/lib/interactive.ts +165 -0
- package/src/lib/markdown-compiler.ts +118 -37
- package/src/lib/package-info.ts +25 -0
- package/src/lib/pid-service.ts +32 -21
- package/src/lib/refs/find.ts +589 -0
- package/src/lib/refs/index.ts +12 -0
- package/src/lib/refs/pick.ts +147 -0
- package/src/lib/refs/scopes.ts +61 -0
- package/src/lib/repos/catalog-display.ts +116 -0
- package/src/lib/repos/catalog-inputs.ts +160 -0
- package/src/lib/repos/catalog.ts +722 -0
- package/src/lib/repos/core-recipe.ts +105 -0
- package/src/lib/repos/defaults.ts +175 -0
- package/src/lib/repos/formats/common.ts +389 -0
- package/src/lib/repos/formats/index-file.ts +215 -0
- package/src/lib/repos/formats/links-map.ts +96 -0
- package/src/lib/repos/formats/lockfile.ts +167 -0
- package/src/lib/repos/formats/patterns.ts +57 -0
- package/src/lib/repos/formats/recipe-manifest.ts +395 -0
- package/src/lib/repos/formats/repo-manifest.ts +88 -0
- package/src/lib/repos/formats/store-entry.ts +84 -0
- package/src/lib/repos/freshness.ts +208 -0
- package/src/lib/repos/git-clone.ts +312 -0
- package/src/lib/repos/identity.ts +89 -0
- package/src/lib/repos/index.ts +58 -0
- package/src/lib/repos/links.ts +353 -0
- package/src/lib/repos/load-manifest.ts +236 -0
- package/src/lib/repos/lock-service.ts +453 -0
- package/src/lib/repos/locked-namespace-resolver.ts +90 -0
- package/src/lib/repos/locked-recipes.ts +254 -0
- package/src/lib/repos/managed-layer.ts +422 -0
- package/src/lib/repos/namespace-resolver.ts +370 -0
- package/src/lib/repos/providers/base.ts +206 -0
- package/src/lib/repos/providers/git.ts +233 -0
- package/src/lib/repos/providers/github.ts +294 -0
- package/src/lib/repos/providers/gitlab.ts +263 -0
- package/src/lib/repos/providers/http.ts +102 -0
- package/src/lib/repos/providers/index-cache.ts +382 -0
- package/src/lib/repos/providers/index.ts +106 -0
- package/src/lib/repos/providers/local.ts +391 -0
- package/src/lib/repos/providers/provider.ts +401 -0
- package/src/lib/repos/recipe-config-layers.ts +287 -0
- package/src/lib/repos/recipe-targets.ts +223 -0
- package/src/lib/repos/ref-search.ts +46 -0
- package/src/lib/repos/ref.ts +513 -0
- package/src/lib/repos/reference-report.ts +122 -0
- package/src/lib/repos/release/bump.ts +161 -0
- package/src/lib/repos/release/git-state.ts +305 -0
- package/src/lib/repos/release/index-builder.ts +635 -0
- package/src/lib/repos/release/index.ts +16 -0
- package/src/lib/repos/release/plan.ts +512 -0
- package/src/lib/repos/release/submit-service.ts +496 -0
- package/src/lib/repos/release/tags.ts +243 -0
- package/src/lib/repos/release/validate.ts +463 -0
- package/src/lib/repos/resolver.ts +789 -0
- package/src/lib/repos/scaffold/index.ts +238 -0
- package/src/lib/repos/scaffold/templates.ts +415 -0
- package/src/lib/repos/seed.ts +414 -0
- package/src/lib/repos/store/contract.ts +64 -0
- package/src/lib/repos/store/hash.ts +114 -0
- package/src/lib/repos/store/recipe-store.ts +599 -0
- package/src/lib/repos/store/settings.ts +58 -0
- package/src/lib/repos/subscription-service.ts +2678 -0
- package/src/lib/repos/trust.ts +447 -0
- package/src/lib/settings.ts +546 -189
- package/src/lib/sous-home.ts +104 -0
- package/src/lib/state.ts +52 -20
- package/src/lib/vars/ask.ts +1152 -0
- package/src/lib/vars/definition-source.ts +252 -0
- package/src/lib/vars/display.ts +233 -0
- package/src/lib/vars/index.ts +18 -0
- package/src/lib/vars/ladder.ts +282 -0
- package/src/lib/vars/mappings.ts +265 -0
- package/src/lib/vars/names.ts +94 -0
- package/src/lib/vars/preanswers.ts +395 -0
- package/src/lib/vars/question-plan.ts +218 -0
- package/src/lib/vars/report.ts +228 -0
- package/src/lib/vars/safe-regex.ts +235 -0
- package/src/lib/vars/validate.ts +312 -0
- package/src/lib/watch-loop.ts +148 -0
- package/src/templating/init-liquid-engine.ts +58 -16
- package/src/utils/choice-prompt.ts +143 -0
- package/src/utils/command-errors.ts +186 -0
- package/src/utils/command-help.ts +45 -0
- package/src/utils/confirm-prompt.ts +110 -0
- package/src/utils/flags.ts +153 -0
- package/src/utils/formatting.ts +540 -55
- package/src/utils/prompts.ts +35 -1
- package/src/utils/sous-directory.ts +245 -0
- package/src/utils/table.ts +603 -0
- package/src/utils/value-prompt.ts +119 -0
- package/shared-prompts/_partials/resume-task.md +0 -51
- package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
- package/shared-prompts/_partials/update-task-file.md +0 -52
- package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
- package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
- package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
- package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
- package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
- package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
- package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
- package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
- package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
- package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
- package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
- package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
- package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two variable reports the `sous vars` commands print.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than inside a command so that `vars list`, `vars show`
|
|
5
|
+
* and the bare `sous vars` all print exactly the same thing: the listing is one
|
|
6
|
+
* function and the detail is another, and each command only decides which one
|
|
7
|
+
* to call.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { ConfigError } from "../errors.js";
|
|
12
|
+
import {
|
|
13
|
+
definedVariableKey,
|
|
14
|
+
definingRecipeKey,
|
|
15
|
+
type DefinedVariable,
|
|
16
|
+
} from "./definition-source.js";
|
|
17
|
+
import {
|
|
18
|
+
displayValue,
|
|
19
|
+
documentationRows,
|
|
20
|
+
renderFacts,
|
|
21
|
+
variableFacts,
|
|
22
|
+
} from "./display.js";
|
|
23
|
+
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
24
|
+
import {
|
|
25
|
+
diagnoseVariable,
|
|
26
|
+
RUNG_LABELS,
|
|
27
|
+
SOURCE_LABELS,
|
|
28
|
+
type LadderContext,
|
|
29
|
+
} from "./ladder.js";
|
|
30
|
+
import { validateAnswer } from "./validate.js";
|
|
31
|
+
import { answerFileFor } from "./ask.js";
|
|
32
|
+
import { bareName } from "./names.js";
|
|
33
|
+
import {
|
|
34
|
+
blankLine,
|
|
35
|
+
heading,
|
|
36
|
+
indent,
|
|
37
|
+
log,
|
|
38
|
+
paragraph,
|
|
39
|
+
showVariables,
|
|
40
|
+
subheading,
|
|
41
|
+
terminalColumns,
|
|
42
|
+
} from "../../utils/formatting.js";
|
|
43
|
+
|
|
44
|
+
/** How far every line of these reports is indented. */
|
|
45
|
+
const INDENT = 2;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The columns the listing shows. The variable and the value answering it are
|
|
49
|
+
* what the reader came for, so they stay however narrow the terminal is. An
|
|
50
|
+
* environment variable name is cut at the front, because the tail of
|
|
51
|
+
* `SOUS_VAR_LOCAL_QUESTIONS_API_URL` is the part that identifies it.
|
|
52
|
+
*/
|
|
53
|
+
const LIST_COLUMNS: TableColumn[] = [
|
|
54
|
+
{ key: "name", header: "Variable", overflow: "truncate", minWidth: 8 },
|
|
55
|
+
{
|
|
56
|
+
key: "recipe",
|
|
57
|
+
header: "Recipe",
|
|
58
|
+
kind: "path",
|
|
59
|
+
overflow: "truncate",
|
|
60
|
+
priority: "medium",
|
|
61
|
+
minWidth: 8,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "envName",
|
|
65
|
+
header: "Answered by",
|
|
66
|
+
overflow: "truncate",
|
|
67
|
+
truncate: "start",
|
|
68
|
+
priority: "medium",
|
|
69
|
+
minWidth: 11,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "value",
|
|
73
|
+
header: "Value",
|
|
74
|
+
overflow: "truncate",
|
|
75
|
+
truncate: "end",
|
|
76
|
+
flex: 1,
|
|
77
|
+
minWidth: 10,
|
|
78
|
+
},
|
|
79
|
+
{ key: "source", header: "Source", priority: "low" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** The columns the detail report's resolution ladder shows. */
|
|
83
|
+
const LADDER_COLUMNS: TableColumn[] = [
|
|
84
|
+
{ key: "envName", header: "Environment variable", overflow: "truncate", truncate: "start" },
|
|
85
|
+
{ key: "rung", header: "Rung", priority: "medium" },
|
|
86
|
+
{ key: "status", header: "Status", flex: 1, priority: "medium" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prints the table of every variable in play, sorted by recipe then name.
|
|
91
|
+
*
|
|
92
|
+
* @param defined - Every variable the project's recipes (or a file) define.
|
|
93
|
+
* @param context - The resolution ladder to read answers from.
|
|
94
|
+
*/
|
|
95
|
+
export function printVariableList(
|
|
96
|
+
defined: DefinedVariable[],
|
|
97
|
+
context: LadderContext
|
|
98
|
+
): void {
|
|
99
|
+
heading("Variables in play");
|
|
100
|
+
|
|
101
|
+
if (defined.length === 0) {
|
|
102
|
+
blankLine();
|
|
103
|
+
paragraph(
|
|
104
|
+
"No recipe in this project defines any variables yet. Subscribe to a recipe " +
|
|
105
|
+
"that publishes some, or read a definitions file with --file."
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rows = [...defined]
|
|
111
|
+
.sort((left, right) => definedVariableKey(left).localeCompare(definedVariableKey(right)))
|
|
112
|
+
.map((entry) => {
|
|
113
|
+
const { resolved } = diagnoseVariable(entry, context);
|
|
114
|
+
const value = displayValue(resolved?.value, entry.definition.secret);
|
|
115
|
+
const source =
|
|
116
|
+
resolved === undefined
|
|
117
|
+
? "nothing yet"
|
|
118
|
+
: `${RUNG_LABELS[resolved.source.rung]}, ${
|
|
119
|
+
SOURCE_LABELS[resolved.source.file] ?? resolved.source.file
|
|
120
|
+
}`;
|
|
121
|
+
return {
|
|
122
|
+
name: entry.definition.name,
|
|
123
|
+
recipe: definingRecipeKey(entry.recipe),
|
|
124
|
+
envName: resolved?.source.envName ?? "",
|
|
125
|
+
value,
|
|
126
|
+
source,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
blankLine();
|
|
131
|
+
for (const line of renderTable(LIST_COLUMNS, rows, { indent: INDENT })) {
|
|
132
|
+
log(indent(line, INDENT));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What the detail report needs beyond the definitions themselves. */
|
|
137
|
+
export interface VariableDetailOptions {
|
|
138
|
+
/**
|
|
139
|
+
* The project's `.sous/` directory, so the storage path can be shown in full.
|
|
140
|
+
* Without it only the env file's name is shown.
|
|
141
|
+
*/
|
|
142
|
+
sousDir?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Prints everything about one variable: the question, the documentation, the
|
|
147
|
+
* labeled facts (the same block the advanced view of a question prints, from
|
|
148
|
+
* the same renderer), every environment variable name on its resolution ladder,
|
|
149
|
+
* and which rung actually answered.
|
|
150
|
+
*
|
|
151
|
+
* @param defined - Every variable the project's recipes (or a file) define.
|
|
152
|
+
* @param context - The resolution ladder to read answers from.
|
|
153
|
+
* @param name - The variable's name, or its `namespace/recipe.name` key.
|
|
154
|
+
* @param options - Where the project's `.sous/` directory is.
|
|
155
|
+
*/
|
|
156
|
+
export function printVariableDetail(
|
|
157
|
+
defined: DefinedVariable[],
|
|
158
|
+
context: LadderContext,
|
|
159
|
+
name: string,
|
|
160
|
+
options: VariableDetailOptions = {}
|
|
161
|
+
): void {
|
|
162
|
+
const matches = defined.filter(
|
|
163
|
+
(entry) => entry.definition.name === name || definedVariableKey(entry) === name
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (matches.length === 0) {
|
|
167
|
+
throw new ConfigError(
|
|
168
|
+
`No variable named '${name}' is in play.\n` +
|
|
169
|
+
` Run 'sous vars list' to see every variable this project's recipes define.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const entry of matches) {
|
|
174
|
+
const { definition } = entry;
|
|
175
|
+
const { resolved, candidates } = diagnoseVariable(entry, context);
|
|
176
|
+
const validity =
|
|
177
|
+
resolved === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: validateAnswer(definition, resolved.value, {
|
|
180
|
+
recipe: definingRecipeKey(entry.recipe),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const file = answerFileFor(definition);
|
|
184
|
+
|
|
185
|
+
heading(`${definition.name} (${definingRecipeKey(entry.recipe)})`);
|
|
186
|
+
blankLine();
|
|
187
|
+
showVariables({
|
|
188
|
+
Question: definition.prompt,
|
|
189
|
+
...documentationRows(definition),
|
|
190
|
+
Recipe: `${definingRecipeKey(entry.recipe)} version ${entry.recipe.version} from ${entry.recipe.repo}`,
|
|
191
|
+
"Stored in": file,
|
|
192
|
+
Value: displayValue(resolved?.value, definition.secret),
|
|
193
|
+
Answer:
|
|
194
|
+
resolved === undefined
|
|
195
|
+
? "nothing in scope answers this variable yet"
|
|
196
|
+
: validity?.ok === true
|
|
197
|
+
? "the value in scope fits this definition"
|
|
198
|
+
: `the value in scope does not fit: ${validity?.ok === false ? validity.message : ""}`,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
blankLine();
|
|
202
|
+
const facts = variableFacts({
|
|
203
|
+
defined: entry,
|
|
204
|
+
storagePath:
|
|
205
|
+
options.sousDir === undefined ? file : path.join(options.sousDir, file),
|
|
206
|
+
storedAs: resolved?.source.envName ?? bareName(definition),
|
|
207
|
+
});
|
|
208
|
+
// renderFacts indents the block itself, so every facts block in the CLI
|
|
209
|
+
// sits at the same depth.
|
|
210
|
+
for (const line of renderFacts(facts, terminalColumns())) log(line);
|
|
211
|
+
|
|
212
|
+
blankLine();
|
|
213
|
+
subheading("Environment variables sous looks at, most specific first");
|
|
214
|
+
blankLine();
|
|
215
|
+
|
|
216
|
+
const rows = candidates.map((candidate) => ({
|
|
217
|
+
envName: candidate.envName,
|
|
218
|
+
rung: RUNG_LABELS[candidate.rung],
|
|
219
|
+
status:
|
|
220
|
+
candidate.envName === resolved?.source.envName
|
|
221
|
+
? `answered it, from ${SOURCE_LABELS[resolved.source.file] ?? resolved.source.file}`
|
|
222
|
+
: "not set",
|
|
223
|
+
}));
|
|
224
|
+
for (const line of renderTable(LADDER_COLUMNS, rows, { indent: INDENT })) {
|
|
225
|
+
log(indent(line, INDENT));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a recipe's regular expression under a time budget.
|
|
3
|
+
*
|
|
4
|
+
* A recipe publishes `validate.pattern` as a plain string, and a consuming
|
|
5
|
+
* project runs it against whatever the person answering types. A pattern with
|
|
6
|
+
* catastrophic backtracking in it (the classic `(a+)+$`) can take effectively
|
|
7
|
+
* forever on a short input, which would hang `sous subscription add` or
|
|
8
|
+
* `sous vars ask` with no way out. So sous never runs a published pattern on
|
|
9
|
+
* its own thread: the match happens inside a worker, and the caller waits only
|
|
10
|
+
* for a fixed budget before giving up on it.
|
|
11
|
+
*
|
|
12
|
+
* The API is SYNCHRONOUS because every caller of `validateAnswer` is
|
|
13
|
+
* synchronous, and the whole variables layer would have to become async to
|
|
14
|
+
* change that. The workable synchronous shape is a worker plus a
|
|
15
|
+
* `SharedArrayBuffer`: the caller posts the work, blocks in `Atomics.wait` for
|
|
16
|
+
* at most the budget, and reads the answer the worker stored in shared memory.
|
|
17
|
+
* The worker thread keeps running while the calling thread is parked, so the
|
|
18
|
+
* result arrives without an event loop turn on this side.
|
|
19
|
+
*
|
|
20
|
+
* COMPILING a pattern is not budgeted, and does not need to be: V8 compiles a
|
|
21
|
+
* regular expression in time proportional to its source text and has no
|
|
22
|
+
* pathological compile step, so the `new RegExp(...)` validity check that
|
|
23
|
+
* `recipe-manifest.ts` runs at parse time is safe as it stands. Only the MATCH
|
|
24
|
+
* can run away.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { Worker } from "node:worker_threads";
|
|
28
|
+
|
|
29
|
+
/** How long a published pattern may run before sous stops waiting for it. */
|
|
30
|
+
export const DEFAULT_PATTERN_BUDGET_MS = 100;
|
|
31
|
+
|
|
32
|
+
/** What running a pattern against an input produced. */
|
|
33
|
+
export type MatchOutcome = "match" | "no-match" | "timeout";
|
|
34
|
+
|
|
35
|
+
/** How long a worker is given to come online before it is written off. */
|
|
36
|
+
const STARTUP_GRACE_MS = 5_000;
|
|
37
|
+
|
|
38
|
+
/** Slot 0 of the shared array: the outcome the worker wrote. */
|
|
39
|
+
const STATUS = 0;
|
|
40
|
+
|
|
41
|
+
/** Slot 1 of the shared array: set once the worker has the match in hand. */
|
|
42
|
+
const STARTED = 1;
|
|
43
|
+
|
|
44
|
+
/** The status words the worker writes into shared memory. */
|
|
45
|
+
const PENDING = 0;
|
|
46
|
+
const MATCHED = 1;
|
|
47
|
+
const DID_NOT_MATCH = 2;
|
|
48
|
+
const FAILED = 3;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The part of a worker this module uses. Narrow on purpose, so a test can hand
|
|
52
|
+
* in a stand-in (or refuse to make one) without constructing a real thread.
|
|
53
|
+
*/
|
|
54
|
+
export interface MatcherWorker {
|
|
55
|
+
/** Delivers one match request to the worker thread. */
|
|
56
|
+
postMessage(message: unknown): void;
|
|
57
|
+
/** Stops the worker, including one that is stuck inside a runaway match. */
|
|
58
|
+
terminate(): unknown;
|
|
59
|
+
/** Lets the process exit while this worker is idle. */
|
|
60
|
+
unref?(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Makes the worker that runs patterns. Returning `undefined` means workers are
|
|
65
|
+
* not available here, which sends `matchWithBudget` down its in-process
|
|
66
|
+
* fallback path.
|
|
67
|
+
*/
|
|
68
|
+
export type MatcherWorkerFactory = () => MatcherWorker | undefined;
|
|
69
|
+
|
|
70
|
+
/** The source the default worker runs. */
|
|
71
|
+
const WORKER_SOURCE = `
|
|
72
|
+
const { parentPort } = require("node:worker_threads");
|
|
73
|
+
parentPort.on("message", (task) => {
|
|
74
|
+
const slots = new Int32Array(task.shared);
|
|
75
|
+
Atomics.store(slots, ${STARTED}, 1);
|
|
76
|
+
Atomics.notify(slots, ${STARTED});
|
|
77
|
+
let code = ${FAILED};
|
|
78
|
+
try {
|
|
79
|
+
code = new RegExp(task.pattern).test(task.input) ? ${MATCHED} : ${DID_NOT_MATCH};
|
|
80
|
+
} catch {
|
|
81
|
+
code = ${FAILED};
|
|
82
|
+
}
|
|
83
|
+
Atomics.store(slots, ${STATUS}, code);
|
|
84
|
+
Atomics.notify(slots, ${STATUS});
|
|
85
|
+
});
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Starts the real worker. The source is passed inline (`eval: true`) rather
|
|
90
|
+
* than as a file so the worker needs no TypeScript loader of its own, and it is
|
|
91
|
+
* unreferenced so an idle matcher never holds the process open.
|
|
92
|
+
*/
|
|
93
|
+
const defaultFactory: MatcherWorkerFactory = () => {
|
|
94
|
+
const worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
95
|
+
worker.unref();
|
|
96
|
+
return worker;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let factory: MatcherWorkerFactory = defaultFactory;
|
|
100
|
+
let worker: MatcherWorker | undefined;
|
|
101
|
+
let workersUnavailable = false;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Replaces the worker factory, for tests. Pass nothing to restore the real one.
|
|
105
|
+
*
|
|
106
|
+
* @param replacement - The factory to use, or `undefined` to restore the default.
|
|
107
|
+
*/
|
|
108
|
+
export function setMatcherWorkerFactory(replacement?: MatcherWorkerFactory): void {
|
|
109
|
+
disposeMatcherWorker();
|
|
110
|
+
factory = replacement ?? defaultFactory;
|
|
111
|
+
workersUnavailable = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Stops and forgets the shared worker, if one was ever started. */
|
|
115
|
+
export function disposeMatcherWorker(): void {
|
|
116
|
+
const running = worker;
|
|
117
|
+
worker = undefined;
|
|
118
|
+
if (running === undefined) return;
|
|
119
|
+
try {
|
|
120
|
+
running.terminate();
|
|
121
|
+
} catch {
|
|
122
|
+
// A worker that cannot be terminated is already gone.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the shared worker, starting it on first use. One worker serves the
|
|
128
|
+
* whole process; it is started only when a pattern is actually being checked,
|
|
129
|
+
* never at import time.
|
|
130
|
+
*/
|
|
131
|
+
function matcher(): MatcherWorker | undefined {
|
|
132
|
+
if (workersUnavailable) return undefined;
|
|
133
|
+
if (worker !== undefined) return worker;
|
|
134
|
+
try {
|
|
135
|
+
worker = factory();
|
|
136
|
+
} catch {
|
|
137
|
+
worker = undefined;
|
|
138
|
+
}
|
|
139
|
+
if (worker === undefined) workersUnavailable = true;
|
|
140
|
+
return worker;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tests `input` against `pattern`, giving up after `budgetMs`.
|
|
145
|
+
*
|
|
146
|
+
* @param pattern - The regular expression source a recipe published.
|
|
147
|
+
* @param input - The text to test it against.
|
|
148
|
+
* @param budgetMs - How long the pattern may run, in milliseconds.
|
|
149
|
+
* @returns Whether it matched, did not match, or ran out of time.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* matchWithBudget("^a+$", "aaa"); // -> "match"
|
|
153
|
+
* matchWithBudget("^a+$", "b"); // -> "no-match"
|
|
154
|
+
* matchWithBudget("(a+)+$", "aaaa!"); // -> "timeout", eventually
|
|
155
|
+
*/
|
|
156
|
+
export function matchWithBudget(
|
|
157
|
+
pattern: string,
|
|
158
|
+
input: string,
|
|
159
|
+
budgetMs: number = DEFAULT_PATTERN_BUDGET_MS
|
|
160
|
+
): MatchOutcome {
|
|
161
|
+
const running = matcher();
|
|
162
|
+
|
|
163
|
+
// No worker here, so there is nothing to run the pattern on but this thread.
|
|
164
|
+
// The budget cannot be enforced in that case; a plain test is still the right
|
|
165
|
+
// answer, because refusing to validate at all would be worse than the risk.
|
|
166
|
+
if (running === undefined) return inProcessMatch(pattern, input);
|
|
167
|
+
|
|
168
|
+
const shared = new SharedArrayBuffer(2 * Int32Array.BYTES_PER_ELEMENT);
|
|
169
|
+
const slots = new Int32Array(shared);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
running.postMessage({ pattern, input, shared });
|
|
173
|
+
} catch {
|
|
174
|
+
disposeMatcherWorker();
|
|
175
|
+
return inProcessMatch(pattern, input);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Starting a thread takes tens of milliseconds, and that time belongs to sous
|
|
179
|
+
// rather than to the pattern, so the budget does not start until the worker
|
|
180
|
+
// says it has the match in hand.
|
|
181
|
+
if (!waitForStart(slots)) {
|
|
182
|
+
// The worker never came online at all, so there is no budgeted thread to
|
|
183
|
+
// run on. This means workers are broken here, not that the pattern is slow.
|
|
184
|
+
disposeMatcherWorker();
|
|
185
|
+
workersUnavailable = true;
|
|
186
|
+
return inProcessMatch(pattern, input);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (Atomics.load(slots, STATUS) === PENDING) {
|
|
190
|
+
Atomics.wait(slots, STATUS, PENDING, budgetMs);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
switch (Atomics.load(slots, STATUS)) {
|
|
194
|
+
case MATCHED:
|
|
195
|
+
return "match";
|
|
196
|
+
case DID_NOT_MATCH:
|
|
197
|
+
return "no-match";
|
|
198
|
+
case FAILED:
|
|
199
|
+
// The worker could not compile the pattern. Compiling is cheap and safe,
|
|
200
|
+
// so repeating it here reproduces the same error for the caller.
|
|
201
|
+
return inProcessMatch(pattern, input);
|
|
202
|
+
default:
|
|
203
|
+
// Still running, and it always might be, so the worker is thrown away and
|
|
204
|
+
// the next check starts a fresh one.
|
|
205
|
+
disposeMatcherWorker();
|
|
206
|
+
return "timeout";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Blocks until the worker reports that it has started matching, or until the
|
|
212
|
+
* startup grace runs out.
|
|
213
|
+
*
|
|
214
|
+
* @param slots - The shared status slots for this request.
|
|
215
|
+
* @returns Whether the worker came online.
|
|
216
|
+
*/
|
|
217
|
+
function waitForStart(slots: Int32Array): boolean {
|
|
218
|
+
const deadline = Date.now() + STARTUP_GRACE_MS;
|
|
219
|
+
while (Atomics.load(slots, STARTED) === 0 && Atomics.load(slots, STATUS) === PENDING) {
|
|
220
|
+
const remaining = deadline - Date.now();
|
|
221
|
+
if (remaining <= 0) return false;
|
|
222
|
+
Atomics.wait(slots, STARTED, 0, remaining);
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The unbudgeted fallback: run the pattern right here.
|
|
229
|
+
*
|
|
230
|
+
* @param pattern - The regular expression source.
|
|
231
|
+
* @param input - The text to test it against.
|
|
232
|
+
*/
|
|
233
|
+
function inProcessMatch(pattern: string, input: string): MatchOutcome {
|
|
234
|
+
return new RegExp(pattern).test(input) ? "match" : "no-match";
|
|
235
|
+
}
|