@soulcraft/kit-schema 2.0.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/CLAUDE.md +77 -0
- package/README.md +271 -0
- package/bun.lock +263 -0
- package/package.json +27 -0
- package/src/index.ts +37 -0
- package/src/loader.ts +183 -0
- package/src/schema.ts +705 -0
- package/src/skills.ts +127 -0
- package/src/types.ts +2338 -0
- package/src/variables.ts +143 -0
- package/tsconfig.base.json +21 -0
- package/tsconfig.json +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soulcraft/kit-schema",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Shared kit manifest schema for Soulcraft platform kits (Venue, Workshop, and future products)",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./*": "./src/*.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^25.3.0",
|
|
20
|
+
"typescript": "^5.7.3",
|
|
21
|
+
"vitest": "^3.0.5"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@soulcraft/theme": "^1.0.0",
|
|
25
|
+
"zod": "^4.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @soulcraft/kit-schema
|
|
3
|
+
* @description Shared kit manifest schema for the Soulcraft platform ecosystem.
|
|
4
|
+
*
|
|
5
|
+
* This package defines the contract between Soulcraft platform products (Venue,
|
|
6
|
+
* Workshop) and the kits that configure them for specific verticals or use cases.
|
|
7
|
+
*
|
|
8
|
+
* ## What is a Kit?
|
|
9
|
+
*
|
|
10
|
+
* A kit is a directory containing:
|
|
11
|
+
* - `kit.json` — The manifest: metadata, feature flags, theme, experience types
|
|
12
|
+
* - `files/` — Template files (CMS content, email templates, waiver text)
|
|
13
|
+
* - `skills/` — AI skill definitions in SKILL.md format
|
|
14
|
+
*
|
|
15
|
+
* ## Key Exports
|
|
16
|
+
*
|
|
17
|
+
* - **Types** — `VenueKitConfig`, `KitManifest`, `VenueFeatures`, etc.
|
|
18
|
+
* - **Schemas** — Zod schemas for validating `kit.json` files
|
|
19
|
+
* - **Loader** — `loadKitFromDirectory()`, `loadVenueKit()`
|
|
20
|
+
* - **Skills** — `parseSkillFile()` for SKILL.md frontmatter parsing
|
|
21
|
+
* - **Variables** — `substituteVariables()` for `{{key}}` template substitution
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { loadVenueKit } from '@soulcraft/kit-schema';
|
|
27
|
+
*
|
|
28
|
+
* const kit = await loadVenueKit('./kits/wicks-and-whiskers');
|
|
29
|
+
* const { features, theme, experienceTypes } = kit.manifest.venue;
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export * from './types.js';
|
|
34
|
+
export * from './schema.js';
|
|
35
|
+
export * from './loader.js';
|
|
36
|
+
export * from './skills.js';
|
|
37
|
+
export * from './variables.js';
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @soulcraft/kit-schema/loader
|
|
3
|
+
* @description Kit manifest loader and directory validator for Soulcraft platform kits.
|
|
4
|
+
*
|
|
5
|
+
* This module provides the reference implementation for loading and validating kits
|
|
6
|
+
* from the filesystem. Platform applications (Venue, Workshop) use this to mount
|
|
7
|
+
* kits at startup.
|
|
8
|
+
*
|
|
9
|
+
* The loader:
|
|
10
|
+
* 1. Reads `kit.json` from the kit directory
|
|
11
|
+
* 2. Validates it against the Zod schema
|
|
12
|
+
* 3. Discovers and parses all SKILL.md files in `skills/`
|
|
13
|
+
* 4. Returns a fully typed {@link LoadedKit} with manifest + skills
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { loadKitFromDirectory } from '@soulcraft/kit-schema/loader';
|
|
18
|
+
*
|
|
19
|
+
* const kit = await loadKitFromDirectory('/app/kits/wicks-and-whiskers');
|
|
20
|
+
* console.log(kit.manifest.name); // 'Wicks & Whiskers'
|
|
21
|
+
* console.log(kit.skills.length); // 4
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { KitManifestSchema } from './schema.js';
|
|
28
|
+
import { parseSkillFile } from './skills.js';
|
|
29
|
+
import type { KitManifest, ParsedSkill, SoulcraftKitConfig, VenueKitConfig } from './types.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A fully loaded and validated kit, ready for use by the platform.
|
|
33
|
+
*/
|
|
34
|
+
export interface LoadedKit {
|
|
35
|
+
/** Validated and typed kit manifest from `kit.json`. */
|
|
36
|
+
manifest: KitManifest;
|
|
37
|
+
/** All SKILL.md files discovered and parsed from `skills/`. */
|
|
38
|
+
skills: ParsedSkill[];
|
|
39
|
+
/** Resolved absolute path to the kit directory root. */
|
|
40
|
+
kitDir: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A fully loaded Venue kit with the manifest narrowed to {@link VenueKitConfig}.
|
|
45
|
+
*/
|
|
46
|
+
export interface LoadedVenueKit extends LoadedKit {
|
|
47
|
+
manifest: VenueKitConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A fully loaded unified Soulcraft kit with the manifest narrowed to {@link SoulcraftKitConfig}.
|
|
52
|
+
*/
|
|
53
|
+
export interface LoadedSoulcraftKit extends LoadedKit {
|
|
54
|
+
manifest: SoulcraftKitConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load and validate a kit from its directory on the filesystem.
|
|
59
|
+
*
|
|
60
|
+
* Reads `kit.json`, validates it with the Zod schema, then discovers all
|
|
61
|
+
* `skills/{name}/SKILL.md` files and parses their frontmatter.
|
|
62
|
+
*
|
|
63
|
+
* @param kitDir - Absolute path to the kit directory (must contain `kit.json`).
|
|
64
|
+
* @returns A {@link LoadedKit} with validated manifest and parsed skills.
|
|
65
|
+
* @throws {Error} If `kit.json` is missing, invalid JSON, or fails schema validation.
|
|
66
|
+
* @throws {Error} If any SKILL.md file has malformed frontmatter.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const kit = await loadKitFromDirectory('/app/kits/wicks-and-whiskers');
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export async function loadKitFromDirectory(kitDir: string): Promise<LoadedKit> {
|
|
74
|
+
const manifestPath = join(kitDir, 'kit.json');
|
|
75
|
+
|
|
76
|
+
let rawManifest: unknown;
|
|
77
|
+
try {
|
|
78
|
+
const content = await readFile(manifestPath, 'utf-8');
|
|
79
|
+
rawManifest = JSON.parse(content);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const message = err instanceof SyntaxError
|
|
82
|
+
? `kit.json at ${manifestPath} is not valid JSON: ${err.message}`
|
|
83
|
+
: `Could not read kit.json at ${manifestPath}: ${String(err)}`;
|
|
84
|
+
throw new Error(message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Strip the JSON Schema $schema key — it's an editor hint, not part of the kit spec.
|
|
88
|
+
if (rawManifest !== null && typeof rawManifest === 'object' && '$schema' in rawManifest) {
|
|
89
|
+
const { $schema: _, ...rest } = rawManifest as Record<string, unknown>;
|
|
90
|
+
rawManifest = rest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = KitManifestSchema.safeParse(rawManifest);
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
const formatted = result.error.issues
|
|
96
|
+
.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
throw new Error(`Invalid kit manifest at ${manifestPath}:\n${formatted}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const skills = await loadSkillsFromDirectory(kitDir);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
manifest: result.data,
|
|
105
|
+
skills,
|
|
106
|
+
kitDir
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load and validate a Venue kit, narrowing the manifest type to {@link VenueKitConfig}.
|
|
112
|
+
*
|
|
113
|
+
* Also accepts `type: 'soulcraft'` kits that declare a `venue` config block —
|
|
114
|
+
* these are valid Venue kits in the unified kit system. The manifest is returned
|
|
115
|
+
* as-is (typed as `VenueKitConfig` for backward compatibility), so callers must
|
|
116
|
+
* check `manifest.type === 'soulcraft'` and access `manifest.venue` directly when
|
|
117
|
+
* they need the venue config.
|
|
118
|
+
*
|
|
119
|
+
* @param kitDir - Absolute path to the Venue kit directory.
|
|
120
|
+
* @returns A {@link LoadedVenueKit} with a narrowed manifest type, or a
|
|
121
|
+
* {@link LoadedSoulcraftKit} when the kit is `type: 'soulcraft'`.
|
|
122
|
+
* @throws {Error} If the loaded kit is not a Venue or Soulcraft kit.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const kit = await loadVenueKit('/app/kits/wicks-and-whiskers');
|
|
127
|
+
* if (kit.manifest.type === 'venue') {
|
|
128
|
+
* const { features } = kit.manifest.venue;
|
|
129
|
+
* } else if (kit.manifest.type === 'soulcraft') {
|
|
130
|
+
* const { features } = kit.manifest.venue ?? {};
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export async function loadVenueKit(kitDir: string): Promise<LoadedVenueKit | LoadedSoulcraftKit> {
|
|
135
|
+
const kit = await loadKitFromDirectory(kitDir);
|
|
136
|
+
if (kit.manifest.type === 'venue') {
|
|
137
|
+
return kit as LoadedVenueKit;
|
|
138
|
+
}
|
|
139
|
+
if (kit.manifest.type === 'soulcraft') {
|
|
140
|
+
return kit as LoadedSoulcraftKit;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Expected a Venue or Soulcraft kit at ${kitDir} but found type: "${kit.manifest.type}"`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Discover and parse all SKILL.md files in a kit's `skills/` subdirectory.
|
|
149
|
+
*
|
|
150
|
+
* Scans for files matching `skills/{name}/SKILL.md`. Non-matching files and
|
|
151
|
+
* directories without a `SKILL.md` are silently skipped.
|
|
152
|
+
*
|
|
153
|
+
* @param kitDir - Absolute path to the kit directory root.
|
|
154
|
+
* @returns Array of parsed skills. Empty array if the `skills/` directory does not exist.
|
|
155
|
+
*/
|
|
156
|
+
async function loadSkillsFromDirectory(kitDir: string): Promise<ParsedSkill[]> {
|
|
157
|
+
const skillsDir = join(kitDir, 'skills');
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const skillsStat = await stat(skillsDir);
|
|
161
|
+
if (!skillsStat.isDirectory()) return [];
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const skills: ParsedSkill[] = [];
|
|
167
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
168
|
+
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (!entry.isDirectory()) continue;
|
|
171
|
+
const skillPath = join(skillsDir, entry.name, 'SKILL.md');
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const content = await readFile(skillPath, 'utf-8');
|
|
175
|
+
const skill = parseSkillFile(content, skillPath);
|
|
176
|
+
skills.push(skill);
|
|
177
|
+
} catch {
|
|
178
|
+
// SKILL.md does not exist or failed to parse — skip this entry
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return skills;
|
|
183
|
+
}
|