@sous-io/plugin-api 0.0.0-stage → 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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * What an intent returns: `IntentOutputs`, one or more `IntentOutput`s in the order they are shown.
3
+ * Each output carries a result as a payload type a reporter knows how to show (`VariableSet`,
4
+ * `Table`, ...), and converts to plain values for machines (`--json`, MCP, a calling intent).
5
+ *
6
+ * A payload is also what a reporter is given to show a structure while an intent runs:
7
+ * `reporter.variables(...)` reports a `VariableSet`.
8
+ */
9
+ /** Marks a payload, so a reporter recognizes one even from another copy of this package. */
10
+ const PAYLOAD = Symbol.for("sous.payload");
11
+ /** Marks an `IntentOutputs`, for the same reason. */
12
+ const OUTPUTS = Symbol.for("sous.intent-outputs");
13
+ /**
14
+ * A structure a reporter knows how to show. Each payload holds plain values only, and converts to
15
+ * them: `toPlain()` gives a plain object, and each type adds its own conversion (`toObject()`,
16
+ * `toRows()`, ...). `toText()` is the payload as plain text, for a reporter that draws nothing.
17
+ */
18
+ export class Payload {
19
+ [PAYLOAD] = true;
20
+ /** Whether a value is a payload, from any copy of plugin-api. */
21
+ static is(value) {
22
+ return typeof value === "object" && value !== null && value[PAYLOAD] === true;
23
+ }
24
+ }
25
+ /** A value as text: nothing for `undefined` and `null`, JSON for an object. */
26
+ function textOf(value) {
27
+ if (value === undefined || value === null) {
28
+ return "";
29
+ }
30
+ if (typeof value !== "object") {
31
+ return String(value);
32
+ }
33
+ try {
34
+ return JSON.stringify(value);
35
+ }
36
+ catch {
37
+ return String(value);
38
+ }
39
+ }
40
+ /** Labels and values, read as one block: `Repository: /work/recipes`. */
41
+ export class VariableSet extends Payload {
42
+ kind = "variable-set";
43
+ entries;
44
+ title;
45
+ /** @param entries - The pairs, or a record read as label and value pairs in its key order. */
46
+ constructor(entries, options = {}) {
47
+ super();
48
+ this.entries = Array.isArray(entries) ? [...entries] : Object.entries(entries).map(([label, value]) => ({ label, value }));
49
+ this.title = options.title;
50
+ }
51
+ /** The pairs as one object: `{ Repository: "/work/recipes" }`. A label used twice keeps its last value. */
52
+ toObject() {
53
+ return Object.fromEntries(this.entries.filter((entry) => entry.label !== "").map((entry) => [entry.label, entry.value]));
54
+ }
55
+ toPlain() {
56
+ return this.toObject();
57
+ }
58
+ toText() {
59
+ const lines = this.entries.map((entry) => `${entry.label}: ${textOf(entry.value)}${entry.detail === undefined ? "" : ` (${entry.detail})`}`);
60
+ return [...(this.title === undefined ? [] : [this.title]), ...lines].join("\n");
61
+ }
62
+ }
63
+ /** Rows under columns. */
64
+ export class Table extends Payload {
65
+ kind = "table";
66
+ columns;
67
+ rows;
68
+ title;
69
+ /** What to say instead of the table when there are no rows. */
70
+ empty;
71
+ /** A note shown under a row, by the row's index; `undefined` for a row with none. */
72
+ rowNotes;
73
+ constructor(columns, rows, options = {}) {
74
+ super();
75
+ this.columns = columns;
76
+ this.rows = rows;
77
+ this.title = options.title;
78
+ this.empty = options.empty;
79
+ this.rowNotes = options.rowNotes;
80
+ }
81
+ /** The rows. */
82
+ toRows() {
83
+ return [...this.rows];
84
+ }
85
+ toPlain() {
86
+ return { rows: this.toRows() };
87
+ }
88
+ toText() {
89
+ if (this.rows.length === 0 && this.empty !== undefined) {
90
+ return this.empty;
91
+ }
92
+ const lines = this.rows.map((row) => this.columns.map((column) => textOf(row[column.key])).join(" "));
93
+ return [...(this.title === undefined ? [] : [this.title]), ...lines].join("\n");
94
+ }
95
+ }
96
+ /** A list of items, with an optional title before it and an optional sentence after it. */
97
+ export class ItemList extends Payload {
98
+ kind = "item-list";
99
+ items;
100
+ title;
101
+ footer;
102
+ constructor(items, options = {}) {
103
+ super();
104
+ this.items = items;
105
+ this.title = options.title;
106
+ this.footer = options.footer;
107
+ }
108
+ /** The items. */
109
+ toArray() {
110
+ return [...this.items];
111
+ }
112
+ toPlain() {
113
+ return { items: this.toArray() };
114
+ }
115
+ toText() {
116
+ return [...(this.title === undefined ? [] : [this.title]), ...this.items.map((item) => `- ${item}`), ...(this.footer === undefined ? [] : [this.footer])].join("\n");
117
+ }
118
+ }
119
+ /** Items nested under other items. */
120
+ export class Tree extends Payload {
121
+ kind = "tree";
122
+ nodes;
123
+ title;
124
+ constructor(nodes, options = {}) {
125
+ super();
126
+ this.nodes = nodes;
127
+ this.title = options.title;
128
+ }
129
+ toPlain() {
130
+ return { nodes: this.nodes };
131
+ }
132
+ toText() {
133
+ const lines = (nodes, depth) => nodes.flatMap((node) => [`${" ".repeat(depth)}${node.label}${node.detail === undefined ? "" : ` (${node.detail})`}`, ...lines(node.children ?? [], depth + 1)]);
134
+ return [...(this.title === undefined ? [] : [this.title]), ...lines(this.nodes, 0)].join("\n");
135
+ }
136
+ }
137
+ /**
138
+ * Text. A paragraph a reporter may wrap, or, with `verbatim`, text shown exactly as given (a value
139
+ * a script reads, `--help`).
140
+ */
141
+ export class TextBlock extends Payload {
142
+ kind = "text-block";
143
+ text;
144
+ verbatim;
145
+ constructor(text, options = {}) {
146
+ super();
147
+ this.text = text;
148
+ this.verbatim = options.verbatim === true;
149
+ }
150
+ toString() {
151
+ return this.text;
152
+ }
153
+ toPlain() {
154
+ return { text: this.text };
155
+ }
156
+ toText() {
157
+ return this.text;
158
+ }
159
+ }
160
+ /** A structured value shown as data (a config as JSON). */
161
+ export class DataValue extends Payload {
162
+ kind = "data-value";
163
+ value;
164
+ constructor(value) {
165
+ super();
166
+ this.value = value;
167
+ }
168
+ /** The value itself. */
169
+ toValue() {
170
+ return this.value;
171
+ }
172
+ /** The value when it is a plain object, else `{ value }`. */
173
+ toPlain() {
174
+ return typeof this.value === "object" && this.value !== null && !Array.isArray(this.value) ? this.value : { value: this.value };
175
+ }
176
+ toText() {
177
+ try {
178
+ return JSON.stringify(this.value, null, 2) ?? String(this.value);
179
+ }
180
+ catch {
181
+ return String(this.value);
182
+ }
183
+ }
184
+ }
185
+ /** One part of what an intent returns. */
186
+ export class IntentOutput {
187
+ title;
188
+ summary;
189
+ result;
190
+ data;
191
+ style;
192
+ constructor(init) {
193
+ this.title = init.title;
194
+ this.summary = init.summary;
195
+ this.result = init.result;
196
+ this.data = init.data;
197
+ this.style = init.style;
198
+ }
199
+ /** What machines get: `data`, else the result as a plain object, else `{}`. */
200
+ toPlain() {
201
+ return (this.data ?? this.result?.toPlain() ?? {});
202
+ }
203
+ /** The output as plain text: title, summary, then the result. */
204
+ toText() {
205
+ return [this.title, this.summary, this.result?.toText()].filter((part) => part !== undefined && part !== "").join("\n");
206
+ }
207
+ }
208
+ /**
209
+ * What an intent returns: its outputs, in the order they are shown.
210
+ *
211
+ * ```ts
212
+ * return new IntentOutputs(new IntentOutput({ summary: "Subscribed to acme/git.", result: new VariableSet({ Recipe: "acme/git" }) }));
213
+ *
214
+ * const prepared = (await ctx.run("build.prepare", {})).toPlain(); // the last output's plain object
215
+ * ```
216
+ */
217
+ export class IntentOutputs {
218
+ [OUTPUTS] = true;
219
+ #outputs;
220
+ /** The exit code the command ends with (a build with errors ends with 1). Default: none, so 0. */
221
+ exitCode;
222
+ constructor(outputs = [], options = {}) {
223
+ this.#outputs = Array.isArray(outputs) ? [...outputs] : [outputs];
224
+ this.exitCode = options.exitCode;
225
+ }
226
+ /** Outputs holding one plain value and nothing to show beyond it. */
227
+ static of(data, init = {}) {
228
+ return new IntentOutputs(new IntentOutput({ ...init, data }));
229
+ }
230
+ /** Whether a value is an `IntentOutputs`, from any copy of plugin-api. */
231
+ static is(value) {
232
+ return typeof value === "object" && value !== null && value[OUTPUTS] === true;
233
+ }
234
+ /** How many outputs there are. */
235
+ get length() {
236
+ return this.#outputs.length;
237
+ }
238
+ /** The outputs, in order. */
239
+ toArray() {
240
+ return [...this.#outputs];
241
+ }
242
+ /** The first output, or `undefined`. */
243
+ first() {
244
+ return this.#outputs[0];
245
+ }
246
+ /** The last output, or `undefined`. */
247
+ last() {
248
+ return this.#outputs[this.#outputs.length - 1];
249
+ }
250
+ /** The last output's plain object (`{}` with none). Never an array. */
251
+ toPlain() {
252
+ return (this.last()?.toPlain() ?? {});
253
+ }
254
+ /** Every output as plain text, separated by a blank line. */
255
+ toText() {
256
+ return this.#outputs.map((output) => output.toText()).filter((text) => text !== "").join("\n\n");
257
+ }
258
+ /** New outputs with more appended, keeping the exit code (for a wrapper or an `after` listener). */
259
+ with(...more) {
260
+ return new IntentOutputs([...this.#outputs, ...more], this.exitCode === undefined ? {} : { exitCode: this.exitCode });
261
+ }
262
+ /**
263
+ * The same outputs with `data` as what machines get: set on the last output, or as one output
264
+ * holding only data when there is none. Use it when the last output shows something built from
265
+ * a larger result.
266
+ *
267
+ * ```ts
268
+ * return new IntentOutputs([commandVariables(ctx), table]).withData({ subscriptions });
269
+ * ```
270
+ */
271
+ withData(data) {
272
+ const outputs = [...this.#outputs];
273
+ const last = outputs.pop();
274
+ const replaced = last === undefined
275
+ ? new IntentOutput({ data })
276
+ : new IntentOutput({ ...(last.title === undefined ? {} : { title: last.title }), ...(last.summary === undefined ? {} : { summary: last.summary }), ...(last.result === undefined ? {} : { result: last.result }), ...(last.style === undefined ? {} : { style: last.style }), data });
277
+ return new IntentOutputs([...outputs, replaced], this.exitCode === undefined ? {} : { exitCode: this.exitCode });
278
+ }
279
+ /** The same outputs with another exit code. */
280
+ withExitCode(exitCode) {
281
+ return new IntentOutputs([...this.#outputs], { exitCode });
282
+ }
283
+ [Symbol.iterator]() {
284
+ return this.#outputs[Symbol.iterator]();
285
+ }
286
+ }
@@ -0,0 +1,144 @@
1
+ import type { ContributorDefinition } from "./config.js";
2
+ import type { Environment, EngineHandle, ProjectInfo } from "./context.js";
3
+ import type { EntityKindOptions, EntityReader } from "./entities.js";
4
+ import type { EventFor, EventForPattern, EventPattern, EventPayload, Listener, ListenerOptions } from "./events.js";
5
+ import type { IntentDefinition, IntentWrapper, IntentInputOf } from "./intents.js";
6
+ import type { EntityKind, EntityType, IntentReturn, ServiceName, ServiceType } from "./registry.js";
7
+ import type { Reporter } from "./reporter.js";
8
+ import type { SousSchema } from "./schema.js";
9
+ import type { ServiceFactory, ServiceOptions, ServiceReader, ServiceWrapper } from "./services.js";
10
+ /**
11
+ * A plugin: a module that registers contributors, intents, listeners, services and entities. Its module's default export is the definition.
12
+ */
13
+ export interface PluginDefinition {
14
+ /** The package name: `sous-plugin-diagrams`, `@sous-io/plugin-skills`. */
15
+ name: string;
16
+ /** Its version, checked against other plugins' `dependsOn`. */
17
+ version?: string;
18
+ /** One sentence saying what it adds. */
19
+ description?: string;
20
+ /** Plugins this one needs, with semver ranges. Sous stops before any intent runs when one is missing. */
21
+ dependsOn?: Readonly<Record<string, string>>;
22
+ /** The default `before` of every contributor it adds. */
23
+ before?: readonly string[];
24
+ /** The default `after` of every contributor it adds. */
25
+ after?: readonly string[];
26
+ /**
27
+ * Service and intent names this plugin's contract declares for other plugins to implement
28
+ * (`compiler.namespace-resolver`). Another plugin registering one is expected, so sous does not
29
+ * warn that it starts with this plugin's prefix.
30
+ */
31
+ implementable?: readonly string[];
32
+ /** Runs once, when the plugin loads during config building. */
33
+ register(sous: Sous): void | Promise<void>;
34
+ }
35
+ /**
36
+ * Declares a plugin. It only gives the definition its type.
37
+ *
38
+ * ```ts
39
+ * export default definePlugin({
40
+ * name: "sous-plugin-diagrams",
41
+ * version: "1.0.0",
42
+ * dependsOn: { "@sous-io/plugin-skills": "^1.0.0" },
43
+ *
44
+ * register(sous) {
45
+ * sous.intents.add({ intent: "diagrams.render", handle: async () => ({ rendered: 0 }) });
46
+ * },
47
+ * });
48
+ * ```
49
+ */
50
+ export declare function definePlugin<const T extends PluginDefinition>(definition: T): T;
51
+ /** A front end: the CLI, the MCP server, a web GUI. */
52
+ export interface FrontEndDefinition {
53
+ /** `cli`, `mcp`, `web`. */
54
+ id: string;
55
+ /** Takes over after the config is built: turns its input into submitted chains. */
56
+ start(engine: EngineHandle, argv: readonly string[]): void | Promise<void>;
57
+ /**
58
+ * Whether `argv` needs the built config (`boolean = true`). It is asked on the config of the
59
+ * built-in plugins' registrations alone; `false` lets the application start the front end without
60
+ * building the config when the project loads no further plugin.
61
+ */
62
+ needsConfig?(engine: EngineHandle, argv: readonly string[]): boolean;
63
+ }
64
+ /** `sous.intents`. */
65
+ export interface IntentRegistry {
66
+ /** Adds an intent. Two plugins adding one name is an error naming both, unless `intents.replace` picks one. */
67
+ add<const N extends string, S extends SousSchema = SousSchema>(definition: IntentDefinition<N, S>): void;
68
+ /** Wraps an intent's handler. */
69
+ wrap<N extends string>(intent: N, wrapper: IntentWrapper<IntentInputOf<N, unknown>, IntentReturn<N>>): void;
70
+ }
71
+ /** `sous.services`. */
72
+ export interface ServiceRegistry extends ServiceReader {
73
+ /** Adds a service with a lifetime. */
74
+ add<N extends ServiceName>(name: N, factory: ServiceFactory<ServiceType<N>>, options?: ServiceOptions): void;
75
+ /** Wraps a service. Wrappers stack in plugin order. */
76
+ wrap<N extends ServiceName>(name: N, wrapper: ServiceWrapper<ServiceType<N>>): void;
77
+ /**
78
+ * Replaces a service. Two plugins replacing one is an error, unless the config picks one
79
+ * (`services.replace."<name>": "<plugin>"`); sous then warns on every run.
80
+ */
81
+ replace<N extends ServiceName>(name: N, factory: ServiceFactory<ServiceType<N>>, options?: ServiceOptions): void;
82
+ }
83
+ /** `sous.entities`. */
84
+ export interface EntityRegistry extends EntityReader {
85
+ /** Defines a kind, with the schema every entry is checked against. */
86
+ define(kind: string, schema: SousSchema, options?: EntityKindOptions): void;
87
+ /**
88
+ * Adds an entity whose existence does not depend on the config.
89
+ *
90
+ * An entity's methods must not use `this`: the engine stores a copy of each entry (its schema
91
+ * check and its method timing make one), so `this` is not the object the plugin made. Close over
92
+ * what a method needs instead:
93
+ *
94
+ * ```ts
95
+ * const root = ".codex";
96
+ *
97
+ * sous.entities.add("skill-target", {
98
+ * id: "codex",
99
+ * directory: () => root, // not `() => this.root`
100
+ * });
101
+ * ```
102
+ */
103
+ add<K extends EntityKind>(kind: K, entry: EntityType<K>): void;
104
+ }
105
+ /**
106
+ * What `register(sous)` receives: everything a plugin uses, scoped to that plugin, so every
107
+ * registration is recorded with it.
108
+ */
109
+ export interface Sous {
110
+ /** The plugin registering. */
111
+ readonly plugin: {
112
+ readonly name: string;
113
+ readonly version: string | undefined;
114
+ };
115
+ readonly project: ProjectInfo | undefined;
116
+ readonly env: Environment;
117
+ /** The engine's reporter, for messages outside any chain. */
118
+ readonly reporter: Reporter;
119
+ /** The engine, as front ends and background intents use it. */
120
+ readonly engine: EngineHandle;
121
+ readonly config: {
122
+ /** Adds a contributor. */
123
+ contribute(definition: ContributorDefinition): void;
124
+ };
125
+ readonly intents: IntentRegistry;
126
+ readonly services: ServiceRegistry;
127
+ readonly entities: EntityRegistry;
128
+ readonly plugins: {
129
+ /** Brings in another plugin as this one's own dependency; users may still disable it by name. */
130
+ add(plugin: PluginDefinition | string): Promise<void>;
131
+ };
132
+ readonly frontEnds: {
133
+ /** Adds a front end. */
134
+ add(definition: FrontEndDefinition): void;
135
+ };
136
+ /** Listens for an event by name, wildcard or regular expression. */
137
+ on<const P extends EventPattern>(pattern: P, listener: Listener<EventForPattern<P>>, options?: ListenerOptions): void;
138
+ /** Listens once, then removes itself. */
139
+ once<const P extends EventPattern>(pattern: P, listener: Listener<EventForPattern<P>>, options?: ListenerOptions): void;
140
+ /** Removes a listener added with the same pattern. */
141
+ off(pattern: EventPattern, listener: Listener<never>): void;
142
+ /** Fires an event and waits for every listener. */
143
+ emit<N extends string>(name: N, payload?: EventPayload<N>): Promise<EventFor<N>>;
144
+ }
package/dist/plugin.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Declares a plugin. It only gives the definition its type.
3
+ *
4
+ * ```ts
5
+ * export default definePlugin({
6
+ * name: "sous-plugin-diagrams",
7
+ * version: "1.0.0",
8
+ * dependsOn: { "@sous-io/plugin-skills": "^1.0.0" },
9
+ *
10
+ * register(sous) {
11
+ * sous.intents.add({ intent: "diagrams.render", handle: async () => ({ rendered: 0 }) });
12
+ * },
13
+ * });
14
+ * ```
15
+ */
16
+ export function definePlugin(definition) {
17
+ return definition;
18
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Asking questions. Services never prompt: they ask through the `Prompter` the front end
3
+ * handed over with the chain (`ctx.prompter`). Every question names the flag or argument that
4
+ * answers it without a prompt, so an agent can do everything a person can.
5
+ */
6
+ /** What every question carries. */
7
+ export interface QuestionBase {
8
+ /** The question, as a person would read it: `Remove the 3 files listed above?` */
9
+ message: string;
10
+ /**
11
+ * The flag or argument that answers this question ahead of time (`--yes`, `--harness <id>`).
12
+ * A prompter that cannot ask names it in its error.
13
+ */
14
+ answeredBy?: string;
15
+ }
16
+ /** A question answered with free text. */
17
+ export interface ValueQuestion extends QuestionBase {
18
+ /** The answer used when the person just presses enter. */
19
+ default?: string;
20
+ /** Returns a message when the answer is not acceptable, so the prompter asks again. */
21
+ validate?: (answer: string) => string | undefined;
22
+ /** Hides what is typed, for secrets. */
23
+ secret?: boolean;
24
+ }
25
+ /** One option of a {@link ChoiceQuestion}. */
26
+ export interface Choice<T> {
27
+ /** What the person sees. */
28
+ label: string;
29
+ value: T;
30
+ /** One more line about the option. */
31
+ description?: string;
32
+ }
33
+ /** A question answered by picking one option. */
34
+ export interface ChoiceQuestion<T> extends QuestionBase {
35
+ choices: ReadonlyArray<Choice<T>>;
36
+ /** The index of the option picked when the person just presses enter. */
37
+ defaultIndex?: number;
38
+ }
39
+ /** A yes-or-no question. */
40
+ export interface ConfirmQuestion extends QuestionBase {
41
+ /** The answer used when the person just presses enter. */
42
+ default?: boolean;
43
+ }
44
+ /** Where a chain's questions go. Each front end supplies its own. */
45
+ export interface Prompter {
46
+ /** Whether this prompter can ask anything at all in this run. */
47
+ readonly interactive: boolean;
48
+ /** Asks for free text. Throws `NonInteractiveError` naming `answeredBy` when it cannot ask. */
49
+ askValue(question: ValueQuestion): Promise<string>;
50
+ /** Asks the person to pick one option. */
51
+ askChoice<T>(question: ChoiceQuestion<T>): Promise<T>;
52
+ /** Asks a yes-or-no question. */
53
+ confirm(question: ConfirmQuestion): Promise<boolean>;
54
+ }
55
+ /**
56
+ * A prompter that never asks: every question fails with a `NonInteractiveError` naming the flag that
57
+ * answers it. Used when nothing can answer, such as in CI or a background intent.
58
+ */
59
+ export declare const nonInteractivePrompter: Prompter;
60
+ /**
61
+ * A prompter that answers from a list of prepared answers, in order, for tests and for front ends
62
+ * that collect answers up front. Running out of answers fails like {@link nonInteractivePrompter}.
63
+ */
64
+ export declare function scriptedPrompter(answers: unknown[]): Prompter & {
65
+ readonly asked: string[];
66
+ };
@@ -0,0 +1,32 @@
1
+ import { NonInteractiveError } from "./errors.js";
2
+ /**
3
+ * A prompter that never asks: every question fails with a `NonInteractiveError` naming the flag that
4
+ * answers it. Used when nothing can answer, such as in CI or a background intent.
5
+ */
6
+ export const nonInteractivePrompter = {
7
+ interactive: false,
8
+ askValue: (question) => Promise.reject(new NonInteractiveError(question.message, question.answeredBy)),
9
+ askChoice: (question) => Promise.reject(new NonInteractiveError(question.message, question.answeredBy)),
10
+ confirm: (question) => Promise.reject(new NonInteractiveError(question.message, question.answeredBy)),
11
+ };
12
+ /**
13
+ * A prompter that answers from a list of prepared answers, in order, for tests and for front ends
14
+ * that collect answers up front. Running out of answers fails like {@link nonInteractivePrompter}.
15
+ */
16
+ export function scriptedPrompter(answers) {
17
+ const asked = [];
18
+ const next = (question) => {
19
+ asked.push(question.message);
20
+ if (answers.length === 0) {
21
+ return Promise.reject(new NonInteractiveError(question.message, question.answeredBy));
22
+ }
23
+ return Promise.resolve(answers.shift());
24
+ };
25
+ return {
26
+ interactive: true,
27
+ asked,
28
+ askValue: (question) => next(question),
29
+ askChoice: (question) => next(question),
30
+ confirm: (question) => next(question),
31
+ };
32
+ }
@@ -0,0 +1,91 @@
1
+ import type { IntentOutputs, PlainObject } from "./outputs.js";
2
+ import type { ConfigKeyEntity, PluginLocatorEntity } from "./entities.js";
3
+ import type { ConfigBuiltPayload } from "./events.js";
4
+ /**
5
+ * The interfaces plugins add their own types to, through TypeScript declaration merging. At run time everything is looked up by its string name; these only
6
+ * give the names their types.
7
+ *
8
+ * ```ts
9
+ * declare module "@sous-io/plugin-api" {
10
+ * interface SousServices {
11
+ * "diagrams.renderer": Renderer;
12
+ * }
13
+ *
14
+ * interface SousIntents {
15
+ * "diagrams.render": { input: { only?: string }; result: { rendered: number } };
16
+ * }
17
+ *
18
+ * interface SousEvents {
19
+ * "diagrams.rendered": { files: RenderResult[] };
20
+ * }
21
+ *
22
+ * interface SousEntities {
23
+ * "harness": HarnessEntity;
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ export interface SousServices {
29
+ }
30
+ /** Each intent's input and result: `"name": { input: ...; result: ... }`. */
31
+ export interface SousIntents {
32
+ }
33
+ /** Each custom event's payload: `"diagrams.rendered": { files: RenderResult[] }`. */
34
+ export interface SousEvents {
35
+ }
36
+ /** Each entity kind's entry type: `"harness": HarnessEntity`. */
37
+ export interface SousEntities {
38
+ }
39
+ /**
40
+ * The type of each top-level config key, for `ctx.config.get("diagrams")`. Optional: a key with no
41
+ * entry here reads as `unknown`. (Added beside the four interfaces core names; it follows the same
42
+ * pattern for the keys plugins add as `config-key` entities.)
43
+ */
44
+ export interface SousConfig {
45
+ }
46
+ /** The entity kinds core itself defines. */
47
+ export interface CoreEntities {
48
+ /** A key the config may hold, with its schema. */
49
+ "config-key": ConfigKeyEntity;
50
+ /** A way to find a plugin's module from the specifier in `plugins.load`. */
51
+ "plugin-locator": PluginLocatorEntity;
52
+ }
53
+ /** The events core itself fires. */
54
+ export interface CoreEvents {
55
+ /** Fired once each time the config has been built (never for "what if" builds). */
56
+ "config.built": ConfigBuiltPayload;
57
+ }
58
+ /** A name any of these registries accepts: a known one, or any string. */
59
+ export type AnyName<Known> = (Known & string) | (string & {});
60
+ /** Every known service name, plus any string. */
61
+ export type ServiceName = AnyName<keyof SousServices>;
62
+ /** The type of a service, or `unknown` when nothing declares it. */
63
+ export type ServiceType<N extends string> = N extends keyof SousServices ? SousServices[N] : unknown;
64
+ /** Every known intent name, plus any string. */
65
+ export type IntentName = AnyName<keyof SousIntents>;
66
+ /** The input type an intent declared, or `unknown`. */
67
+ export type IntentInput<N extends string> = N extends keyof SousIntents ? SousIntents[N] extends {
68
+ input: infer I;
69
+ } ? I : unknown : unknown;
70
+ /** The result type an intent declared, or `unknown`. */
71
+ export type IntentResult<N extends string> = N extends keyof SousIntents ? SousIntents[N] extends {
72
+ result: infer R;
73
+ } ? R : unknown : unknown;
74
+ /**
75
+ * What an intent's handler returns: its outputs, whose `toPlain()` is the declared result (or a plain
76
+ * object when the intent declares none).
77
+ */
78
+ export type IntentReturn<N extends string> = IntentOutputs<IntentResult<N> extends object ? IntentResult<N> : PlainObject>;
79
+ /** Every entity kind, core's and the plugins'. */
80
+ export type AllEntities = CoreEntities & SousEntities;
81
+ /** Every known entity kind, plus any string. */
82
+ export type EntityKind = AnyName<keyof AllEntities>;
83
+ /** What every entity has. */
84
+ export interface EntityBase {
85
+ /** Unique within its kind. */
86
+ id: string;
87
+ }
88
+ /** The entry type of an entity kind, or {@link EntityBase} when nothing declares it. */
89
+ export type EntityType<K extends string> = K extends keyof AllEntities ? AllEntities[K] : EntityBase & Record<string, unknown>;
90
+ /** The type of a top-level config key, or `unknown`. */
91
+ export type ConfigKeyType<K extends string> = K extends keyof SousConfig ? SousConfig[K] : unknown;
@@ -0,0 +1 @@
1
+ export {};