@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.
- package/LICENSE +201 -0
- package/README.md +97 -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 -3
package/dist/schema.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Turns a small JSON Schema into a schema sous accepts, with no schema library. It checks the
|
|
14
|
+
* subset {@link JsonSchema} lists, fills in `default`s, and gives its JSON Schema back to front ends.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* sous.entities.add("cli-global-argument", {
|
|
18
|
+
* id: "debug",
|
|
19
|
+
* schema: jsonSchema<boolean>({ type: "boolean", default: false }),
|
|
20
|
+
* description: "Show every message, debug ones included",
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function jsonSchema(schema) {
|
|
25
|
+
const converter = {
|
|
26
|
+
input: () => ({ ...schema }),
|
|
27
|
+
output: () => ({ ...schema }),
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
"~standard": {
|
|
31
|
+
version: 1,
|
|
32
|
+
vendor: "sous",
|
|
33
|
+
validate(value) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
const result = check(schema, value, [], issues);
|
|
36
|
+
if (issues.length > 0) {
|
|
37
|
+
return { issues: issues.map((issue) => ({ message: issue.message, path: issue.path })) };
|
|
38
|
+
}
|
|
39
|
+
return { value: result };
|
|
40
|
+
},
|
|
41
|
+
jsonSchema: converter,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The JSON Schema of any schema, when it has one: through the Standard JSON Schema converter
|
|
47
|
+
* (zod 4 and {@link jsonSchema} have it). `undefined` when the schema cannot describe itself.
|
|
48
|
+
*/
|
|
49
|
+
export function toJsonSchema(schema) {
|
|
50
|
+
const converter = schema?.["~standard"]
|
|
51
|
+
?.jsonSchema;
|
|
52
|
+
if (!converter) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return converter.input({ target: "draft-2020-12" });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Checks a value against any schema and returns the checked value (with defaults filled in) or the
|
|
64
|
+
* issues, each with a path string. Asynchronous schemas are awaited.
|
|
65
|
+
*/
|
|
66
|
+
export async function validateWith(schema, value) {
|
|
67
|
+
const result = await schema["~standard"].validate(value);
|
|
68
|
+
return result.issues ? { issues: result.issues.map(toSchemaIssue) } : { value: result.value };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The synchronous form of {@link validateWith}. A schema that answers with a promise is reported
|
|
72
|
+
* as one issue, since nothing can wait for it here.
|
|
73
|
+
*/
|
|
74
|
+
export function validateWithSync(schema, value) {
|
|
75
|
+
const result = schema["~standard"].validate(value);
|
|
76
|
+
if (result instanceof Promise) {
|
|
77
|
+
return { issues: [{ path: "", message: "This schema checks values asynchronously, which is not supported here." }] };
|
|
78
|
+
}
|
|
79
|
+
return result.issues ? { issues: result.issues.map(toSchemaIssue) } : { value: result.value };
|
|
80
|
+
}
|
|
81
|
+
/** Whether a value looks like a schema sous accepts. */
|
|
82
|
+
export function isSchema(value) {
|
|
83
|
+
const props = value?.["~standard"];
|
|
84
|
+
return typeof props?.validate === "function";
|
|
85
|
+
}
|
|
86
|
+
function toSchemaIssue(issue) {
|
|
87
|
+
const segments = (issue.path ?? []).map((segment) => typeof segment === "object" && segment !== null ? segment.key : segment);
|
|
88
|
+
return { path: formatIssuePath(segments), message: issue.message };
|
|
89
|
+
}
|
|
90
|
+
/** Writes issue path segments the way sous writes paths: `a.b[2]."c.d"`. */
|
|
91
|
+
export function formatIssuePath(segments) {
|
|
92
|
+
let out = "";
|
|
93
|
+
for (const segment of segments) {
|
|
94
|
+
if (typeof segment === "number") {
|
|
95
|
+
out += `[${segment}]`;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const key = String(segment);
|
|
99
|
+
const part = /^[A-Za-z_$][\w$-]*$/.test(key) ? key : JSON.stringify(key);
|
|
100
|
+
out += out === "" ? part : `.${part}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function check(schema, value, path, issues) {
|
|
106
|
+
if (value === undefined && schema.default !== undefined) {
|
|
107
|
+
value = structuredClone(schema.default);
|
|
108
|
+
}
|
|
109
|
+
if (schema.anyOf) {
|
|
110
|
+
for (const option of schema.anyOf) {
|
|
111
|
+
const optionIssues = [];
|
|
112
|
+
const result = check(option, value, path, optionIssues);
|
|
113
|
+
if (optionIssues.length === 0) {
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
issues.push({ path: path, message: "Expected a value matching one of the allowed forms." });
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
if (schema.const !== undefined && !sameJson(value, schema.const)) {
|
|
121
|
+
issues.push({ path: path, message: `Expected ${JSON.stringify(schema.const)}.` });
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if (schema.enum && !schema.enum.some((option) => sameJson(option, value))) {
|
|
125
|
+
const options = schema.enum.map((option) => JSON.stringify(option)).join(", ");
|
|
126
|
+
issues.push({ path: path, message: `Expected one of ${options}.` });
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
if (schema.type !== undefined) {
|
|
130
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
131
|
+
if (!types.some((type) => hasType(value, type))) {
|
|
132
|
+
const got = value === undefined ? "nothing" : describeType(value);
|
|
133
|
+
issues.push({ path: path, message: `Expected ${types.join(" or ")}, got ${got}.` });
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isPlainObject(value) && (schema.properties || schema.required || schema.additionalProperties !== undefined)) {
|
|
138
|
+
return checkObject(schema, value, path, issues);
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(value) && schema.items) {
|
|
141
|
+
return value.map((item, index) => check(schema.items, item, [...path, index], issues));
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function checkObject(schema, value, path, issues) {
|
|
146
|
+
const out = {};
|
|
147
|
+
const properties = schema.properties ?? {};
|
|
148
|
+
for (const name of schema.required ?? []) {
|
|
149
|
+
if (value[name] === undefined && properties[name]?.default === undefined) {
|
|
150
|
+
issues.push({ path: [...path, name], message: "Required, but missing." });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
154
|
+
// A missing optional property is fine; required ones were reported above.
|
|
155
|
+
if (value[name] === undefined && propertySchema.default === undefined) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const checked = check(propertySchema, value[name], [...path, name], issues);
|
|
159
|
+
if (checked !== undefined) {
|
|
160
|
+
out[name] = checked;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const [name, item] of Object.entries(value)) {
|
|
164
|
+
if (name in properties) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (schema.additionalProperties === false) {
|
|
168
|
+
issues.push({ path: [...path, name], message: "Not a known key." });
|
|
169
|
+
}
|
|
170
|
+
else if (typeof schema.additionalProperties === "object") {
|
|
171
|
+
out[name] = check(schema.additionalProperties, item, [...path, name], issues);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
out[name] = item;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function hasType(value, type) {
|
|
180
|
+
switch (type) {
|
|
181
|
+
case "string":
|
|
182
|
+
return typeof value === "string";
|
|
183
|
+
case "number":
|
|
184
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
185
|
+
case "integer":
|
|
186
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
187
|
+
case "boolean":
|
|
188
|
+
return typeof value === "boolean";
|
|
189
|
+
case "null":
|
|
190
|
+
return value === null;
|
|
191
|
+
case "array":
|
|
192
|
+
return Array.isArray(value);
|
|
193
|
+
case "object":
|
|
194
|
+
return isPlainObject(value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function describeType(value) {
|
|
198
|
+
if (value === null) {
|
|
199
|
+
return "null";
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
return "an array";
|
|
203
|
+
}
|
|
204
|
+
return typeof value === "object" ? "an object" : `${typeof value} ${JSON.stringify(value)}`;
|
|
205
|
+
}
|
|
206
|
+
function isPlainObject(value) {
|
|
207
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
208
|
+
}
|
|
209
|
+
function sameJson(a, b) {
|
|
210
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
211
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { BuiltConfig, ConfigReader } from "./config.js";
|
|
2
|
+
import type { Environment, ProjectInfo } from "./context.js";
|
|
3
|
+
import type { EntityReader } from "./entities.js";
|
|
4
|
+
import type { ServiceName, ServiceType } from "./registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* How long one service object lives:
|
|
7
|
+
* - `singleton`: one for the whole process, for services that never look at the config;
|
|
8
|
+
* - `config`: one per config build, shared by every chain using that config;
|
|
9
|
+
* - `factory`: a new one on every get.
|
|
10
|
+
*
|
|
11
|
+
* **While the config is being built, only singletons can be used.** A contributor (or a plugin's
|
|
12
|
+
* `register`) that gets a `config` or `factory` service through `sous.services` gets a
|
|
13
|
+
* `SousError`: there is no built config to make it for yet, and what it read would not be
|
|
14
|
+
* recorded, so the contributor would not re-run when it changed. A service that contributors need
|
|
15
|
+
* must be a singleton that takes what it needs from the config as arguments, and reads nothing of
|
|
16
|
+
* the config itself:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* sous.services.add("diagrams.layout", () => new Layout(), { lifetime: "singleton" });
|
|
20
|
+
*
|
|
21
|
+
* sous.config.contribute({
|
|
22
|
+
* id: "diagrams",
|
|
23
|
+
* contribute(config) {
|
|
24
|
+
* const layout = sous.services.get("diagrams.layout");
|
|
25
|
+
*
|
|
26
|
+
* // the contributor reads through the builder, so its reads are recorded
|
|
27
|
+
* config.atPath("diagrams.boxes").set(layout.boxes(config.atPath("diagrams.style").get()));
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* A singleton is made with the config of its first use; it must not keep that config.
|
|
33
|
+
*/
|
|
34
|
+
export type ServiceLifetime = "singleton" | "config" | "factory";
|
|
35
|
+
/** What a service factory and a service wrapper get besides the config. */
|
|
36
|
+
export interface ServiceContext {
|
|
37
|
+
/** The plugin whose factory or wrapper this is. */
|
|
38
|
+
readonly plugin: string;
|
|
39
|
+
/** The config this object is made for. A singleton gets the config of its first use. */
|
|
40
|
+
readonly config: BuiltConfig;
|
|
41
|
+
/** Other services, for the same config. */
|
|
42
|
+
readonly services: ServiceReader;
|
|
43
|
+
readonly entities: EntityReader;
|
|
44
|
+
readonly project: ProjectInfo | undefined;
|
|
45
|
+
readonly env: Environment;
|
|
46
|
+
/**
|
|
47
|
+
* The engine's working directory (where the process started). Core always sets it; it is
|
|
48
|
+
* optional only so contexts made by hand (tests, adapters) need not. A service shared by chains
|
|
49
|
+
* from several front ends should take a chain's `ctx.cwd` as an argument instead.
|
|
50
|
+
*/
|
|
51
|
+
readonly cwd?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Makes a service object. */
|
|
54
|
+
export type ServiceFactory<T> = (config: ConfigReader, ctx: ServiceContext) => T;
|
|
55
|
+
/** Wraps a service object. Wrappers stack in plugin order, the last plugin's outermost. */
|
|
56
|
+
export type ServiceWrapper<T> = (inner: T, ctx: ServiceContext) => T;
|
|
57
|
+
/** Options for adding or replacing a service. */
|
|
58
|
+
export interface ServiceOptions {
|
|
59
|
+
/** Default `config`. */
|
|
60
|
+
lifetime?: ServiceLifetime;
|
|
61
|
+
}
|
|
62
|
+
/** Reading services. */
|
|
63
|
+
export interface ServiceReader {
|
|
64
|
+
/** The service, created on first use. Throws `NotFoundError` when no plugin provides it. */
|
|
65
|
+
get<N extends ServiceName>(name: N): ServiceType<N>;
|
|
66
|
+
/** Whether an active plugin provides it. */
|
|
67
|
+
has(name: string): boolean;
|
|
68
|
+
}
|
package/dist/services.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing and budgets. Every core stage and every call into a plugin is timed, and each has a
|
|
3
|
+
* budget, changeable under the `budgets` config key (`false` turns a warning off).
|
|
4
|
+
*/
|
|
5
|
+
/** What was timed. */
|
|
6
|
+
export type TimingCategory = "config.build" | "config.stage" | "config.pass" | "config.validate" | "contributor" | "plugin.load" | "plugin.register" | "chain" | "intent" | "listener" | "service.create" | "service.method" | "entity.method";
|
|
7
|
+
/** Every timing category, for checks that each has a budget. */
|
|
8
|
+
export declare const TIMING_CATEGORIES: readonly TimingCategory[];
|
|
9
|
+
/** Everything timed under one key (`intent:build`, `contributor:confd/610-mac.jsonc`). */
|
|
10
|
+
export interface TimingEntry {
|
|
11
|
+
/** `<category>:<name>`. */
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly category: TimingCategory;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** The plugin whose code ran, when it was plugin code. */
|
|
16
|
+
readonly plugin: string | undefined;
|
|
17
|
+
readonly count: number;
|
|
18
|
+
readonly totalMs: number;
|
|
19
|
+
readonly maxMs: number;
|
|
20
|
+
/** The budget each call is held to, or `false` when its warning is off. */
|
|
21
|
+
readonly budgetMs: number | false;
|
|
22
|
+
/** How many calls went over budget. */
|
|
23
|
+
readonly overBudget: number;
|
|
24
|
+
}
|
|
25
|
+
/** Reading timings. */
|
|
26
|
+
export interface Timings {
|
|
27
|
+
/** Every entry, in the order first timed. */
|
|
28
|
+
entries(): TimingEntry[];
|
|
29
|
+
/** One entry, by key. */
|
|
30
|
+
get(key: string): TimingEntry | undefined;
|
|
31
|
+
}
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing and budgets. Every core stage and every call into a plugin is timed, and each has a
|
|
3
|
+
* budget, changeable under the `budgets` config key (`false` turns a warning off).
|
|
4
|
+
*/
|
|
5
|
+
/** Every timing category, for checks that each has a budget. */
|
|
6
|
+
export const TIMING_CATEGORIES = [
|
|
7
|
+
"config.build",
|
|
8
|
+
"config.stage",
|
|
9
|
+
"config.pass",
|
|
10
|
+
"config.validate",
|
|
11
|
+
"contributor",
|
|
12
|
+
"plugin.load",
|
|
13
|
+
"plugin.register",
|
|
14
|
+
"chain",
|
|
15
|
+
"intent",
|
|
16
|
+
"listener",
|
|
17
|
+
"service.create",
|
|
18
|
+
"service.method",
|
|
19
|
+
"entity.method",
|
|
20
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sous-io/plugin-api",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "The plugin interface of sous: definePlugin, the shared type interfaces, Reporter, Prompter and the error classes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/sous-io/sous.git",
|
|
10
|
+
"directory": "packages/plugin-api"
|
|
11
|
+
},
|
|
12
|
+
"private": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"source": "./src/index.ts",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"typecheck": "tsc --noEmit -p .",
|
|
34
|
+
"build": "tsc -p tsconfig.build.json"
|
|
35
|
+
}
|
|
6
36
|
}
|