@outfitter/schema 0.2.0
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 +126 -0
- package/dist/diff.d.ts +4 -0
- package/dist/diff.js +7 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +7 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.js +7 -0
- package/dist/shared/@outfitter/schema-cq7f2r5z.js +32 -0
- package/dist/shared/@outfitter/schema-d4xhpz76.js +91 -0
- package/dist/shared/@outfitter/schema-qm3b0skz.d.ts +26 -0
- package/dist/shared/@outfitter/schema-tnrxpckr.js +85 -0
- package/dist/shared/@outfitter/schema-vzq7nzs3.d.ts +59 -0
- package/dist/shared/@outfitter/schema-wgnvy5zh.d.ts +44 -0
- package/dist/shared/@outfitter/schema-xqcdyq2s.d.ts +21 -0
- package/dist/shared/@outfitter/schema-y9k7tad6.js +180 -0
- package/dist/surface.d.ts +3 -0
- package/dist/surface.js +14 -0
- package/package.json +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @outfitter/schema
|
|
2
|
+
|
|
3
|
+
Schema introspection, surface map generation, and drift detection for Outfitter action registries.
|
|
4
|
+
|
|
5
|
+
**Stability: Active** -- APIs evolving based on usage.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @outfitter/schema zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`zod` is a peer dependency because schema generation converts Zod input/output contracts into JSON Schema.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createActionRegistry, defineAction, Result } from "@outfitter/contracts";
|
|
19
|
+
import { generateSurfaceMap } from "@outfitter/schema/surface";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
const registry = createActionRegistry().add(
|
|
23
|
+
defineAction({
|
|
24
|
+
id: "doctor",
|
|
25
|
+
description: "Validate environment",
|
|
26
|
+
surfaces: ["cli", "mcp"],
|
|
27
|
+
input: z.object({ verbose: z.boolean().optional() }),
|
|
28
|
+
handler: async () => Result.ok({ ok: true }),
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const surface = generateSurfaceMap(registry, {
|
|
33
|
+
version: "1.0.0",
|
|
34
|
+
generator: "runtime",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log(surface.actions.map((a) => a.id));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Subpath Exports
|
|
41
|
+
|
|
42
|
+
| Subpath | What's In It |
|
|
43
|
+
|---------|---------------|
|
|
44
|
+
| `@outfitter/schema` | Full public API from all modules |
|
|
45
|
+
| `@outfitter/schema/manifest` | `generateManifest`, `ActionManifest*` types |
|
|
46
|
+
| `@outfitter/schema/surface` | `generateSurfaceMap`, snapshot path + read/write helpers |
|
|
47
|
+
| `@outfitter/schema/diff` | `diffSurfaceMaps`, diff result types |
|
|
48
|
+
| `@outfitter/schema/markdown` | `formatManifestMarkdown`, markdown formatting options |
|
|
49
|
+
|
|
50
|
+
## Core Workflows
|
|
51
|
+
|
|
52
|
+
### 1) Generate a manifest from an action registry
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { generateManifest } from "@outfitter/schema/manifest";
|
|
56
|
+
|
|
57
|
+
const manifest = generateManifest(registry, {
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
surface: "mcp", // optional: cli | mcp | api
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(manifest.errors.validation.exit); // 1
|
|
63
|
+
console.log(manifest.outputModes); // ["human", "json", "jsonl", "tree", "table"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2) Generate and persist a surface map snapshot
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
generateSurfaceMap,
|
|
71
|
+
resolveSnapshotPath,
|
|
72
|
+
writeSurfaceMap,
|
|
73
|
+
} from "@outfitter/schema/surface";
|
|
74
|
+
|
|
75
|
+
const surface = generateSurfaceMap(registry, {
|
|
76
|
+
version: "1.0.0",
|
|
77
|
+
generator: "build",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const path = resolveSnapshotPath(process.cwd(), ".outfitter", "v1.0.0");
|
|
81
|
+
await writeSurfaceMap(surface, path);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3) Detect schema drift in CI
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { generateSurfaceMap, readSurfaceMap } from "@outfitter/schema/surface";
|
|
88
|
+
import { diffSurfaceMaps } from "@outfitter/schema/diff";
|
|
89
|
+
|
|
90
|
+
const committed = await readSurfaceMap(".outfitter/snapshots/v1.0.0.json");
|
|
91
|
+
const current = generateSurfaceMap(registry, { version: "1.0.0" });
|
|
92
|
+
|
|
93
|
+
const diff = diffSurfaceMaps(committed, current);
|
|
94
|
+
if (diff.hasChanges) {
|
|
95
|
+
console.error("Surface drift detected", diff);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4) Generate markdown reference docs
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { formatManifestMarkdown } from "@outfitter/schema/markdown";
|
|
104
|
+
|
|
105
|
+
const manifest = generateManifest(registry, { surface: "mcp", version: "1.0.0" });
|
|
106
|
+
|
|
107
|
+
const markdown = formatManifestMarkdown(manifest, {
|
|
108
|
+
surface: "mcp",
|
|
109
|
+
title: "MCP Tool Reference",
|
|
110
|
+
toc: true,
|
|
111
|
+
timestamp: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await Bun.write("docs/reference/tools.md", markdown);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API Notes
|
|
118
|
+
|
|
119
|
+
- `generateManifest()` accepts either an `ActionRegistry` or a plain array of action specs.
|
|
120
|
+
- `generateSurfaceMap()` wraps manifest output with envelope metadata (`$schema`, `generator`).
|
|
121
|
+
- `diffSurfaceMaps()` ignores volatile timestamps and reports granular change categories (`input`, `output`, `surfaces`, `cli`, `mcp`, `api`, metadata changes).
|
|
122
|
+
- `formatManifestMarkdown()` renders either MCP-tool docs or CLI-command docs from the same manifest source.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DiffEntry, ModifiedEntry, SurfaceMapDiff, diffSurfaceMaps } from "./shared/@outfitter/schema-qm3b0skz";
|
|
2
|
+
import "./shared/@outfitter/schema-wgnvy5zh";
|
|
3
|
+
import "./shared/@outfitter/schema-vzq7nzs3";
|
|
4
|
+
export { diffSurfaceMaps, SurfaceMapDiff, ModifiedEntry, DiffEntry };
|
package/dist/diff.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DiffEntry, ModifiedEntry, SurfaceMapDiff, diffSurfaceMaps } from "./shared/@outfitter/schema-qm3b0skz";
|
|
2
|
+
import { GenerateSurfaceMapOptions, SurfaceMap, generateSurfaceMap, readSurfaceMap, resolveSnapshotPath, writeSurfaceMap } from "./shared/@outfitter/schema-wgnvy5zh";
|
|
3
|
+
import { MarkdownFormatOptions, formatManifestMarkdown } from "./shared/@outfitter/schema-xqcdyq2s";
|
|
4
|
+
import { ActionManifest, ActionManifestEntry, ActionSource, GenerateManifestOptions, ManifestApiSpec, ManifestCliOption, ManifestCliSpec, ManifestMcpSpec, generateManifest } from "./shared/@outfitter/schema-vzq7nzs3";
|
|
5
|
+
export { writeSurfaceMap, resolveSnapshotPath, readSurfaceMap, generateSurfaceMap, generateManifest, formatManifestMarkdown, diffSurfaceMaps, SurfaceMapDiff, SurfaceMap, ModifiedEntry, MarkdownFormatOptions, ManifestMcpSpec, ManifestCliSpec, ManifestCliOption, ManifestApiSpec, GenerateSurfaceMapOptions, GenerateManifestOptions, DiffEntry, ActionSource, ActionManifestEntry, ActionManifest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
generateSurfaceMap,
|
|
4
|
+
readSurfaceMap,
|
|
5
|
+
resolveSnapshotPath,
|
|
6
|
+
writeSurfaceMap
|
|
7
|
+
} from "./shared/@outfitter/schema-cq7f2r5z.js";
|
|
8
|
+
import {
|
|
9
|
+
diffSurfaceMaps
|
|
10
|
+
} from "./shared/@outfitter/schema-d4xhpz76.js";
|
|
11
|
+
import {
|
|
12
|
+
formatManifestMarkdown
|
|
13
|
+
} from "./shared/@outfitter/schema-y9k7tad6.js";
|
|
14
|
+
import {
|
|
15
|
+
generateManifest
|
|
16
|
+
} from "./shared/@outfitter/schema-tnrxpckr.js";
|
|
17
|
+
export {
|
|
18
|
+
writeSurfaceMap,
|
|
19
|
+
resolveSnapshotPath,
|
|
20
|
+
readSurfaceMap,
|
|
21
|
+
generateSurfaceMap,
|
|
22
|
+
generateManifest,
|
|
23
|
+
formatManifestMarkdown,
|
|
24
|
+
diffSurfaceMaps
|
|
25
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ActionManifest, ActionManifestEntry, ActionSource, GenerateManifestOptions, ManifestApiSpec, ManifestCliOption, ManifestCliSpec, ManifestMcpSpec, generateManifest } from "./shared/@outfitter/schema-vzq7nzs3";
|
|
2
|
+
export { generateManifest, ManifestMcpSpec, ManifestCliSpec, ManifestCliOption, ManifestApiSpec, GenerateManifestOptions, ActionSource, ActionManifestEntry, ActionManifest };
|
package/dist/manifest.js
ADDED
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
generateManifest
|
|
4
|
+
} from "./schema-tnrxpckr.js";
|
|
5
|
+
|
|
6
|
+
// packages/schema/src/surface.ts
|
|
7
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var SURFACE_MAP_SCHEMA = "https://outfitter.dev/surface/v1";
|
|
10
|
+
function generateSurfaceMap(source, options) {
|
|
11
|
+
const manifest = generateManifest(source, options);
|
|
12
|
+
return {
|
|
13
|
+
...manifest,
|
|
14
|
+
$schema: SURFACE_MAP_SCHEMA,
|
|
15
|
+
generator: options?.generator ?? "runtime"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function writeSurfaceMap(surfaceMap, outputPath) {
|
|
19
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
20
|
+
const content = `${JSON.stringify(surfaceMap, null, 2)}
|
|
21
|
+
`;
|
|
22
|
+
await writeFile(outputPath, content, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
async function readSurfaceMap(inputPath) {
|
|
25
|
+
const content = await readFile(inputPath, "utf-8");
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
}
|
|
28
|
+
function resolveSnapshotPath(cwd, outputDir, version) {
|
|
29
|
+
return join(cwd, outputDir, "snapshots", `${version}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { generateSurfaceMap, writeSurfaceMap, readSurfaceMap, resolveSnapshotPath };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/schema/src/diff.ts
|
|
3
|
+
function stableJson(value) {
|
|
4
|
+
return JSON.stringify(value, (_key, val) => {
|
|
5
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
6
|
+
const sorted = {};
|
|
7
|
+
for (const k of Object.keys(val).sort()) {
|
|
8
|
+
sorted[k] = val[k];
|
|
9
|
+
}
|
|
10
|
+
return sorted;
|
|
11
|
+
}
|
|
12
|
+
return val;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function compareEntries(committed, current) {
|
|
16
|
+
const changes = [];
|
|
17
|
+
if (stableJson(committed.input) !== stableJson(current.input)) {
|
|
18
|
+
changes.push("input");
|
|
19
|
+
}
|
|
20
|
+
if (stableJson(committed.output) !== stableJson(current.output)) {
|
|
21
|
+
changes.push("output");
|
|
22
|
+
}
|
|
23
|
+
if (JSON.stringify([...committed.surfaces].sort()) !== JSON.stringify([...current.surfaces].sort())) {
|
|
24
|
+
changes.push("surfaces");
|
|
25
|
+
}
|
|
26
|
+
if (committed.description !== current.description) {
|
|
27
|
+
changes.push("description");
|
|
28
|
+
}
|
|
29
|
+
if (stableJson(committed.cli) !== stableJson(current.cli)) {
|
|
30
|
+
changes.push("cli");
|
|
31
|
+
}
|
|
32
|
+
if (stableJson(committed.mcp) !== stableJson(current.mcp)) {
|
|
33
|
+
changes.push("mcp");
|
|
34
|
+
}
|
|
35
|
+
if (stableJson(committed.api) !== stableJson(current.api)) {
|
|
36
|
+
changes.push("api");
|
|
37
|
+
}
|
|
38
|
+
return changes;
|
|
39
|
+
}
|
|
40
|
+
function diffSurfaceMaps(committed, current) {
|
|
41
|
+
const metadataChanges = [];
|
|
42
|
+
if (committed.version !== current.version) {
|
|
43
|
+
metadataChanges.push("version");
|
|
44
|
+
}
|
|
45
|
+
if (JSON.stringify([...committed.surfaces].sort()) !== JSON.stringify([...current.surfaces].sort())) {
|
|
46
|
+
metadataChanges.push("surfaces");
|
|
47
|
+
}
|
|
48
|
+
if (stableJson(committed.errors) !== stableJson(current.errors)) {
|
|
49
|
+
metadataChanges.push("errors");
|
|
50
|
+
}
|
|
51
|
+
if (stableJson(committed.outputModes) !== stableJson(current.outputModes)) {
|
|
52
|
+
metadataChanges.push("outputModes");
|
|
53
|
+
}
|
|
54
|
+
if ("$schema" in committed && "$schema" in current && committed.$schema !== current.$schema) {
|
|
55
|
+
metadataChanges.push("$schema");
|
|
56
|
+
}
|
|
57
|
+
const committedMap = new Map(committed.actions.map((a) => [a.id, a]));
|
|
58
|
+
const currentMap = new Map(current.actions.map((a) => [a.id, a]));
|
|
59
|
+
const added = [];
|
|
60
|
+
const removed = [];
|
|
61
|
+
const modified = [];
|
|
62
|
+
for (const [id] of currentMap) {
|
|
63
|
+
if (!committedMap.has(id)) {
|
|
64
|
+
added.push({ id });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [id] of committedMap) {
|
|
68
|
+
if (!currentMap.has(id)) {
|
|
69
|
+
removed.push({ id });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const [id, committedEntry] of committedMap) {
|
|
73
|
+
const currentEntry = currentMap.get(id);
|
|
74
|
+
if (!currentEntry) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const changes = compareEntries(committedEntry, currentEntry);
|
|
78
|
+
if (changes.length > 0) {
|
|
79
|
+
modified.push({ id, changes });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
hasChanges: added.length > 0 || removed.length > 0 || modified.length > 0 || metadataChanges.length > 0,
|
|
84
|
+
added,
|
|
85
|
+
removed,
|
|
86
|
+
modified,
|
|
87
|
+
metadataChanges
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { diffSurfaceMaps };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SurfaceMap } from "./schema-wgnvy5zh";
|
|
2
|
+
interface SurfaceMapDiff {
|
|
3
|
+
readonly hasChanges: boolean;
|
|
4
|
+
readonly added: readonly DiffEntry[];
|
|
5
|
+
readonly removed: readonly DiffEntry[];
|
|
6
|
+
readonly modified: readonly ModifiedEntry[];
|
|
7
|
+
readonly metadataChanges: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
interface DiffEntry {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
}
|
|
12
|
+
interface ModifiedEntry extends DiffEntry {
|
|
13
|
+
readonly changes: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compare two surface maps and return a structured diff.
|
|
17
|
+
*
|
|
18
|
+
* Ignores volatile fields like `generatedAt`. Reports added, removed,
|
|
19
|
+
* and modified actions with specific change categories.
|
|
20
|
+
*
|
|
21
|
+
* @param committed - The previously committed surface map
|
|
22
|
+
* @param current - The current runtime surface map
|
|
23
|
+
* @returns Structured diff result
|
|
24
|
+
*/
|
|
25
|
+
declare function diffSurfaceMaps(committed: SurfaceMap, current: SurfaceMap): SurfaceMapDiff;
|
|
26
|
+
export { SurfaceMapDiff, DiffEntry, ModifiedEntry, diffSurfaceMaps };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/schema/src/manifest.ts
|
|
3
|
+
import {
|
|
4
|
+
ACTION_SURFACES,
|
|
5
|
+
DEFAULT_REGISTRY_SURFACES,
|
|
6
|
+
exitCodeMap,
|
|
7
|
+
statusCodeMap,
|
|
8
|
+
zodToJsonSchema
|
|
9
|
+
} from "@outfitter/contracts";
|
|
10
|
+
function isActionRegistry(source) {
|
|
11
|
+
return "list" in source && typeof source.list === "function";
|
|
12
|
+
}
|
|
13
|
+
var OUTPUT_MODES = ["human", "json", "jsonl", "tree", "table"];
|
|
14
|
+
function buildErrorTaxonomy() {
|
|
15
|
+
const taxonomy = {};
|
|
16
|
+
for (const category of Object.keys(exitCodeMap)) {
|
|
17
|
+
taxonomy[category] = {
|
|
18
|
+
exit: exitCodeMap[category],
|
|
19
|
+
http: statusCodeMap[category]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return taxonomy;
|
|
23
|
+
}
|
|
24
|
+
function actionToManifestEntry(action) {
|
|
25
|
+
const surfaces = [
|
|
26
|
+
...action.surfaces ?? DEFAULT_REGISTRY_SURFACES
|
|
27
|
+
];
|
|
28
|
+
return {
|
|
29
|
+
id: action.id,
|
|
30
|
+
description: action.description,
|
|
31
|
+
surfaces,
|
|
32
|
+
input: zodToJsonSchema(action.input),
|
|
33
|
+
output: action.output ? zodToJsonSchema(action.output) : undefined,
|
|
34
|
+
cli: action.cli ? {
|
|
35
|
+
group: action.cli.group,
|
|
36
|
+
command: action.cli.command,
|
|
37
|
+
description: action.cli.description,
|
|
38
|
+
aliases: action.cli.aliases && action.cli.aliases.length > 0 ? action.cli.aliases : undefined,
|
|
39
|
+
options: action.cli.options && action.cli.options.length > 0 ? action.cli.options.map((o) => ({
|
|
40
|
+
flags: o.flags,
|
|
41
|
+
description: o.description,
|
|
42
|
+
defaultValue: o.defaultValue,
|
|
43
|
+
required: o.required
|
|
44
|
+
})) : undefined
|
|
45
|
+
} : undefined,
|
|
46
|
+
mcp: action.mcp ? {
|
|
47
|
+
tool: action.mcp.tool,
|
|
48
|
+
description: action.mcp.description,
|
|
49
|
+
deferLoading: action.mcp.deferLoading
|
|
50
|
+
} : undefined,
|
|
51
|
+
api: action.api ? {
|
|
52
|
+
method: action.api.method,
|
|
53
|
+
path: action.api.path,
|
|
54
|
+
tags: action.api.tags
|
|
55
|
+
} : undefined
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function generateManifest(source, options) {
|
|
59
|
+
let actions = isActionRegistry(source) ? source.list() : [...source];
|
|
60
|
+
if (options?.surface) {
|
|
61
|
+
const surface = options.surface;
|
|
62
|
+
actions = actions.filter((action) => {
|
|
63
|
+
const surfaces = action.surfaces ?? DEFAULT_REGISTRY_SURFACES;
|
|
64
|
+
return surfaces.includes(surface);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const surfaceSet = new Set;
|
|
68
|
+
for (const action of actions) {
|
|
69
|
+
for (const s of action.surfaces ?? DEFAULT_REGISTRY_SURFACES) {
|
|
70
|
+
if (ACTION_SURFACES.includes(s)) {
|
|
71
|
+
surfaceSet.add(s);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
version: options?.version ?? "1.0.0",
|
|
77
|
+
generatedAt: new Date().toISOString(),
|
|
78
|
+
surfaces: [...surfaceSet].sort(),
|
|
79
|
+
actions: actions.map(actionToManifestEntry),
|
|
80
|
+
errors: buildErrorTaxonomy(),
|
|
81
|
+
outputModes: [...OUTPUT_MODES]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { generateManifest };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ActionRegistry, ActionSurface, AnyActionSpec, ErrorCategory, JsonSchema } from "@outfitter/contracts";
|
|
2
|
+
interface ActionManifest {
|
|
3
|
+
readonly version: string;
|
|
4
|
+
readonly generatedAt: string;
|
|
5
|
+
readonly surfaces: ActionSurface[];
|
|
6
|
+
readonly actions: ActionManifestEntry[];
|
|
7
|
+
readonly errors: Record<ErrorCategory, {
|
|
8
|
+
exit: number;
|
|
9
|
+
http: number;
|
|
10
|
+
}>;
|
|
11
|
+
readonly outputModes: string[];
|
|
12
|
+
}
|
|
13
|
+
interface ActionManifestEntry {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly description?: string | undefined;
|
|
16
|
+
readonly surfaces: ActionSurface[];
|
|
17
|
+
readonly input: JsonSchema;
|
|
18
|
+
readonly output?: JsonSchema | undefined;
|
|
19
|
+
readonly cli?: ManifestCliSpec | undefined;
|
|
20
|
+
readonly mcp?: ManifestMcpSpec | undefined;
|
|
21
|
+
readonly api?: ManifestApiSpec | undefined;
|
|
22
|
+
}
|
|
23
|
+
interface ManifestCliSpec {
|
|
24
|
+
readonly group?: string | undefined;
|
|
25
|
+
readonly command?: string | undefined;
|
|
26
|
+
readonly description?: string | undefined;
|
|
27
|
+
readonly aliases?: readonly string[] | undefined;
|
|
28
|
+
readonly options?: readonly ManifestCliOption[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
interface ManifestCliOption {
|
|
31
|
+
readonly flags: string;
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly defaultValue?: string | boolean | string[] | undefined;
|
|
34
|
+
readonly required?: boolean | undefined;
|
|
35
|
+
}
|
|
36
|
+
interface ManifestMcpSpec {
|
|
37
|
+
readonly tool?: string | undefined;
|
|
38
|
+
readonly description?: string | undefined;
|
|
39
|
+
readonly deferLoading?: boolean | undefined;
|
|
40
|
+
}
|
|
41
|
+
interface ManifestApiSpec {
|
|
42
|
+
readonly method?: string | undefined;
|
|
43
|
+
readonly path?: string | undefined;
|
|
44
|
+
readonly tags?: readonly string[] | undefined;
|
|
45
|
+
}
|
|
46
|
+
interface GenerateManifestOptions {
|
|
47
|
+
readonly surface?: ActionSurface;
|
|
48
|
+
readonly version?: string;
|
|
49
|
+
}
|
|
50
|
+
type ActionSource = ActionRegistry | readonly AnyActionSpec[];
|
|
51
|
+
/**
|
|
52
|
+
* Generate a manifest from an action registry or action array.
|
|
53
|
+
*
|
|
54
|
+
* @param source - ActionRegistry or array of ActionSpec
|
|
55
|
+
* @param options - Filtering and version options
|
|
56
|
+
* @returns The manifest object
|
|
57
|
+
*/
|
|
58
|
+
declare function generateManifest(source: ActionSource, options?: GenerateManifestOptions): ActionManifest;
|
|
59
|
+
export { ActionManifest, ActionManifestEntry, ManifestCliSpec, ManifestCliOption, ManifestMcpSpec, ManifestApiSpec, GenerateManifestOptions, ActionSource, generateManifest };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ActionManifest, ActionSource, GenerateManifestOptions } from "./schema-vzq7nzs3";
|
|
2
|
+
interface SurfaceMap extends ActionManifest {
|
|
3
|
+
readonly $schema: string;
|
|
4
|
+
readonly generator: "runtime" | "build";
|
|
5
|
+
}
|
|
6
|
+
interface GenerateSurfaceMapOptions extends GenerateManifestOptions {
|
|
7
|
+
readonly generator?: "runtime" | "build";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generate a surface map from an action registry or action array.
|
|
11
|
+
*
|
|
12
|
+
* Wraps `generateManifest()` with envelope metadata (`$schema`, `generator`).
|
|
13
|
+
*
|
|
14
|
+
* @param source - ActionRegistry or array of ActionSpec
|
|
15
|
+
* @param options - Filtering, version, and generator options
|
|
16
|
+
* @returns The surface map object
|
|
17
|
+
*/
|
|
18
|
+
declare function generateSurfaceMap(source: ActionSource, options?: GenerateSurfaceMapOptions): SurfaceMap;
|
|
19
|
+
/**
|
|
20
|
+
* Write a surface map to disk as pretty-printed JSON.
|
|
21
|
+
*
|
|
22
|
+
* Creates parent directories if they don't exist.
|
|
23
|
+
*
|
|
24
|
+
* @param surfaceMap - The surface map to write
|
|
25
|
+
* @param outputPath - Absolute path for the output file
|
|
26
|
+
*/
|
|
27
|
+
declare function writeSurfaceMap(surfaceMap: SurfaceMap, outputPath: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Read a surface map from disk.
|
|
30
|
+
*
|
|
31
|
+
* @param inputPath - Absolute path to the surface map file
|
|
32
|
+
* @returns The parsed surface map
|
|
33
|
+
*/
|
|
34
|
+
declare function readSurfaceMap(inputPath: string): Promise<SurfaceMap>;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the file path for a named snapshot.
|
|
37
|
+
*
|
|
38
|
+
* @param cwd - Project root directory
|
|
39
|
+
* @param outputDir - Output directory name (e.g., ".outfitter")
|
|
40
|
+
* @param version - Snapshot version label
|
|
41
|
+
* @returns Absolute path to the snapshot file
|
|
42
|
+
*/
|
|
43
|
+
declare function resolveSnapshotPath(cwd: string, outputDir: string, version: string): string;
|
|
44
|
+
export { SurfaceMap, GenerateSurfaceMapOptions, generateSurfaceMap, writeSurfaceMap, readSurfaceMap, resolveSnapshotPath };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ActionManifest } from "./schema-vzq7nzs3";
|
|
2
|
+
type DocSurface = "mcp" | "cli";
|
|
3
|
+
interface MarkdownFormatOptions {
|
|
4
|
+
/** Which surface to document. Default: "mcp" */
|
|
5
|
+
readonly surface?: DocSurface;
|
|
6
|
+
/** Document title. Default: surface-specific (e.g., "MCP Tools Reference") */
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
/** Include table of contents. Default: true when 2+ entries */
|
|
9
|
+
readonly toc?: boolean;
|
|
10
|
+
/** Include generated timestamp. Default: true */
|
|
11
|
+
readonly timestamp?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Format an action manifest as a markdown reference document.
|
|
15
|
+
*
|
|
16
|
+
* @param manifest - The manifest to format
|
|
17
|
+
* @param options - Formatting options
|
|
18
|
+
* @returns Formatted markdown string
|
|
19
|
+
*/
|
|
20
|
+
declare function formatManifestMarkdown(manifest: ActionManifest, options?: MarkdownFormatOptions): string;
|
|
21
|
+
export { MarkdownFormatOptions, formatManifestMarkdown };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/schema/src/markdown.ts
|
|
3
|
+
var DEFAULT_TITLES = {
|
|
4
|
+
mcp: "MCP Tools Reference",
|
|
5
|
+
cli: "CLI Reference"
|
|
6
|
+
};
|
|
7
|
+
function formatManifestMarkdown(manifest, options) {
|
|
8
|
+
const surface = options?.surface ?? "mcp";
|
|
9
|
+
const title = options?.title ?? DEFAULT_TITLES[surface];
|
|
10
|
+
const showTimestamp = options?.timestamp ?? true;
|
|
11
|
+
const sections = [];
|
|
12
|
+
sections.push(renderHeader(title, manifest.version, showTimestamp ? manifest.generatedAt : undefined));
|
|
13
|
+
const sorted = manifest.actions.filter((action) => action.surfaces.includes(surface)).sort((a, b) => {
|
|
14
|
+
const nameA = displayName(a, surface);
|
|
15
|
+
const nameB = displayName(b, surface);
|
|
16
|
+
return nameA.localeCompare(nameB);
|
|
17
|
+
});
|
|
18
|
+
if (sorted.length === 0) {
|
|
19
|
+
sections.push("_No tools registered._");
|
|
20
|
+
return sections.join(`
|
|
21
|
+
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
const showToc = options?.toc ?? sorted.length >= 2;
|
|
25
|
+
if (showToc) {
|
|
26
|
+
sections.push(renderToc(sorted, surface));
|
|
27
|
+
}
|
|
28
|
+
for (const action of sorted) {
|
|
29
|
+
sections.push(surface === "cli" ? renderCliCommand(action) : renderMcpTool(action));
|
|
30
|
+
}
|
|
31
|
+
return `${sections.join(`
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
`)}
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
function renderHeader(title, version, generatedAt) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push(`# ${title}`);
|
|
41
|
+
const meta = [];
|
|
42
|
+
meta.push(`schema v${version}`);
|
|
43
|
+
if (generatedAt) {
|
|
44
|
+
meta.push(generatedAt);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(`> Generated from ${meta.join(" | ")}`);
|
|
48
|
+
return lines.join(`
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
function renderToc(actions, surface) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
lines.push("## Table of Contents");
|
|
54
|
+
lines.push("");
|
|
55
|
+
for (const action of actions) {
|
|
56
|
+
const name = displayName(action, surface);
|
|
57
|
+
lines.push(`- [${name}](#${slugify(name)})`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join(`
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
function renderMcpTool(action) {
|
|
63
|
+
const name = displayName(action, "mcp");
|
|
64
|
+
const desc = action.mcp?.description ?? action.description;
|
|
65
|
+
const lines = [];
|
|
66
|
+
lines.push(`## ${name}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
if (desc) {
|
|
69
|
+
lines.push(desc);
|
|
70
|
+
}
|
|
71
|
+
if (action.mcp?.deferLoading) {
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("> Deferred loading: must be explicitly loaded before use.");
|
|
74
|
+
}
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("### Parameters");
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push(renderSchemaTable(action.input));
|
|
79
|
+
return lines.join(`
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
function renderCliCommand(action) {
|
|
83
|
+
const name = displayName(action, "cli");
|
|
84
|
+
const desc = action.cli?.description ?? action.description;
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`## ${name}`);
|
|
87
|
+
lines.push("");
|
|
88
|
+
if (desc) {
|
|
89
|
+
lines.push(desc);
|
|
90
|
+
}
|
|
91
|
+
if (action.cli?.aliases && action.cli.aliases.length > 0) {
|
|
92
|
+
lines.push("");
|
|
93
|
+
const aliasStr = action.cli.aliases.map((a) => `\`${a}\``).join(", ");
|
|
94
|
+
lines.push(`**Aliases:** ${aliasStr}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("### Options");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(renderCliOptionsTable(action.cli));
|
|
100
|
+
return lines.join(`
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
function renderCliOptionsTable(cli) {
|
|
104
|
+
const options = cli?.options;
|
|
105
|
+
if (!options || options.length === 0) {
|
|
106
|
+
return "_No options._";
|
|
107
|
+
}
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push("| Flag | Description | Default |");
|
|
110
|
+
lines.push("|------|-------------|---------|");
|
|
111
|
+
for (const opt of options) {
|
|
112
|
+
const defaultStr = opt.defaultValue !== undefined ? `\`${JSON.stringify(opt.defaultValue)}\`` : "\u2014";
|
|
113
|
+
lines.push(`| \`${escapeMarkdown(opt.flags)}\` | ${escapeMarkdown(opt.description)} | ${defaultStr} |`);
|
|
114
|
+
}
|
|
115
|
+
return lines.join(`
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
function renderSchemaTable(schema) {
|
|
119
|
+
const properties = schema.properties;
|
|
120
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
121
|
+
return "_No parameters._";
|
|
122
|
+
}
|
|
123
|
+
const required = new Set(schema.required ?? []);
|
|
124
|
+
const lines = [];
|
|
125
|
+
lines.push("| Property | Type | Required | Description |");
|
|
126
|
+
lines.push("|----------|------|----------|-------------|");
|
|
127
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
128
|
+
const typeStr = formatType(prop);
|
|
129
|
+
const isRequired = required.has(key) ? "Yes" : "No";
|
|
130
|
+
const descParts = [];
|
|
131
|
+
if (prop.description) {
|
|
132
|
+
descParts.push(escapeMarkdown(prop.description));
|
|
133
|
+
}
|
|
134
|
+
if (prop.default !== undefined) {
|
|
135
|
+
descParts.push(`(default: \`${JSON.stringify(prop.default)}\`)`);
|
|
136
|
+
}
|
|
137
|
+
const desc = descParts.join(" ");
|
|
138
|
+
lines.push(`| \`${key}\` | ${typeStr} | ${isRequired} | ${desc} |`);
|
|
139
|
+
}
|
|
140
|
+
return lines.join(`
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
function formatType(schema) {
|
|
144
|
+
if (schema.enum) {
|
|
145
|
+
return schema.enum.map((v) => `\`${JSON.stringify(v)}\``).join(" \\| ");
|
|
146
|
+
}
|
|
147
|
+
if (schema.anyOf) {
|
|
148
|
+
return schema.anyOf.map(formatType).join(" \\| ");
|
|
149
|
+
}
|
|
150
|
+
if (schema.oneOf) {
|
|
151
|
+
return schema.oneOf.map(formatType).join(" \\| ");
|
|
152
|
+
}
|
|
153
|
+
if (schema.type === "array") {
|
|
154
|
+
const items = schema.items;
|
|
155
|
+
if (items && !Array.isArray(items) && items.type) {
|
|
156
|
+
return `array of ${items.type}`;
|
|
157
|
+
}
|
|
158
|
+
return "array";
|
|
159
|
+
}
|
|
160
|
+
return schema.type ?? "unknown";
|
|
161
|
+
}
|
|
162
|
+
function escapeMarkdown(text) {
|
|
163
|
+
return text.replace(/\|/g, "\\|");
|
|
164
|
+
}
|
|
165
|
+
function slugify(text) {
|
|
166
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
167
|
+
}
|
|
168
|
+
function displayName(action, surface) {
|
|
169
|
+
if (surface === "cli") {
|
|
170
|
+
const cli = action.cli;
|
|
171
|
+
if (!cli)
|
|
172
|
+
return action.id;
|
|
173
|
+
const group = cli.group;
|
|
174
|
+
const command = cli.command ?? action.id;
|
|
175
|
+
return group ? `${group} ${command}` : command;
|
|
176
|
+
}
|
|
177
|
+
return action.mcp?.tool ?? action.id;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { formatManifestMarkdown };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { GenerateSurfaceMapOptions, SurfaceMap, generateSurfaceMap, readSurfaceMap, resolveSnapshotPath, writeSurfaceMap } from "./shared/@outfitter/schema-wgnvy5zh";
|
|
2
|
+
import "./shared/@outfitter/schema-vzq7nzs3";
|
|
3
|
+
export { writeSurfaceMap, resolveSnapshotPath, readSurfaceMap, generateSurfaceMap, SurfaceMap, GenerateSurfaceMapOptions };
|
package/dist/surface.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
generateSurfaceMap,
|
|
4
|
+
readSurfaceMap,
|
|
5
|
+
resolveSnapshotPath,
|
|
6
|
+
writeSurfaceMap
|
|
7
|
+
} from "./shared/@outfitter/schema-cq7f2r5z.js";
|
|
8
|
+
import"./shared/@outfitter/schema-tnrxpckr.js";
|
|
9
|
+
export {
|
|
10
|
+
writeSurfaceMap,
|
|
11
|
+
resolveSnapshotPath,
|
|
12
|
+
readSurfaceMap,
|
|
13
|
+
generateSurfaceMap
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@outfitter/schema",
|
|
3
|
+
"description": "Schema introspection, surface map generation, and drift detection for Outfitter",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"./diff": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/diff.d.ts",
|
|
21
|
+
"default": "./dist/diff.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"./manifest": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/manifest.d.ts",
|
|
27
|
+
"default": "./dist/manifest.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./markdown": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/markdown.d.ts",
|
|
33
|
+
"default": "./dist/markdown.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
"./surface": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/surface.d.ts",
|
|
40
|
+
"default": "./dist/surface.js"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"sideEffects": false,
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "cd ../.. && bunup --filter @outfitter/schema",
|
|
47
|
+
"test": "bun test",
|
|
48
|
+
"test:watch": "bun test --watch",
|
|
49
|
+
"lint": "biome lint ./src",
|
|
50
|
+
"lint:fix": "biome lint --write ./src",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"clean": "rm -rf dist"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@outfitter/contracts": "workspace:*",
|
|
56
|
+
"@outfitter/types": "workspace:*"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"zod": "^4.3.5"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/bun": "^1.3.7",
|
|
63
|
+
"typescript": "^5.9.3"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"bun": ">=1.3.7"
|
|
67
|
+
},
|
|
68
|
+
"keywords": [
|
|
69
|
+
"outfitter",
|
|
70
|
+
"schema",
|
|
71
|
+
"manifest",
|
|
72
|
+
"surface-map",
|
|
73
|
+
"introspection",
|
|
74
|
+
"typescript"
|
|
75
|
+
],
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"repository": {
|
|
78
|
+
"type": "git",
|
|
79
|
+
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
80
|
+
"directory": "packages/schema"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
}
|
|
85
|
+
}
|