@forgeailab/spark-schema 0.1.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/package.json +24 -0
- package/src/capabilities.ts +52 -0
- package/src/index.ts +6 -0
- package/src/pack.ts +101 -0
- package/src/parse.ts +115 -0
- package/src/preset.ts +23 -0
- package/src/state.ts +31 -0
- package/src/template.ts +20 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeailab/spark-schema",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Zod schemas + TOML parsers for spark pack and template manifests.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": "./src/index.ts",
|
|
14
|
+
"main": "./src/index.ts",
|
|
15
|
+
"types": "./src/index.ts",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"smol-toml": "latest",
|
|
18
|
+
"zod": "latest"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"bun-types": "latest",
|
|
22
|
+
"typescript": "latest"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const PACK_CAPABILITY_VALUES = [
|
|
4
|
+
'db',
|
|
5
|
+
'db-pg',
|
|
6
|
+
'auth',
|
|
7
|
+
'payments',
|
|
8
|
+
'email',
|
|
9
|
+
'ui-kit',
|
|
10
|
+
'local-runtime',
|
|
11
|
+
'deploy-target',
|
|
12
|
+
'e2e',
|
|
13
|
+
'ai-sdk',
|
|
14
|
+
'blob-storage',
|
|
15
|
+
'analytics',
|
|
16
|
+
'sync',
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export const TEMPLATE_CAPABILITY_VALUES = ['static', 'server', 'react', 'native', 'vue', 'svelte', 'mdx-content', 'edge-runtime'] as const;
|
|
20
|
+
|
|
21
|
+
export const PackCapability = z.enum(PACK_CAPABILITY_VALUES);
|
|
22
|
+
export type PackCapability = z.infer<typeof PackCapability>;
|
|
23
|
+
|
|
24
|
+
export const TemplateCapability = z.enum(TEMPLATE_CAPABILITY_VALUES);
|
|
25
|
+
export type TemplateCapability = z.infer<typeof TemplateCapability>;
|
|
26
|
+
|
|
27
|
+
const templateCapabilityLookup: ReadonlySet<string> = new Set(TEMPLATE_CAPABILITY_VALUES);
|
|
28
|
+
const capabilityOverlap = PACK_CAPABILITY_VALUES.filter((tag) => templateCapabilityLookup.has(tag));
|
|
29
|
+
|
|
30
|
+
if (capabilityOverlap.length > 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`PackCapability and TemplateCapability must not overlap: ${capabilityOverlap.join(', ')}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const EXCLUSIVE_CAPABILITIES: ReadonlySet<PackCapability> = new Set<PackCapability>([
|
|
37
|
+
'db',
|
|
38
|
+
'auth',
|
|
39
|
+
'payments',
|
|
40
|
+
'ui-kit',
|
|
41
|
+
'sync',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export const NON_EXCLUSIVE_CAPABILITIES: ReadonlySet<PackCapability> = new Set<PackCapability>([
|
|
45
|
+
'ai-sdk',
|
|
46
|
+
'analytics',
|
|
47
|
+
'email',
|
|
48
|
+
'blob-storage',
|
|
49
|
+
'e2e',
|
|
50
|
+
'deploy-target',
|
|
51
|
+
'local-runtime',
|
|
52
|
+
]);
|
package/src/index.ts
ADDED
package/src/pack.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PackCapability, TemplateCapability } from './capabilities.ts';
|
|
3
|
+
|
|
4
|
+
const packNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
5
|
+
const npmPackageNamePattern =
|
|
6
|
+
/^(?=.{1,214}$)(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
7
|
+
const semverPattern =
|
|
8
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
|
+
|
|
10
|
+
export const PackCategory = z.enum([
|
|
11
|
+
'db',
|
|
12
|
+
'auth',
|
|
13
|
+
'payments',
|
|
14
|
+
'email',
|
|
15
|
+
'ui',
|
|
16
|
+
'ai',
|
|
17
|
+
'infra',
|
|
18
|
+
'testing',
|
|
19
|
+
'deploy',
|
|
20
|
+
'analytics',
|
|
21
|
+
'storage',
|
|
22
|
+
]);
|
|
23
|
+
export type PackCategory = z.infer<typeof PackCategory>;
|
|
24
|
+
|
|
25
|
+
export const FileMode = z.enum(['create', 'create-or-skip', 'append', 'merge-json', 'template']);
|
|
26
|
+
export type FileMode = z.infer<typeof FileMode>;
|
|
27
|
+
|
|
28
|
+
export const PackName = z.string().regex(packNamePattern, {
|
|
29
|
+
message: 'Pack names must match /^[a-z][a-z0-9-]*$/',
|
|
30
|
+
});
|
|
31
|
+
export type PackName = z.infer<typeof PackName>;
|
|
32
|
+
|
|
33
|
+
export const SemverString = z.string().regex(semverPattern, {
|
|
34
|
+
message: 'Version must be a valid semver string',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const PackDependenciesSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
runtime: z.array(z.string().min(1)).optional(),
|
|
40
|
+
dev: z.array(z.string().min(1)).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
export const PackEnvSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
required: z.array(z.string().min(1)).optional(),
|
|
47
|
+
optional: z.array(z.string().min(1)).optional(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
|
|
51
|
+
export const PackFileOperationSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
mode: FileMode,
|
|
54
|
+
from: z.string().min(1),
|
|
55
|
+
to: z.string().min(1),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
export const PackSkillsSchema = z
|
|
60
|
+
.object({
|
|
61
|
+
copy: z.array(z.string().min(1)).optional(),
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
64
|
+
|
|
65
|
+
export const PackTasksSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
file: z.string().min(1).optional(),
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
|
|
71
|
+
const RuntimePackageBlockSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
package: z.string().regex(npmPackageNamePattern, {
|
|
74
|
+
message: 'Runtime package names must be valid npm package names',
|
|
75
|
+
}),
|
|
76
|
+
version: z.string().min(1),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
|
|
80
|
+
export const PackManifestSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
name: PackName,
|
|
83
|
+
version: SemverString,
|
|
84
|
+
category: PackCategory,
|
|
85
|
+
description: z.string().min(1).optional(),
|
|
86
|
+
provides: z.array(PackCapability),
|
|
87
|
+
requires: z.array(PackCapability),
|
|
88
|
+
conflicts: z.array(PackCapability),
|
|
89
|
+
requires_runtime: z.array(TemplateCapability),
|
|
90
|
+
compatible_scaffolds: z.array(z.string().min(1)).optional().default([]),
|
|
91
|
+
dependencies: PackDependenciesSchema.optional(),
|
|
92
|
+
env: PackEnvSchema.optional(),
|
|
93
|
+
files: z.array(PackFileOperationSchema).optional(),
|
|
94
|
+
skills: PackSkillsSchema.optional(),
|
|
95
|
+
tasks: PackTasksSchema.optional(),
|
|
96
|
+
runtime_package: RuntimePackageBlockSchema.optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict();
|
|
99
|
+
|
|
100
|
+
export type RuntimePackageBlock = z.infer<typeof RuntimePackageBlockSchema>;
|
|
101
|
+
export type PackManifest = z.infer<typeof PackManifestSchema>;
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parse } from 'smol-toml';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { PackManifestSchema, type PackManifest } from './pack.ts';
|
|
4
|
+
import { PresetManifestSchema, type PresetManifest } from './preset.ts';
|
|
5
|
+
import { TemplateManifestSchema, type TemplateManifest } from './template.ts';
|
|
6
|
+
|
|
7
|
+
export type ParseError = {
|
|
8
|
+
message: string;
|
|
9
|
+
issues?: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Result<TData, TError> =
|
|
13
|
+
| {
|
|
14
|
+
ok: true;
|
|
15
|
+
data: TData;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: TError;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const forbiddenPackKeys = [
|
|
23
|
+
'post_install',
|
|
24
|
+
'hooks',
|
|
25
|
+
'pre_add',
|
|
26
|
+
'pre_install',
|
|
27
|
+
'post_add',
|
|
28
|
+
'scripts',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatIssuePath(path: PropertyKey[]): string {
|
|
36
|
+
return path.length === 0 ? '<root>' : path.map(String).join('.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toParseError(error: unknown): ParseError {
|
|
40
|
+
if (error instanceof z.ZodError) {
|
|
41
|
+
return {
|
|
42
|
+
message: 'Manifest validation failed',
|
|
43
|
+
issues: error.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
return {
|
|
49
|
+
message: error.message,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
message: 'Unknown TOML parse error',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertNoForbiddenPackFields(parsed: unknown): void {
|
|
59
|
+
if (!isRecord(parsed)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const key of forbiddenPackKeys) {
|
|
64
|
+
if (Object.hasOwn(parsed, key)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Unsupported pack manifest field "${key}" is forbidden; pack installs are declarative and cannot run shell hooks.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parsePackToml(raw: string): Result<PackManifest, ParseError> {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = parse(raw);
|
|
75
|
+
assertNoForbiddenPackFields(parsed);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
data: PackManifestSchema.parse(parsed),
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: toParseError(error),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseTemplateToml(raw: string): Result<TemplateManifest, ParseError> {
|
|
90
|
+
try {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
data: TemplateManifestSchema.parse(parse(raw)),
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: toParseError(error),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parsePresetToml(raw: string): Result<PresetManifest, ParseError> {
|
|
104
|
+
try {
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
data: PresetManifestSchema.parse(parse(raw)),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: toParseError(error),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/preset.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const packNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
4
|
+
|
|
5
|
+
export const PresetManifestSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
name: z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(packNamePattern, {
|
|
10
|
+
message: 'Preset names must match /^[a-z][a-z0-9-]*$/',
|
|
11
|
+
})
|
|
12
|
+
.optional(),
|
|
13
|
+
description: z.string().min(1).optional(),
|
|
14
|
+
compatible_scaffolds: z.array(z.string().min(1)),
|
|
15
|
+
packs: z.array(
|
|
16
|
+
z.string().regex(packNamePattern, {
|
|
17
|
+
message: 'Pack names must match /^[a-z][a-z0-9-]*$/',
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
|
|
23
|
+
export type PresetManifest = z.infer<typeof PresetManifestSchema>;
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const StateAppendedBlockSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
to: z.string().min(1),
|
|
6
|
+
marker: z.string().min(1),
|
|
7
|
+
content_hash: z.string().min(1).optional(),
|
|
8
|
+
})
|
|
9
|
+
.strict();
|
|
10
|
+
|
|
11
|
+
export const StateInstalledPackSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
name: z.string().min(1),
|
|
14
|
+
version: z.string().min(1),
|
|
15
|
+
files: z.array(z.string().min(1)).default([]),
|
|
16
|
+
appended_blocks: z.array(StateAppendedBlockSchema).default([]),
|
|
17
|
+
env: z.array(z.string().min(1)).default([]),
|
|
18
|
+
tasks: z.array(z.string().min(1)).default([]),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
|
|
22
|
+
export const StateFileSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
schema_version: z.literal(1),
|
|
25
|
+
installed_packs: z.array(StateInstalledPackSchema).default([]),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
export type StateAppendedBlock = z.infer<typeof StateAppendedBlockSchema>;
|
|
30
|
+
export type StateInstalledPack = z.infer<typeof StateInstalledPackSchema>;
|
|
31
|
+
export type StateFile = z.infer<typeof StateFileSchema>;
|
package/src/template.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { TemplateCapability } from './capabilities.ts';
|
|
3
|
+
|
|
4
|
+
const templateNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
5
|
+
|
|
6
|
+
export const TemplateStatus = z.enum(['stable', 'planned']);
|
|
7
|
+
export type TemplateStatus = z.infer<typeof TemplateStatus>;
|
|
8
|
+
|
|
9
|
+
export const TemplateManifestSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
name: z.string().regex(templateNamePattern, {
|
|
12
|
+
message: 'Template names must match /^[a-z][a-z0-9-]*$/',
|
|
13
|
+
}),
|
|
14
|
+
status: TemplateStatus,
|
|
15
|
+
provides: z.array(TemplateCapability),
|
|
16
|
+
description: z.string().min(1),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
20
|
+
export type TemplateManifest = z.infer<typeof TemplateManifestSchema>;
|