@sous-io/plugin-api 0.0.1 → 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/LICENSE +201 -0
- package/README.md +96 -2
- package/dist/command-variables.d.ts +13 -0
- package/dist/command-variables.js +28 -0
- package/dist/config.d.ts +170 -0
- package/dist/config.js +1 -0
- package/dist/context.d.ts +159 -0
- package/dist/context.js +1 -0
- package/dist/entities.d.ts +60 -0
- package/dist/entities.js +1 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.js +138 -0
- package/dist/events.d.ts +90 -0
- package/dist/events.js +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +14 -0
- package/dist/intents.d.ts +151 -0
- package/dist/intents.js +1 -0
- package/dist/json.d.ts +84 -0
- package/dist/json.js +7 -0
- package/dist/node.d.ts +81 -0
- package/dist/node.js +1 -0
- package/dist/outputs.d.ts +193 -0
- package/dist/outputs.js +286 -0
- package/dist/plugin.d.ts +144 -0
- package/dist/plugin.js +18 -0
- package/dist/prompter.d.ts +66 -0
- package/dist/prompter.js +32 -0
- package/dist/registry.d.ts +91 -0
- package/dist/registry.js +1 -0
- package/dist/report-styles.d.ts +220 -0
- package/dist/report-styles.js +60 -0
- package/dist/reporter.d.ts +270 -0
- package/dist/reporter.js +388 -0
- package/dist/schema.d.ts +131 -0
- package/dist/schema.js +211 -0
- package/dist/services.d.ts +68 -0
- package/dist/services.js +1 -0
- package/dist/timing.d.ts +31 -0
- package/dist/timing.js +20 -0
- package/package.json +33 -5
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting: services and intents never print. They report through the `Reporter` the front
|
|
3
|
+
* end handed over with the chain (`ctx.reporter`), and the front end decides how it looks.
|
|
4
|
+
*
|
|
5
|
+
* Every message goes through one method, `log(level, message, details?, style?)`. The level says how
|
|
6
|
+
* important it is, `details` carries its facts, and the style says what kind of entry it is (an
|
|
7
|
+
* operation, a labeled value, a table, ...). Each front end's reporter decides how each style looks:
|
|
8
|
+
* the CLI draws it with colors and alignment, MCP sends `details` as data. On top of `log` sit one
|
|
9
|
+
* method per level (`info`, `warn`, ...) and one or more methods per style (`operationStart`, ...),
|
|
10
|
+
* all defined once, in {@link BaseReporter}.
|
|
11
|
+
*/
|
|
12
|
+
import { DataValue, IntentOutputs, ItemList, Payload, Table, TextBlock, Tree, VariableSet } from "./outputs.js";
|
|
13
|
+
import { ReportEntryStyle, } from "./report-styles.js";
|
|
14
|
+
export { ReportEntryStyle } from "./report-styles.js";
|
|
15
|
+
/** The eight syslog levels, by name. */
|
|
16
|
+
export const LogLevel = {
|
|
17
|
+
Debug: "debug",
|
|
18
|
+
Info: "info",
|
|
19
|
+
Notice: "notice",
|
|
20
|
+
Warning: "warning",
|
|
21
|
+
Error: "error",
|
|
22
|
+
Critical: "critical",
|
|
23
|
+
Alert: "alert",
|
|
24
|
+
Emergency: "emergency",
|
|
25
|
+
};
|
|
26
|
+
/** The eight syslog levels, from least to most severe. */
|
|
27
|
+
export const LOG_LEVELS = [
|
|
28
|
+
LogLevel.Debug,
|
|
29
|
+
LogLevel.Info,
|
|
30
|
+
LogLevel.Notice,
|
|
31
|
+
LogLevel.Warning,
|
|
32
|
+
LogLevel.Error,
|
|
33
|
+
LogLevel.Critical,
|
|
34
|
+
LogLevel.Alert,
|
|
35
|
+
LogLevel.Emergency,
|
|
36
|
+
];
|
|
37
|
+
/** Whether a string names a log level. */
|
|
38
|
+
export function isLogLevel(value) {
|
|
39
|
+
return typeof value === "string" && LOG_LEVELS.includes(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether a message at `level` shows when the threshold is `threshold`.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* isLevelShown("warning", "notice"); // true
|
|
46
|
+
* isLevelShown("debug", "info"); // false
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function isLevelShown(level, threshold) {
|
|
50
|
+
return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(threshold);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The text of any message: an error's chain one cause per line, a payload's or outputs' text.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* messageText(new Error("Could not download", { cause: new Error("DNS failed") }));
|
|
57
|
+
* // "Could not download\nCaused by: DNS failed"
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function messageText(message) {
|
|
61
|
+
if (typeof message === "string") {
|
|
62
|
+
return message;
|
|
63
|
+
}
|
|
64
|
+
if (message instanceof Error) {
|
|
65
|
+
return errorChain(message).map((error, index) => (index === 0 ? error.message : `Caused by: ${error.message}`)).join("\n");
|
|
66
|
+
}
|
|
67
|
+
return message.toText();
|
|
68
|
+
}
|
|
69
|
+
/** An error and every error under it through `cause`, outermost first (a cycle stops it). */
|
|
70
|
+
export function errorChain(error) {
|
|
71
|
+
const chain = [];
|
|
72
|
+
let current = error;
|
|
73
|
+
while (current instanceof Error && !chain.includes(current)) {
|
|
74
|
+
chain.push(current);
|
|
75
|
+
current = current.cause;
|
|
76
|
+
}
|
|
77
|
+
return chain;
|
|
78
|
+
}
|
|
79
|
+
// Plain values only (no class instances), so a log pipeline can always serialize them; a front end
|
|
80
|
+
// never needs them to show the message, which says everything a person needs.
|
|
81
|
+
/**
|
|
82
|
+
* The shared part of every reporter, and the ONE place every reporting method is defined.
|
|
83
|
+
* `Reporter`, the type plugins see, is derived from this class's public members, and the
|
|
84
|
+
* {@link Reporting} mixin copies its convenience methods onto other classes, so a method added here
|
|
85
|
+
* reaches every reporter and every class that mixes it in.
|
|
86
|
+
*
|
|
87
|
+
* A subclass writes a message that passes the threshold (`write`); everything else is shared.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* class LinesReporter extends BaseReporter {
|
|
91
|
+
* readonly lines: string[] = [];
|
|
92
|
+
*
|
|
93
|
+
* protected write(level: LogLevel, message: string): void {
|
|
94
|
+
* this.lines.push(`${level}: ${message}`);
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* const reporter = new LinesReporter(LogLevel.Warning);
|
|
99
|
+
* reporter.notice("Skipped."); // below the threshold: dropped
|
|
100
|
+
* reporter.warn("Stale."); // lines: ["warning: Stale."]
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export class BaseReporter {
|
|
104
|
+
thresholdSource;
|
|
105
|
+
/**
|
|
106
|
+
* @param thresholdSource - The least severe level shown (default `debug`, everything). A function
|
|
107
|
+
* is read on every message, for a threshold that changes while the reporter is in use.
|
|
108
|
+
*/
|
|
109
|
+
constructor(thresholdSource = LogLevel.Debug) {
|
|
110
|
+
this.thresholdSource = thresholdSource;
|
|
111
|
+
}
|
|
112
|
+
/** The least severe level shown now. */
|
|
113
|
+
threshold() {
|
|
114
|
+
return typeof this.thresholdSource === "function" ? this.thresholdSource() : this.thresholdSource;
|
|
115
|
+
}
|
|
116
|
+
/** Whether a message at `level` passes the threshold now. */
|
|
117
|
+
shows(level) {
|
|
118
|
+
return isLevelShown(level, this.threshold());
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reports one message. Every other reporting method ends here.
|
|
122
|
+
*
|
|
123
|
+
* @param style - What kind of entry it is (default `ReportEntryStyle.Plain`, a paragraph).
|
|
124
|
+
*/
|
|
125
|
+
log(level, message, details, style = ReportEntryStyle.Plain) {
|
|
126
|
+
if (this.shows(level)) {
|
|
127
|
+
this.write(level, message, details, style);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// -----------------------------------------------------------------------------------------
|
|
131
|
+
// One method per level
|
|
132
|
+
/** Reports a message for whoever is debugging sous. */
|
|
133
|
+
debug(message, details, style) {
|
|
134
|
+
this.log(LogLevel.Debug, message, details, style);
|
|
135
|
+
}
|
|
136
|
+
/** Reports an ordinary message. */
|
|
137
|
+
info(message, details, style) {
|
|
138
|
+
this.log(LogLevel.Info, message, details, style);
|
|
139
|
+
}
|
|
140
|
+
/** Reports a normal but significant event. */
|
|
141
|
+
notice(message, details, style) {
|
|
142
|
+
this.log(LogLevel.Notice, message, details, style);
|
|
143
|
+
}
|
|
144
|
+
/** Reports something that may need the user's attention. */
|
|
145
|
+
warning(message, details, style) {
|
|
146
|
+
this.log(LogLevel.Warning, message, details, style);
|
|
147
|
+
}
|
|
148
|
+
/** Shorthand for {@link warning}. */
|
|
149
|
+
warn(message, details, style) {
|
|
150
|
+
this.log(LogLevel.Warning, message, details, style);
|
|
151
|
+
}
|
|
152
|
+
/** Reports something that failed. */
|
|
153
|
+
error(message, details, style) {
|
|
154
|
+
this.log(LogLevel.Error, message, details, style);
|
|
155
|
+
}
|
|
156
|
+
/** Reports a failure that stops the run. */
|
|
157
|
+
critical(message, details, style) {
|
|
158
|
+
this.log(LogLevel.Critical, message, details, style);
|
|
159
|
+
}
|
|
160
|
+
/** Reports something that must be acted on at once. */
|
|
161
|
+
alert(message, details, style) {
|
|
162
|
+
this.log(LogLevel.Alert, message, details, style);
|
|
163
|
+
}
|
|
164
|
+
/** Reports that sous cannot work at all. */
|
|
165
|
+
emergency(message, details, style) {
|
|
166
|
+
this.log(LogLevel.Emergency, message, details, style);
|
|
167
|
+
}
|
|
168
|
+
/** Shorthand for {@link emergency}. */
|
|
169
|
+
emerg(message, details, style) {
|
|
170
|
+
this.log(LogLevel.Emergency, message, details, style);
|
|
171
|
+
}
|
|
172
|
+
// -----------------------------------------------------------------------------------------
|
|
173
|
+
// One or more methods per style. Each forces its style, turns its parameters into a plain
|
|
174
|
+
// `message` and typed `details`, and has a default level the last parameter overrides.
|
|
175
|
+
/** Text shown exactly as given, such as a value a script reads: no wrapping, no color. */
|
|
176
|
+
text(content, level = LogLevel.Info) {
|
|
177
|
+
this.log(level, new TextBlock(content, { verbatim: true }), undefined, ReportEntryStyle.Text);
|
|
178
|
+
}
|
|
179
|
+
/** An explanation that is neither a warning nor an error. */
|
|
180
|
+
note(text, level = LogLevel.Notice) {
|
|
181
|
+
this.log(level, text, undefined, ReportEntryStyle.Note);
|
|
182
|
+
}
|
|
183
|
+
/** The start of an operation: `Building the project`. */
|
|
184
|
+
operationStart(operationName, level = LogLevel.Notice) {
|
|
185
|
+
this.log(level, operationName, { name: operationName }, ReportEntryStyle.OperationStart);
|
|
186
|
+
}
|
|
187
|
+
/** A step inside the current operation: one target, one recipe, one check. */
|
|
188
|
+
stepStart(stepName, level = LogLevel.Info) {
|
|
189
|
+
this.log(level, stepName, { name: stepName }, ReportEntryStyle.StepStart);
|
|
190
|
+
}
|
|
191
|
+
/** One line of detail inside the current operation or step: `Read 12 recipes.` */
|
|
192
|
+
detail(text, level = LogLevel.Info) {
|
|
193
|
+
this.log(level, text, undefined, ReportEntryStyle.Detail);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* One action done (or, in a dry run, to be done) to one target. The level defaults to `error`
|
|
197
|
+
* for `failed` and `info` for everything else.
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* reporter.operation("written", "/work/app/CLAUDE.md", { tokens: 1840 });
|
|
201
|
+
* reporter.operation("skipped", "/work/app/AGENTS.md", { note: 'left out by the transform "strip"' });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
operation(action, target, facts = {}, level) {
|
|
205
|
+
const details = { action, target, ...facts };
|
|
206
|
+
const text = facts.note === undefined ? `${action}: ${target}` : `${action}: ${target} (${facts.note})`;
|
|
207
|
+
this.log(level ?? (action === "failed" ? LogLevel.Error : LogLevel.Info), text, details, ReportEntryStyle.Operation);
|
|
208
|
+
}
|
|
209
|
+
/** How an operation ended: `Done.`, or `Done with 2 errors.` The level defaults to `info`, or `error` when it failed. */
|
|
210
|
+
operationEnd(summary, ok = true, level) {
|
|
211
|
+
this.log(level ?? (ok ? LogLevel.Info : LogLevel.Error), summary, { ok }, ReportEntryStyle.OperationEnd);
|
|
212
|
+
}
|
|
213
|
+
/** One label and its value: `Repository: /work/recipes`. */
|
|
214
|
+
variable(label, value, detail, level = LogLevel.Info) {
|
|
215
|
+
this.log(level, new VariableSet([detail === undefined ? { label, value } : { label, value, detail }]), undefined, ReportEntryStyle.Variable);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Labels and values, read as one block (a terminal lines their colons up). A record is read as
|
|
219
|
+
* label and value pairs, in its key order.
|
|
220
|
+
*/
|
|
221
|
+
variables(entries, title, level = LogLevel.Info) {
|
|
222
|
+
this.log(level, new VariableSet(entries, title === undefined ? {} : { title }), undefined, ReportEntryStyle.VariableList);
|
|
223
|
+
}
|
|
224
|
+
/** The facts a command runs with (its project, its config, what it reads), shown before its output. */
|
|
225
|
+
commandVariables(entries, level = LogLevel.Info) {
|
|
226
|
+
this.log(level, new VariableSet(entries, { title: "Command Variables" }), undefined, ReportEntryStyle.CommandVariables);
|
|
227
|
+
}
|
|
228
|
+
/** A list of items, with an optional title before it and an optional sentence after it. */
|
|
229
|
+
list(items, options = {}, level = LogLevel.Info) {
|
|
230
|
+
this.log(level, new ItemList(items, options), undefined, ReportEntryStyle.List);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A table of rows. With no rows, a front end shows `options.empty` when it is given;
|
|
234
|
+
* `options.rowNotes[i]` is a note shown under row `i`.
|
|
235
|
+
*/
|
|
236
|
+
table(columns, rows, options = {}, level = LogLevel.Info) {
|
|
237
|
+
this.log(level, new Table(columns, rows, options), undefined, ReportEntryStyle.Table);
|
|
238
|
+
}
|
|
239
|
+
/** Items nested under other items, such as each contributor and the changes it made. */
|
|
240
|
+
tree(nodes, title, level = LogLevel.Info) {
|
|
241
|
+
this.log(level, new Tree(nodes, title === undefined ? {} : { title }), undefined, ReportEntryStyle.Tree);
|
|
242
|
+
}
|
|
243
|
+
/** How many things were found: `Found 5 recipes`. `entity` is already in the right number. */
|
|
244
|
+
count(count, entity, verb = "Found", level = LogLevel.Info) {
|
|
245
|
+
this.log(level, `${verb === "" ? "" : `${verb} `}${count} ${entity}`, { count, entity, verb }, ReportEntryStyle.Count);
|
|
246
|
+
}
|
|
247
|
+
/** Says that a run is a dry run, or what a dry run left undone: `Nothing was written.` */
|
|
248
|
+
dryRun(text, level = LogLevel.Notice) {
|
|
249
|
+
this.log(level, text, undefined, ReportEntryStyle.DryRun);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Something wrong, with its cause and what to do about it. A one-sentence warning with nothing
|
|
253
|
+
* more to say is `warn(text)` instead.
|
|
254
|
+
*
|
|
255
|
+
* ```ts
|
|
256
|
+
* reporter.problem("The compilation transform \"strip\" is not registered.", { subject: "CLAUDE.md", hint: "Install the plugin that adds it." });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
problem(summary, facts = {}, level = LogLevel.Warning) {
|
|
260
|
+
const details = { summary, ...facts };
|
|
261
|
+
this.log(level, joinLines(summary, [facts.cause, facts.hint].filter((part) => part !== undefined)), details, ReportEntryStyle.Problem);
|
|
262
|
+
}
|
|
263
|
+
/** What a name on the command line resolved to, and why (`reason`, one sentence). */
|
|
264
|
+
resolvedReference(reason, facts, level = LogLevel.Notice) {
|
|
265
|
+
this.log(level, reason, { ...facts, reason }, ReportEntryStyle.ResolvedReference);
|
|
266
|
+
}
|
|
267
|
+
/** The question about to be asked, and where its answer goes. */
|
|
268
|
+
question(facts, level = LogLevel.Notice) {
|
|
269
|
+
this.log(level, `Question ${facts.index} of ${facts.total}: ${facts.name}`, { ...facts }, ReportEntryStyle.Question);
|
|
270
|
+
}
|
|
271
|
+
/** A command sous runs, or (with `dryRun`) would run. */
|
|
272
|
+
command(command, args, facts = {}, level = LogLevel.Notice) {
|
|
273
|
+
const line = [command, ...args].join(" ");
|
|
274
|
+
const text = `${facts.dryRun ? "Would run" : "Running"}: ${line}${facts.cwd === undefined ? "" : ` (in ${facts.cwd})`}`;
|
|
275
|
+
this.log(level, text, { command, args, ...facts }, ReportEntryStyle.Command);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Progress through a longer step. A terminal draws one bar and redraws it in place; the event
|
|
279
|
+
* with `finished: true` ends it.
|
|
280
|
+
*/
|
|
281
|
+
progress(event, level = LogLevel.Info) {
|
|
282
|
+
this.log(level, event.label, { ...event }, ReportEntryStyle.Progress);
|
|
283
|
+
}
|
|
284
|
+
/** A structured value shown as data (a config as JSON). */
|
|
285
|
+
data(value, level = LogLevel.Info) {
|
|
286
|
+
this.log(level, new DataValue(value), undefined, ReportEntryStyle.Data);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A command's result: what its intent returned. A terminal sends it to standard output, and
|
|
290
|
+
* everything else to standard error.
|
|
291
|
+
*/
|
|
292
|
+
result(outputs, level = LogLevel.Notice) {
|
|
293
|
+
this.log(level, outputs, undefined, ReportEntryStyle.Result);
|
|
294
|
+
}
|
|
295
|
+
/** The sous banner. */
|
|
296
|
+
banner(level = LogLevel.Info) {
|
|
297
|
+
this.log(level, "sous", undefined, ReportEntryStyle.Banner);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** The lines that are given, one per line; an array's lines are indented two spaces under a title. */
|
|
301
|
+
function joinLines(first, rest, last) {
|
|
302
|
+
const body = first === undefined ? rest : rest.map((line) => ` ${line}`);
|
|
303
|
+
return [first, ...body, last].filter((line) => line !== undefined).join("\n");
|
|
304
|
+
}
|
|
305
|
+
/** The members of `BaseReporter.prototype` that {@link Reporting} does not copy. */
|
|
306
|
+
const NOT_ALIASED = ["constructor", "log", "write", "threshold", "shows"];
|
|
307
|
+
/**
|
|
308
|
+
* Gives a class every reporting method of {@link BaseReporter} (`info`, `warn`, `operationStart`,
|
|
309
|
+
* ...) as a shorthand for the same method on its `reporter`, which the class provides. The methods
|
|
310
|
+
* are copied from `BaseReporter.prototype` when the mixin is applied, so nothing lists them twice.
|
|
311
|
+
* They are public, since a type can only make a member protected inside a class body.
|
|
312
|
+
*
|
|
313
|
+
* ```ts
|
|
314
|
+
* class Compiler extends Reporting(Object) {
|
|
315
|
+
* constructor(private readonly deps: { reporter: Reporter }) {
|
|
316
|
+
* super();
|
|
317
|
+
* }
|
|
318
|
+
*
|
|
319
|
+
* protected get reporter(): Reporter {
|
|
320
|
+
* return this.deps.reporter;
|
|
321
|
+
* }
|
|
322
|
+
*
|
|
323
|
+
* run(): void {
|
|
324
|
+
* this.info("Compiling"); // this.deps.reporter.info("Compiling")
|
|
325
|
+
* }
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export function Reporting(Base) {
|
|
330
|
+
// The class provides `reporter` (a getter or a field); it is read when a method is called. It is
|
|
331
|
+
// not declared here, since a declared type cannot hold a protected member.
|
|
332
|
+
class WithReporting extends Base {
|
|
333
|
+
}
|
|
334
|
+
for (const name of Object.getOwnPropertyNames(BaseReporter.prototype)) {
|
|
335
|
+
if (NOT_ALIASED.includes(name)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
Object.defineProperty(WithReporting.prototype, name, {
|
|
339
|
+
configurable: true,
|
|
340
|
+
writable: true,
|
|
341
|
+
value(...args) {
|
|
342
|
+
return this.reporter[name](...args);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return WithReporting;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Builds a reporter from a function that receives every message, whatever its level.
|
|
350
|
+
*
|
|
351
|
+
* ```ts
|
|
352
|
+
* const reporter = createReporter((level, message) => lines.push(`${level}: ${message}`));
|
|
353
|
+
* reporter.warn("Recipe X declares a variable no template reads.");
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
export function createReporter(log) {
|
|
357
|
+
return new (class FunctionReporter extends BaseReporter {
|
|
358
|
+
write(level, message, details, style) {
|
|
359
|
+
log(level, message, details, style);
|
|
360
|
+
}
|
|
361
|
+
})();
|
|
362
|
+
}
|
|
363
|
+
/** A reporter that drops everything. */
|
|
364
|
+
export const nullReporter = createReporter(() => { });
|
|
365
|
+
/**
|
|
366
|
+
* A reporter that keeps every message, to hand on later ({@link flushTo}) or to read in tests.
|
|
367
|
+
*/
|
|
368
|
+
export class BufferedReporter extends BaseReporter {
|
|
369
|
+
/** Every message, in order. */
|
|
370
|
+
messages = [];
|
|
371
|
+
write(level, message, details, style) {
|
|
372
|
+
this.messages.push(details === undefined ? { level, message, style } : { level, message, details, style });
|
|
373
|
+
}
|
|
374
|
+
/** The text of the messages at a level ({@link messageText}). */
|
|
375
|
+
at(level) {
|
|
376
|
+
return this.messages.filter((entry) => entry.level === level).map((entry) => messageText(entry.message));
|
|
377
|
+
}
|
|
378
|
+
/** The messages of a style. */
|
|
379
|
+
styled(style) {
|
|
380
|
+
return this.messages.filter((entry) => entry.style === style);
|
|
381
|
+
}
|
|
382
|
+
/** Sends every kept message to another reporter, in order, and forgets them. */
|
|
383
|
+
flushTo(reporter) {
|
|
384
|
+
for (const entry of this.messages.splice(0)) {
|
|
385
|
+
reporter.log(entry.level, entry.message, entry.details, entry.style);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schemas.
|
|
3
|
+
*
|
|
4
|
+
* Sous accepts any schema that follows the Standard Schema interface (zod 4, valibot, arktype and
|
|
5
|
+
* others do), so a plugin uses the schema library it likes. Front ends that generate flags or tools
|
|
6
|
+
* from a schema (the CLI, MCP) also need its JSON Schema, which the Standard JSON Schema interface
|
|
7
|
+
* gives; zod 4 has both.
|
|
8
|
+
*
|
|
9
|
+
* For plugins that want no schema library at all (core and plugin-cli among them, to keep start-up
|
|
10
|
+
* fast), {@link jsonSchema} turns a small JSON Schema into a schema sous accepts.
|
|
11
|
+
*/
|
|
12
|
+
/** The Standard Schema interface (https://standardschema.dev), version 1. */
|
|
13
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
14
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
15
|
+
}
|
|
16
|
+
export declare namespace StandardSchemaV1 {
|
|
17
|
+
interface Props<Input = unknown, Output = Input> {
|
|
18
|
+
readonly version: 1;
|
|
19
|
+
readonly vendor: string;
|
|
20
|
+
readonly validate: (value: unknown, options?: Options | undefined) => Result<Output> | Promise<Result<Output>>;
|
|
21
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
22
|
+
}
|
|
23
|
+
interface Options {
|
|
24
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
25
|
+
}
|
|
26
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
27
|
+
interface SuccessResult<Output> {
|
|
28
|
+
readonly value: Output;
|
|
29
|
+
readonly issues?: undefined;
|
|
30
|
+
}
|
|
31
|
+
interface FailureResult {
|
|
32
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
33
|
+
}
|
|
34
|
+
interface Issue {
|
|
35
|
+
readonly message: string;
|
|
36
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
37
|
+
}
|
|
38
|
+
interface PathSegment {
|
|
39
|
+
readonly key: PropertyKey;
|
|
40
|
+
}
|
|
41
|
+
interface Types<Input = unknown, Output = Input> {
|
|
42
|
+
readonly input: Input;
|
|
43
|
+
readonly output: Output;
|
|
44
|
+
}
|
|
45
|
+
type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
46
|
+
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
47
|
+
}
|
|
48
|
+
/** The converter the Standard JSON Schema interface adds to `~standard`. */
|
|
49
|
+
export interface StandardJsonSchemaConverter {
|
|
50
|
+
readonly input: (options: {
|
|
51
|
+
readonly target: string;
|
|
52
|
+
}) => Record<string, unknown>;
|
|
53
|
+
readonly output: (options: {
|
|
54
|
+
readonly target: string;
|
|
55
|
+
}) => Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/** Any schema sous accepts: Standard Schema, optionally with a JSON Schema converter. */
|
|
58
|
+
export type SousSchema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output>;
|
|
59
|
+
/** The value a schema produces. */
|
|
60
|
+
export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer O> ? O : unknown;
|
|
61
|
+
/** The value a schema accepts. */
|
|
62
|
+
export type InferInput<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown;
|
|
63
|
+
/** One problem found while checking a value. */
|
|
64
|
+
export interface SchemaIssue {
|
|
65
|
+
/** Where, as a path string (`compilation.targets[2].id`); `""` for the value itself. */
|
|
66
|
+
path: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The subset of JSON Schema {@link jsonSchema} checks: `type`, `enum`, `const`, `properties`,
|
|
71
|
+
* `required`, `additionalProperties`, `items`, `anyOf`, `default` and `description`.
|
|
72
|
+
*/
|
|
73
|
+
export interface JsonSchema {
|
|
74
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
75
|
+
description?: string;
|
|
76
|
+
default?: unknown;
|
|
77
|
+
enum?: readonly unknown[];
|
|
78
|
+
const?: unknown;
|
|
79
|
+
properties?: Record<string, JsonSchema>;
|
|
80
|
+
required?: readonly string[];
|
|
81
|
+
additionalProperties?: boolean | JsonSchema;
|
|
82
|
+
items?: JsonSchema;
|
|
83
|
+
anyOf?: readonly JsonSchema[];
|
|
84
|
+
[extra: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
/** The JSON Schema type names. */
|
|
87
|
+
export type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "null" | "object" | "array";
|
|
88
|
+
/**
|
|
89
|
+
* Turns a small JSON Schema into a schema sous accepts, with no schema library. It checks the
|
|
90
|
+
* subset {@link JsonSchema} lists, fills in `default`s, and gives its JSON Schema back to front ends.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* sous.entities.add("cli-global-argument", {
|
|
94
|
+
* id: "debug",
|
|
95
|
+
* schema: jsonSchema<boolean>({ type: "boolean", default: false }),
|
|
96
|
+
* description: "Show every message, debug ones included",
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function jsonSchema<T = unknown>(schema: JsonSchema): SousSchema<T, T>;
|
|
101
|
+
/**
|
|
102
|
+
* The JSON Schema of any schema, when it has one: through the Standard JSON Schema converter
|
|
103
|
+
* (zod 4 and {@link jsonSchema} have it). `undefined` when the schema cannot describe itself.
|
|
104
|
+
*/
|
|
105
|
+
export declare function toJsonSchema(schema: SousSchema | undefined): JsonSchema | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Checks a value against any schema and returns the checked value (with defaults filled in) or the
|
|
108
|
+
* issues, each with a path string. Asynchronous schemas are awaited.
|
|
109
|
+
*/
|
|
110
|
+
export declare function validateWith<T>(schema: SousSchema<unknown, T>, value: unknown): Promise<{
|
|
111
|
+
value: T;
|
|
112
|
+
issues?: undefined;
|
|
113
|
+
} | {
|
|
114
|
+
value?: undefined;
|
|
115
|
+
issues: SchemaIssue[];
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* The synchronous form of {@link validateWith}. A schema that answers with a promise is reported
|
|
119
|
+
* as one issue, since nothing can wait for it here.
|
|
120
|
+
*/
|
|
121
|
+
export declare function validateWithSync<T>(schema: SousSchema<unknown, T>, value: unknown): {
|
|
122
|
+
value: T;
|
|
123
|
+
issues?: undefined;
|
|
124
|
+
} | {
|
|
125
|
+
value?: undefined;
|
|
126
|
+
issues: SchemaIssue[];
|
|
127
|
+
};
|
|
128
|
+
/** Whether a value looks like a schema sous accepts. */
|
|
129
|
+
export declare function isSchema(value: unknown): value is SousSchema;
|
|
130
|
+
/** Writes issue path segments the way sous writes paths: `a.b[2]."c.d"`. */
|
|
131
|
+
export declare function formatIssuePath(segments: readonly PropertyKey[]): string;
|