-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathregistry.ts
More file actions
104 lines (85 loc) · 2.96 KB
/
Copy pathregistry.ts
File metadata and controls
104 lines (85 loc) · 2.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import fs from 'node:fs';
import path from 'node:path';
import { z } from 'zod';
const RelativePluginPathSchema = z
.string()
.min(1)
.refine((value) => !path.isAbsolute(value), 'must be a relative path')
.refine((value) => !value.split(/[\\/]+/).includes('..'), 'must not contain .. segments');
export const PluginIdSchema = z
.string()
.min(1)
.regex(/^[a-z][a-zA-Z0-9]*$/, 'must use lower camelCase letters and numbers');
const CommitSchema = z
.string()
.min(7)
.regex(/^[0-9a-f]{7,40}$/i, 'must be a git commit sha');
const VersionSchema = z
.string()
.regex(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/, 'must be a semver-like version');
export const PluginRegistryEntrySchema = z.object({
pluginId: PluginIdSchema,
version: VersionSchema,
type: z.literal('tool'),
source: z.string().url(),
commit: CommitSchema,
submodule: RelativePluginPathSchema.refine(
(value) => value.startsWith('packages/tools/'),
'must live under packages/tools/'
),
path: RelativePluginPathSchema.default('.'),
status: z.enum(['pending', 'active', 'revoked', 'deprecated', 'rejected']).default('pending'),
support: z.enum(['community']).default('community'),
latestPublishEvent: RelativePluginPathSchema.optional(),
latestRevokeEvent: RelativePluginPathSchema.optional()
}).strict();
export const PluginRegistrySchema = z.object({
version: z.literal(1),
plugins: z.array(PluginRegistryEntrySchema)
}).strict();
export type PluginRegistry = z.infer<typeof PluginRegistrySchema>;
export type PluginRegistryEntry = z.infer<typeof PluginRegistryEntrySchema>;
export type RegistryValidationIssue = {
path: string;
message: string;
};
export function parseRegistryJson(input: unknown): PluginRegistry {
const registry = PluginRegistrySchema.parse(input);
assertUniquePluginIds(registry);
return registry;
}
export function readRegistryFile(filePath: string): PluginRegistry {
const raw = fs.readFileSync(filePath, 'utf8');
return parseRegistryJson(JSON.parse(raw));
}
export function validateRegistryJson(input: unknown): RegistryValidationIssue[] {
const result = PluginRegistrySchema.safeParse(input);
const issues: RegistryValidationIssue[] = [];
if (!result.success) {
return result.error.issues.map((issue) => ({
path: issue.path.join('.') || '<root>',
message: issue.message
}));
}
try {
assertUniquePluginIds(result.data);
} catch (error) {
issues.push({
path: 'plugins',
message: error instanceof Error ? error.message : String(error)
});
}
return issues;
}
export function resolvePluginRoot(repoRoot: string, plugin: PluginRegistryEntry): string {
return path.join(repoRoot, plugin.submodule, plugin.path);
}
function assertUniquePluginIds(registry: PluginRegistry): void {
const seen = new Set<string>();
for (const plugin of registry.plugins) {
if (seen.has(plugin.pluginId)) {
throw new Error(`duplicate pluginId: ${plugin.pluginId}`);
}
seen.add(plugin.pluginId);
}
}