@agentrules/core 0.0.10 → 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/README.md +2 -2
- package/dist/index.d.ts +884 -148
- package/dist/index.js +624 -216
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,41 +1,9 @@
|
|
|
1
1
|
import { ZodError, z } from "zod";
|
|
2
2
|
import { createTwoFilesPatch } from "diff";
|
|
3
3
|
|
|
4
|
-
//#region src/constants.ts
|
|
5
|
-
/**
|
|
6
|
-
* Shared constants for agentrules presets and registry.
|
|
7
|
-
*/
|
|
8
|
-
/** Filename for preset configuration */
|
|
9
|
-
const PRESET_CONFIG_FILENAME = "agentrules.json";
|
|
10
|
-
/** Directory name for preset metadata (README, LICENSE, etc.) */
|
|
11
|
-
const AGENT_RULES_DIR = ".agentrules";
|
|
12
|
-
/** JSON Schema URL for preset configuration */
|
|
13
|
-
const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
14
|
-
/** API root path segment */
|
|
15
|
-
const API_PATH = "api";
|
|
16
|
-
/** Default version identifier for latest preset version */
|
|
17
|
-
const LATEST_VERSION = "latest";
|
|
18
|
-
/**
|
|
19
|
-
* API endpoint paths (relative to registry base URL).
|
|
20
|
-
*/
|
|
21
|
-
const API_ENDPOINTS = {
|
|
22
|
-
presets: {
|
|
23
|
-
base: `${API_PATH}/presets`,
|
|
24
|
-
get: (slug, platform, version = LATEST_VERSION) => `${API_PATH}/presets/${encodeURIComponent(slug)}/${encodeURIComponent(platform)}/${encodeURIComponent(version)}`,
|
|
25
|
-
unpublish: (slug, platform, version) => `${API_PATH}/presets/${encodeURIComponent(slug)}/${encodeURIComponent(platform)}/${encodeURIComponent(version)}`
|
|
26
|
-
},
|
|
27
|
-
auth: {
|
|
28
|
-
session: `${API_PATH}/auth/get-session`,
|
|
29
|
-
deviceCode: `${API_PATH}/auth/device/code`,
|
|
30
|
-
deviceToken: `${API_PATH}/auth/device/token`
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
4
|
//#region src/platform/types.ts
|
|
36
5
|
/**
|
|
37
|
-
*
|
|
38
|
-
* Add new platforms here - types and config will follow.
|
|
6
|
+
* Platform and rule type definitions.
|
|
39
7
|
*/
|
|
40
8
|
const PLATFORM_ID_TUPLE = [
|
|
41
9
|
"opencode",
|
|
@@ -46,30 +14,169 @@ const PLATFORM_ID_TUPLE = [
|
|
|
46
14
|
|
|
47
15
|
//#endregion
|
|
48
16
|
//#region src/platform/config.ts
|
|
49
|
-
/** List of supported platform IDs as a readonly tuple */
|
|
50
17
|
const PLATFORM_IDS = PLATFORM_ID_TUPLE;
|
|
51
18
|
/**
|
|
52
|
-
* Platform
|
|
53
|
-
* Single source of truth for all platform paths.
|
|
19
|
+
* Platform configuration including supported rule types and install paths.
|
|
54
20
|
*/
|
|
55
21
|
const PLATFORMS = {
|
|
56
22
|
opencode: {
|
|
23
|
+
label: "OpenCode",
|
|
57
24
|
projectDir: ".opencode",
|
|
58
|
-
globalDir: "~/.config/opencode"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
25
|
+
globalDir: "~/.config/opencode",
|
|
26
|
+
types: {
|
|
27
|
+
instruction: {
|
|
28
|
+
description: "Project instructions",
|
|
29
|
+
format: "markdown",
|
|
30
|
+
extension: "md",
|
|
31
|
+
projectPath: "AGENTS.md",
|
|
32
|
+
globalPath: "~/.config/opencode/AGENTS.md"
|
|
33
|
+
},
|
|
34
|
+
agent: {
|
|
35
|
+
description: "Specialized AI agent",
|
|
36
|
+
format: "markdown",
|
|
37
|
+
extension: "md",
|
|
38
|
+
projectPath: ".opencode/agent/{name}.md",
|
|
39
|
+
globalPath: "~/.config/opencode/agent/{name}.md"
|
|
40
|
+
},
|
|
41
|
+
command: {
|
|
42
|
+
description: "Custom slash command",
|
|
43
|
+
format: "markdown",
|
|
44
|
+
extension: "md",
|
|
45
|
+
projectPath: ".opencode/command/{name}.md",
|
|
46
|
+
globalPath: "~/.config/opencode/command/{name}.md"
|
|
47
|
+
},
|
|
48
|
+
tool: {
|
|
49
|
+
description: "Custom tool",
|
|
50
|
+
format: "typescript",
|
|
51
|
+
extension: "ts",
|
|
52
|
+
projectPath: ".opencode/tool/{name}.ts",
|
|
53
|
+
globalPath: "~/.config/opencode/tool/{name}.ts"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
63
56
|
},
|
|
64
57
|
claude: {
|
|
58
|
+
label: "Claude Code",
|
|
65
59
|
projectDir: ".claude",
|
|
66
|
-
globalDir: "~/.claude"
|
|
60
|
+
globalDir: "~/.claude",
|
|
61
|
+
types: {
|
|
62
|
+
instruction: {
|
|
63
|
+
description: "Project instructions",
|
|
64
|
+
format: "markdown",
|
|
65
|
+
extension: "md",
|
|
66
|
+
projectPath: "CLAUDE.md",
|
|
67
|
+
globalPath: "~/.claude/CLAUDE.md"
|
|
68
|
+
},
|
|
69
|
+
command: {
|
|
70
|
+
description: "Custom slash command",
|
|
71
|
+
format: "markdown",
|
|
72
|
+
extension: "md",
|
|
73
|
+
projectPath: ".claude/commands/{name}.md",
|
|
74
|
+
globalPath: "~/.claude/commands/{name}.md"
|
|
75
|
+
},
|
|
76
|
+
skill: {
|
|
77
|
+
description: "Custom skill",
|
|
78
|
+
format: "markdown",
|
|
79
|
+
extension: "md",
|
|
80
|
+
projectPath: ".claude/skills/{name}/SKILL.md",
|
|
81
|
+
globalPath: "~/.claude/skills/{name}/SKILL.md"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
67
84
|
},
|
|
68
85
|
cursor: {
|
|
86
|
+
label: "Cursor",
|
|
69
87
|
projectDir: ".cursor",
|
|
70
|
-
globalDir: "~/.cursor"
|
|
88
|
+
globalDir: "~/.cursor",
|
|
89
|
+
types: {
|
|
90
|
+
instruction: {
|
|
91
|
+
description: "Project instructions",
|
|
92
|
+
format: "markdown",
|
|
93
|
+
extension: "md",
|
|
94
|
+
projectPath: "AGENTS.md",
|
|
95
|
+
globalPath: null
|
|
96
|
+
},
|
|
97
|
+
rule: {
|
|
98
|
+
description: "Custom rule",
|
|
99
|
+
format: "mdc",
|
|
100
|
+
extension: "mdc",
|
|
101
|
+
projectPath: ".cursor/rules/{name}.mdc",
|
|
102
|
+
globalPath: null
|
|
103
|
+
},
|
|
104
|
+
command: {
|
|
105
|
+
description: "Custom slash command",
|
|
106
|
+
format: "markdown",
|
|
107
|
+
extension: "md",
|
|
108
|
+
projectPath: ".cursor/commands/{name}.md",
|
|
109
|
+
globalPath: "~/.cursor/commands/{name}.md"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
codex: {
|
|
114
|
+
label: "Codex",
|
|
115
|
+
projectDir: ".codex",
|
|
116
|
+
globalDir: "~/.codex",
|
|
117
|
+
types: {
|
|
118
|
+
instruction: {
|
|
119
|
+
description: "Project instructions",
|
|
120
|
+
format: "markdown",
|
|
121
|
+
extension: "md",
|
|
122
|
+
projectPath: "AGENTS.md",
|
|
123
|
+
globalPath: "~/.codex/AGENTS.md"
|
|
124
|
+
},
|
|
125
|
+
command: {
|
|
126
|
+
description: "Custom prompt",
|
|
127
|
+
format: "markdown",
|
|
128
|
+
extension: "md",
|
|
129
|
+
projectPath: null,
|
|
130
|
+
globalPath: "~/.codex/prompts/{name}.md"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
71
133
|
}
|
|
72
134
|
};
|
|
135
|
+
/** Valid rule types for each platform. Must be kept in sync with PLATFORMS. */
|
|
136
|
+
const PLATFORM_RULE_TYPES = {
|
|
137
|
+
opencode: [
|
|
138
|
+
"instruction",
|
|
139
|
+
"command",
|
|
140
|
+
"agent",
|
|
141
|
+
"tool"
|
|
142
|
+
],
|
|
143
|
+
claude: [
|
|
144
|
+
"instruction",
|
|
145
|
+
"command",
|
|
146
|
+
"skill"
|
|
147
|
+
],
|
|
148
|
+
codex: ["instruction", "command"],
|
|
149
|
+
cursor: [
|
|
150
|
+
"instruction",
|
|
151
|
+
"command",
|
|
152
|
+
"rule"
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
/** Get valid rule types for a specific platform */
|
|
156
|
+
function getValidRuleTypes(platform) {
|
|
157
|
+
return PLATFORM_RULE_TYPES[platform];
|
|
158
|
+
}
|
|
159
|
+
/** Check if a type is valid for a given platform */
|
|
160
|
+
function isValidRuleType(platform, type) {
|
|
161
|
+
return PLATFORM_RULE_TYPES[platform].includes(type);
|
|
162
|
+
}
|
|
163
|
+
/** Get the configuration for a specific platform + type combination */
|
|
164
|
+
function getRuleTypeConfig(platform, type) {
|
|
165
|
+
const platformConfig = PLATFORMS[platform];
|
|
166
|
+
return platformConfig.types[type];
|
|
167
|
+
}
|
|
168
|
+
/** Get the install path for a rule, replacing {name} placeholder */
|
|
169
|
+
function getInstallPath(platform, type, name, location = "project") {
|
|
170
|
+
const config = getRuleTypeConfig(platform, type);
|
|
171
|
+
if (!config) return null;
|
|
172
|
+
const pathTemplate = location === "project" ? config.projectPath : config.globalPath;
|
|
173
|
+
if (!pathTemplate) return null;
|
|
174
|
+
return pathTemplate.replace("{name}", name);
|
|
175
|
+
}
|
|
176
|
+
/** Get platform configuration */
|
|
177
|
+
function getPlatformConfig(platform) {
|
|
178
|
+
return PLATFORMS[platform];
|
|
179
|
+
}
|
|
73
180
|
|
|
74
181
|
//#endregion
|
|
75
182
|
//#region src/platform/utils.ts
|
|
@@ -117,25 +224,56 @@ function toUint8Array(payload) {
|
|
|
117
224
|
}
|
|
118
225
|
|
|
119
226
|
//#endregion
|
|
120
|
-
//#region src/
|
|
227
|
+
//#region src/schemas/common.ts
|
|
121
228
|
const PLATFORM_ID_SET = new Set(PLATFORM_IDS);
|
|
122
|
-
const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
|
|
123
|
-
const platformIdSchema = z.enum(PLATFORM_IDS);
|
|
124
|
-
const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title must be 80 characters or less");
|
|
125
|
-
const descriptionSchema = z.string().trim().min(1, "Description is required").max(500, "Description must be 500 characters or less");
|
|
126
|
-
const versionSchema = z.string().trim().regex(VERSION_REGEX, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
|
|
127
|
-
const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
|
|
128
229
|
const TAG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
129
230
|
const TAG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-tag)";
|
|
231
|
+
/**
|
|
232
|
+
* Schema for a single tag.
|
|
233
|
+
* - Max 35 characters
|
|
234
|
+
* - Lowercase alphanumeric with hyphens
|
|
235
|
+
* - Platform names blocked (redundant with platform field)
|
|
236
|
+
*/
|
|
130
237
|
const tagSchema = z.string().trim().min(1, "Tag cannot be empty").max(35, "Tag must be 35 characters or less").regex(TAG_REGEX, TAG_ERROR).refine((tag) => !PLATFORM_ID_SET.has(tag), { message: "Platform names cannot be used as tags (redundant with platform field)" });
|
|
238
|
+
/**
|
|
239
|
+
* Schema for tags array.
|
|
240
|
+
* - 1-10 tags required
|
|
241
|
+
*/
|
|
131
242
|
const tagsSchema = z.array(tagSchema).min(1, "At least one tag is required").max(10, "Maximum 10 tags allowed");
|
|
243
|
+
const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
244
|
+
const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
|
|
245
|
+
/**
|
|
246
|
+
* Schema for preset/rule name.
|
|
247
|
+
* - Max 64 characters
|
|
248
|
+
* - Lowercase alphanumeric with hyphens
|
|
249
|
+
*/
|
|
250
|
+
const nameSchema = z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less").regex(NAME_REGEX, NAME_ERROR);
|
|
251
|
+
/**
|
|
252
|
+
* Schema for display title.
|
|
253
|
+
* - Max 80 characters
|
|
254
|
+
*/
|
|
255
|
+
const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title must be 80 characters or less");
|
|
256
|
+
/**
|
|
257
|
+
* Schema for description.
|
|
258
|
+
* - Max 500 characters
|
|
259
|
+
*/
|
|
260
|
+
const descriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/preset/schema.ts
|
|
264
|
+
const VERSION_REGEX$1 = /^[1-9]\d*\.\d+$/;
|
|
265
|
+
const platformIdSchema = z.enum(PLATFORM_IDS);
|
|
266
|
+
/**
|
|
267
|
+
* Schema for required description (presets require a description).
|
|
268
|
+
* Extends base descriptionSchema with min(1) constraint.
|
|
269
|
+
*/
|
|
270
|
+
const requiredDescriptionSchema = descriptionSchema.min(1, "Description is required");
|
|
271
|
+
const versionSchema = z.string().trim().regex(VERSION_REGEX$1, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
|
|
272
|
+
const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
|
|
132
273
|
const featureSchema = z.string().trim().min(1, "Feature cannot be empty").max(100, "Feature must be 100 characters or less");
|
|
133
274
|
const featuresSchema = z.array(featureSchema).max(5, "Maximum 5 features allowed");
|
|
134
275
|
const installMessageSchema = z.string().trim().max(2e3, "Install message must be 2000 characters or less");
|
|
135
276
|
const contentSchema = z.string();
|
|
136
|
-
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
137
|
-
const SLUG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
|
|
138
|
-
const slugSchema = z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less").regex(SLUG_REGEX, SLUG_ERROR);
|
|
139
277
|
const COMMON_LICENSES = [
|
|
140
278
|
"MIT",
|
|
141
279
|
"Apache-2.0",
|
|
@@ -148,60 +286,101 @@ const licenseSchema = z.string().trim().min(1, "License is required").max(128, "
|
|
|
148
286
|
const pathSchema = z.string().trim().min(1);
|
|
149
287
|
const ignorePatternSchema = z.string().trim().min(1, "Ignore pattern cannot be empty");
|
|
150
288
|
const ignoreSchema = z.array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
|
|
289
|
+
/**
|
|
290
|
+
* Schema for agentrulesDir - directory containing metadata files (README, LICENSE, INSTALL).
|
|
291
|
+
* Defaults to ".agentrules" if not specified.
|
|
292
|
+
* Use "." to read metadata from the project root.
|
|
293
|
+
*/
|
|
294
|
+
const agentrulesPathSchema = z.string().trim().min(1, "agentrulesDir cannot be empty");
|
|
295
|
+
/**
|
|
296
|
+
* Platform entry - either a platform ID string or an object with optional path.
|
|
297
|
+
*
|
|
298
|
+
* Examples:
|
|
299
|
+
* - "opencode" (shorthand, uses default directory)
|
|
300
|
+
* - { platform: "opencode", path: "rules" } (custom path)
|
|
301
|
+
*/
|
|
302
|
+
const platformEntryObjectSchema = z.object({
|
|
303
|
+
platform: platformIdSchema,
|
|
304
|
+
path: pathSchema.optional()
|
|
305
|
+
}).strict();
|
|
306
|
+
const platformEntrySchema = z.union([platformIdSchema, platformEntryObjectSchema]);
|
|
307
|
+
/**
|
|
308
|
+
* Preset config schema.
|
|
309
|
+
*
|
|
310
|
+
* Uses a unified `platforms` array that accepts either:
|
|
311
|
+
* - Platform ID strings: `["opencode", "claude"]`
|
|
312
|
+
* - Objects with optional path: `[{ platform: "opencode", path: "rules" }]`
|
|
313
|
+
* - Mixed: `["opencode", { platform: "claude", path: "my-claude" }]`
|
|
314
|
+
*/
|
|
151
315
|
const presetConfigSchema = z.object({
|
|
152
316
|
$schema: z.string().optional(),
|
|
153
|
-
name:
|
|
317
|
+
name: nameSchema,
|
|
154
318
|
title: titleSchema,
|
|
155
319
|
version: majorVersionSchema.optional(),
|
|
156
|
-
description:
|
|
320
|
+
description: requiredDescriptionSchema,
|
|
157
321
|
tags: tagsSchema,
|
|
158
322
|
features: featuresSchema.optional(),
|
|
159
323
|
license: licenseSchema,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
324
|
+
ignore: ignoreSchema.optional(),
|
|
325
|
+
agentrulesDir: agentrulesPathSchema.optional(),
|
|
326
|
+
platforms: z.array(platformEntrySchema).min(1, "At least one platform is required")
|
|
163
327
|
}).strict();
|
|
164
328
|
const bundledFileSchema = z.object({
|
|
165
329
|
path: z.string().min(1),
|
|
166
330
|
size: z.number().int().nonnegative(),
|
|
167
331
|
checksum: z.string().length(64),
|
|
168
|
-
|
|
332
|
+
content: z.string()
|
|
169
333
|
});
|
|
170
334
|
/**
|
|
171
|
-
* Schema for
|
|
335
|
+
* Schema for per-platform variant in publish input.
|
|
336
|
+
*/
|
|
337
|
+
const publishVariantInputSchema = z.object({
|
|
338
|
+
platform: platformIdSchema,
|
|
339
|
+
files: z.array(bundledFileSchema).min(1),
|
|
340
|
+
readmeContent: contentSchema.optional(),
|
|
341
|
+
licenseContent: contentSchema.optional(),
|
|
342
|
+
installMessage: installMessageSchema.optional()
|
|
343
|
+
});
|
|
344
|
+
/**
|
|
345
|
+
* Schema for what clients send to publish a preset (multi-platform).
|
|
346
|
+
*
|
|
347
|
+
* One publish call creates ONE version with ALL platform variants.
|
|
172
348
|
* Version is optional major version. Registry assigns full MAJOR.MINOR.
|
|
349
|
+
*
|
|
350
|
+
* Note: Clients send `name` (e.g., "my-preset"), and the registry defines the format of the slug.
|
|
351
|
+
* For example, a namespaced slug could be returned as "username/my-preset"
|
|
173
352
|
*/
|
|
174
353
|
const presetPublishInputSchema = z.object({
|
|
175
|
-
|
|
176
|
-
platform: platformIdSchema,
|
|
354
|
+
name: nameSchema,
|
|
177
355
|
title: titleSchema,
|
|
178
|
-
description:
|
|
356
|
+
description: requiredDescriptionSchema,
|
|
179
357
|
tags: tagsSchema,
|
|
180
358
|
license: licenseSchema,
|
|
181
|
-
licenseContent: contentSchema.optional(),
|
|
182
|
-
readmeContent: contentSchema.optional(),
|
|
183
359
|
features: featuresSchema.optional(),
|
|
184
|
-
|
|
185
|
-
files: z.array(bundledFileSchema).min(1),
|
|
360
|
+
variants: z.array(publishVariantInputSchema).min(1, "At least one platform variant is required"),
|
|
186
361
|
version: majorVersionSchema.optional()
|
|
187
362
|
});
|
|
188
363
|
/**
|
|
189
364
|
* Schema for what registries store and return.
|
|
190
|
-
* Includes
|
|
191
|
-
*/
|
|
192
|
-
const presetBundleSchema = presetPublishInputSchema.omit({
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
readmeContent: true,
|
|
196
|
-
licenseContent: true,
|
|
197
|
-
installMessage: true
|
|
365
|
+
* Includes full namespaced slug and version assigned by registry.
|
|
366
|
+
*/
|
|
367
|
+
const presetBundleSchema = presetPublishInputSchema.omit({
|
|
368
|
+
name: true,
|
|
369
|
+
version: true
|
|
198
370
|
}).extend({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
fileCount: z.number().int().nonnegative(),
|
|
202
|
-
totalSize: z.number().int().nonnegative()
|
|
371
|
+
slug: z.string().trim().min(1),
|
|
372
|
+
version: versionSchema
|
|
203
373
|
});
|
|
204
|
-
|
|
374
|
+
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/preset/types.ts
|
|
377
|
+
/**
|
|
378
|
+
* Normalize a raw platform entry to the object form.
|
|
379
|
+
*/
|
|
380
|
+
function normalizePlatformEntry(entry) {
|
|
381
|
+
if (typeof entry === "string") return { platform: entry };
|
|
382
|
+
return entry;
|
|
383
|
+
}
|
|
205
384
|
|
|
206
385
|
//#endregion
|
|
207
386
|
//#region src/builder/utils.ts
|
|
@@ -210,9 +389,10 @@ function cleanInstallMessage(value) {
|
|
|
210
389
|
const trimmed = value.trim();
|
|
211
390
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
212
391
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Validate raw preset config from JSON.
|
|
394
|
+
* Returns the raw config shape (before normalization).
|
|
395
|
+
*/
|
|
216
396
|
function validatePresetConfig(config, slug) {
|
|
217
397
|
try {
|
|
218
398
|
return presetConfigSchema.parse(config);
|
|
@@ -235,7 +415,7 @@ const NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
|
235
415
|
* Directory name for bundle files in static registry output.
|
|
236
416
|
* Used by `agentrules registry build` to structure output.
|
|
237
417
|
*/
|
|
238
|
-
const STATIC_BUNDLE_DIR = "
|
|
418
|
+
const STATIC_BUNDLE_DIR = "registry";
|
|
239
419
|
/**
|
|
240
420
|
* Compute SHA-256 hash using Web Crypto API (works in browser and Node.js 15+)
|
|
241
421
|
*/
|
|
@@ -246,121 +426,128 @@ async function sha256(data) {
|
|
|
246
426
|
}
|
|
247
427
|
/**
|
|
248
428
|
* Builds a PresetPublishInput from preset input.
|
|
249
|
-
*
|
|
429
|
+
*
|
|
430
|
+
* PresetInput always has platformFiles array (unified format).
|
|
250
431
|
*/
|
|
251
432
|
async function buildPresetPublishInput(options) {
|
|
252
|
-
const { preset
|
|
253
|
-
if (!NAME_PATTERN.test(
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
433
|
+
const { preset, version } = options;
|
|
434
|
+
if (!NAME_PATTERN.test(preset.name)) throw new Error(`Invalid name "${preset.name}". Names must be lowercase kebab-case.`);
|
|
435
|
+
const config = validatePresetConfig(preset.config, preset.name);
|
|
436
|
+
const majorVersion = version ?? config.version;
|
|
437
|
+
const platforms = preset.config.platforms;
|
|
438
|
+
if (platforms.length === 0) throw new Error(`Preset ${preset.name} must specify at least one platform.`);
|
|
439
|
+
for (const entry of platforms) ensureKnownPlatform(entry.platform, preset.name);
|
|
440
|
+
const variants = [];
|
|
441
|
+
for (const entry of platforms) {
|
|
442
|
+
const platformData = preset.platformFiles.find((pf) => pf.platform === entry.platform);
|
|
443
|
+
if (!platformData) throw new Error(`Preset ${preset.name} is missing files for platform "${entry.platform}".`);
|
|
444
|
+
if (platformData.files.length === 0) throw new Error(`Preset ${preset.name} has no files for platform "${entry.platform}".`);
|
|
445
|
+
const files = await createBundledFilesFromInputs(platformData.files);
|
|
446
|
+
variants.push({
|
|
447
|
+
platform: entry.platform,
|
|
448
|
+
files,
|
|
449
|
+
readmeContent: platformData.readmeContent?.trim() || preset.readmeContent?.trim() || void 0,
|
|
450
|
+
licenseContent: platformData.licenseContent?.trim() || preset.licenseContent?.trim() || void 0,
|
|
451
|
+
installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(preset.installMessage)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
264
454
|
return {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
readmeContent,
|
|
273
|
-
features,
|
|
274
|
-
installMessage,
|
|
275
|
-
files,
|
|
455
|
+
name: preset.name,
|
|
456
|
+
title: config.title,
|
|
457
|
+
description: config.description,
|
|
458
|
+
tags: config.tags ?? [],
|
|
459
|
+
license: config.license,
|
|
460
|
+
features: config.features ?? [],
|
|
461
|
+
variants,
|
|
276
462
|
...majorVersion !== void 0 && { version: majorVersion }
|
|
277
463
|
};
|
|
278
464
|
}
|
|
279
465
|
/**
|
|
280
|
-
* Builds a static registry with
|
|
281
|
-
*
|
|
282
|
-
*
|
|
466
|
+
* Builds a static registry with items and bundles.
|
|
467
|
+
*
|
|
468
|
+
* Uses the same model as dynamic publishing:
|
|
469
|
+
* - Each PresetInput (single or multi-platform) becomes one item
|
|
470
|
+
* - Each platform variant becomes one bundle
|
|
283
471
|
*/
|
|
284
472
|
async function buildPresetRegistry(options) {
|
|
285
473
|
const bundleBase = normalizeBundleBase(options.bundleBase);
|
|
286
|
-
const
|
|
474
|
+
const items = [];
|
|
287
475
|
const bundles = [];
|
|
288
476
|
for (const presetInput of options.presets) {
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
fileCount: files.length,
|
|
314
|
-
totalSize
|
|
315
|
-
};
|
|
316
|
-
entries.push(entry);
|
|
317
|
-
const bundle = {
|
|
318
|
-
slug: presetInput.slug,
|
|
319
|
-
platform,
|
|
320
|
-
title: presetConfig.title,
|
|
321
|
-
version,
|
|
322
|
-
description: presetConfig.description,
|
|
323
|
-
tags: presetConfig.tags ?? [],
|
|
324
|
-
license: presetConfig.license,
|
|
325
|
-
licenseContent,
|
|
326
|
-
readmeContent,
|
|
327
|
-
features,
|
|
328
|
-
installMessage,
|
|
329
|
-
files
|
|
477
|
+
const publishInput = await buildPresetPublishInput({ preset: presetInput });
|
|
478
|
+
const slug = publishInput.name;
|
|
479
|
+
const version = `${publishInput.version ?? 1}.0`;
|
|
480
|
+
const presetVariants = publishInput.variants.map((v) => ({
|
|
481
|
+
platform: v.platform,
|
|
482
|
+
bundleUrl: getBundlePath(bundleBase, slug, v.platform, version),
|
|
483
|
+
fileCount: v.files.length,
|
|
484
|
+
totalSize: v.files.reduce((sum, f) => sum + f.content.length, 0)
|
|
485
|
+
}));
|
|
486
|
+
presetVariants.sort((a, b) => a.platform.localeCompare(b.platform));
|
|
487
|
+
const item = {
|
|
488
|
+
kind: "preset",
|
|
489
|
+
slug,
|
|
490
|
+
name: publishInput.title,
|
|
491
|
+
title: publishInput.title,
|
|
492
|
+
description: publishInput.description,
|
|
493
|
+
tags: publishInput.tags,
|
|
494
|
+
license: publishInput.license,
|
|
495
|
+
features: publishInput.features ?? [],
|
|
496
|
+
versions: [{
|
|
497
|
+
version,
|
|
498
|
+
isLatest: true,
|
|
499
|
+
variants: presetVariants
|
|
500
|
+
}]
|
|
330
501
|
};
|
|
331
|
-
|
|
502
|
+
items.push(item);
|
|
503
|
+
for (const variant of publishInput.variants) {
|
|
504
|
+
const bundle = {
|
|
505
|
+
name: publishInput.name,
|
|
506
|
+
slug,
|
|
507
|
+
platform: variant.platform,
|
|
508
|
+
title: publishInput.title,
|
|
509
|
+
version,
|
|
510
|
+
description: publishInput.description,
|
|
511
|
+
tags: publishInput.tags,
|
|
512
|
+
license: publishInput.license,
|
|
513
|
+
features: publishInput.features,
|
|
514
|
+
files: variant.files,
|
|
515
|
+
readmeContent: variant.readmeContent,
|
|
516
|
+
licenseContent: variant.licenseContent,
|
|
517
|
+
installMessage: variant.installMessage
|
|
518
|
+
};
|
|
519
|
+
bundles.push(bundle);
|
|
520
|
+
}
|
|
332
521
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}, {});
|
|
522
|
+
items.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
523
|
+
bundles.sort((a, b) => {
|
|
524
|
+
if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
|
|
525
|
+
return a.slug.localeCompare(b.slug);
|
|
526
|
+
});
|
|
339
527
|
return {
|
|
340
|
-
|
|
341
|
-
index,
|
|
528
|
+
items,
|
|
342
529
|
bundles
|
|
343
530
|
};
|
|
344
531
|
}
|
|
345
532
|
async function createBundledFilesFromInputs(files) {
|
|
346
533
|
const results = await Promise.all(files.map(async (file) => {
|
|
347
|
-
const payload = normalizeFilePayload(file.
|
|
348
|
-
const
|
|
534
|
+
const payload = normalizeFilePayload(file.content);
|
|
535
|
+
const content = encodeFilePayload(payload, file.path);
|
|
349
536
|
const checksum = await sha256(payload);
|
|
350
537
|
return {
|
|
351
538
|
path: toPosixPath(file.path),
|
|
352
539
|
size: payload.length,
|
|
353
540
|
checksum,
|
|
354
|
-
|
|
541
|
+
content
|
|
355
542
|
};
|
|
356
543
|
}));
|
|
357
544
|
return results.sort((a, b) => a.path.localeCompare(b.path));
|
|
358
545
|
}
|
|
359
|
-
function normalizeFilePayload(
|
|
360
|
-
if (typeof
|
|
361
|
-
if (
|
|
362
|
-
if (ArrayBuffer.isView(
|
|
363
|
-
return new Uint8Array(
|
|
546
|
+
function normalizeFilePayload(content) {
|
|
547
|
+
if (typeof content === "string") return new TextEncoder().encode(content);
|
|
548
|
+
if (content instanceof ArrayBuffer) return new Uint8Array(content);
|
|
549
|
+
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
550
|
+
return new Uint8Array(content);
|
|
364
551
|
}
|
|
365
552
|
function encodeFilePayload(data, filePath) {
|
|
366
553
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -370,36 +557,22 @@ function encodeFilePayload(data, filePath) {
|
|
|
370
557
|
throw new Error(`Binary files are not supported: "${filePath}". Only UTF-8 text files are allowed.`);
|
|
371
558
|
}
|
|
372
559
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Normalize bundle base by removing trailing slashes.
|
|
375
|
-
* Returns empty string if base is undefined/empty (use default relative path).
|
|
376
|
-
*/
|
|
377
560
|
function normalizeBundleBase(base) {
|
|
378
561
|
if (!base) return "";
|
|
379
562
|
return base.replace(/\/+$/, "");
|
|
380
563
|
}
|
|
381
|
-
|
|
382
|
-
* Returns the bundle URL/path for a preset.
|
|
383
|
-
* Format: {base}/{STATIC_BUNDLE_DIR}/{slug}/{platform}/{version}
|
|
384
|
-
*/
|
|
385
|
-
function getBundlePath(base, slug, platform, version = LATEST_VERSION) {
|
|
564
|
+
function getBundlePath(base, slug, platform, version) {
|
|
386
565
|
const prefix = base ? `${base}/` : "";
|
|
387
566
|
return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${platform}/${version}`;
|
|
388
567
|
}
|
|
389
568
|
function ensureKnownPlatform(platform, slug) {
|
|
390
569
|
if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
391
570
|
}
|
|
392
|
-
function sortBySlugAndPlatform(items) {
|
|
393
|
-
items.sort((a, b) => {
|
|
394
|
-
if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
|
|
395
|
-
return a.slug.localeCompare(b.slug);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
571
|
|
|
399
572
|
//#endregion
|
|
400
573
|
//#region src/client/bundle.ts
|
|
401
574
|
function decodeBundledFile(file) {
|
|
402
|
-
return encodeUtf8(file.
|
|
575
|
+
return encodeUtf8(file.content);
|
|
403
576
|
}
|
|
404
577
|
async function verifyBundledFileChecksum(file, payload) {
|
|
405
578
|
const bytes = toUint8Array(payload);
|
|
@@ -427,39 +600,48 @@ async function sha256Hex(payload) {
|
|
|
427
600
|
}
|
|
428
601
|
|
|
429
602
|
//#endregion
|
|
430
|
-
//#region src/
|
|
603
|
+
//#region src/constants.ts
|
|
604
|
+
/**
|
|
605
|
+
* Shared constants for agentrules presets and registry.
|
|
606
|
+
*/
|
|
607
|
+
/** Filename for preset configuration */
|
|
608
|
+
const PRESET_CONFIG_FILENAME = "agentrules.json";
|
|
609
|
+
/** Directory name for preset metadata (README, LICENSE, etc.) */
|
|
610
|
+
const AGENT_RULES_DIR = ".agentrules";
|
|
611
|
+
/** JSON Schema URL for preset configuration */
|
|
612
|
+
const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
613
|
+
/** API root path segment */
|
|
614
|
+
const API_PATH = "api";
|
|
615
|
+
/** Default version identifier for latest preset version */
|
|
616
|
+
const LATEST_VERSION = "latest";
|
|
431
617
|
/**
|
|
432
|
-
*
|
|
433
|
-
* Returns the entry metadata and the absolute bundle URL.
|
|
618
|
+
* API endpoint paths (relative to registry base URL).
|
|
434
619
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* @param version - Version to resolve (defaults to "latest")
|
|
620
|
+
* Note on slug handling:
|
|
621
|
+
* - Slugs may contain slashes (e.g., "username/my-preset") which flow through as path segments
|
|
622
|
+
* - The client is responsible for validating values before making requests
|
|
439
623
|
*/
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
}
|
|
624
|
+
const API_ENDPOINTS = {
|
|
625
|
+
presets: {
|
|
626
|
+
base: `${API_PATH}/presets`,
|
|
627
|
+
unpublish: (slug, version) => `${API_PATH}/presets/${slug}/${version}`
|
|
628
|
+
},
|
|
629
|
+
auth: {
|
|
630
|
+
session: `${API_PATH}/auth/get-session`,
|
|
631
|
+
deviceCode: `${API_PATH}/auth/device/code`,
|
|
632
|
+
deviceToken: `${API_PATH}/auth/device/token`
|
|
633
|
+
},
|
|
634
|
+
rules: {
|
|
635
|
+
base: `${API_PATH}/rules`,
|
|
636
|
+
bySlug: (slug) => `${API_PATH}/rules/${slug}`
|
|
637
|
+
},
|
|
638
|
+
items: { get: (slug) => `${API_PATH}/items/${slug}` }
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/client/registry.ts
|
|
461
643
|
/**
|
|
462
|
-
* Fetches a bundle from an absolute URL
|
|
644
|
+
* Fetches a bundle from an absolute URL.
|
|
463
645
|
*/
|
|
464
646
|
async function fetchBundle(bundleUrl) {
|
|
465
647
|
const response = await fetch(bundleUrl);
|
|
@@ -470,6 +652,232 @@ async function fetchBundle(bundleUrl) {
|
|
|
470
652
|
throw new Error(`Unable to parse bundle JSON: ${error.message}`);
|
|
471
653
|
}
|
|
472
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Resolves a slug to get all versions and platform variants.
|
|
657
|
+
*
|
|
658
|
+
* @param baseUrl - Registry base URL
|
|
659
|
+
* @param slug - Content slug (may contain slashes, e.g., "username/my-preset")
|
|
660
|
+
* @param version - Optional version filter (server may ignore for static registries)
|
|
661
|
+
* @returns Resolved data, or null if not found
|
|
662
|
+
* @throws Error on network/server errors
|
|
663
|
+
*/
|
|
664
|
+
async function resolveSlug(baseUrl, slug, version) {
|
|
665
|
+
const url = new URL(API_ENDPOINTS.items.get(slug), baseUrl);
|
|
666
|
+
if (version) url.searchParams.set("version", version);
|
|
667
|
+
let response;
|
|
668
|
+
try {
|
|
669
|
+
response = await fetch(url);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
throw new Error(`Failed to connect to registry: ${error.message}`);
|
|
672
|
+
}
|
|
673
|
+
if (response.status === 404) return null;
|
|
674
|
+
if (!response.ok) {
|
|
675
|
+
let errorMessage = `Registry returned ${response.status} ${response.statusText}`;
|
|
676
|
+
try {
|
|
677
|
+
const body = await response.json();
|
|
678
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") errorMessage = body.error;
|
|
679
|
+
} catch {}
|
|
680
|
+
throw new Error(errorMessage);
|
|
681
|
+
}
|
|
682
|
+
const data = await response.json();
|
|
683
|
+
if (data.kind === "preset") {
|
|
684
|
+
for (const ver of data.versions) for (const variant of ver.variants) if ("bundleUrl" in variant) variant.bundleUrl = new URL(variant.bundleUrl, baseUrl).toString();
|
|
685
|
+
}
|
|
686
|
+
return data;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/resolve/schema.ts
|
|
691
|
+
const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
|
|
692
|
+
const presetVariantBundleSchema = z.object({
|
|
693
|
+
platform: platformIdSchema,
|
|
694
|
+
bundleUrl: z.string().min(1),
|
|
695
|
+
fileCount: z.number().int().nonnegative(),
|
|
696
|
+
totalSize: z.number().int().nonnegative()
|
|
697
|
+
});
|
|
698
|
+
const presetVariantInlineSchema = z.object({
|
|
699
|
+
platform: platformIdSchema,
|
|
700
|
+
content: z.string().min(1),
|
|
701
|
+
fileCount: z.number().int().nonnegative(),
|
|
702
|
+
totalSize: z.number().int().nonnegative()
|
|
703
|
+
});
|
|
704
|
+
const presetVariantSchema = z.union([presetVariantBundleSchema, presetVariantInlineSchema]);
|
|
705
|
+
const ruleVariantSchema = z.object({
|
|
706
|
+
platform: platformIdSchema,
|
|
707
|
+
type: z.string().min(1),
|
|
708
|
+
content: z.string().min(1)
|
|
709
|
+
});
|
|
710
|
+
const presetVersionSchema = z.object({
|
|
711
|
+
version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
|
|
712
|
+
isLatest: z.boolean(),
|
|
713
|
+
publishedAt: z.string().datetime().optional(),
|
|
714
|
+
variants: z.array(presetVariantSchema).min(1)
|
|
715
|
+
});
|
|
716
|
+
const ruleVersionSchema = z.object({
|
|
717
|
+
version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
|
|
718
|
+
isLatest: z.boolean(),
|
|
719
|
+
publishedAt: z.string().datetime().optional(),
|
|
720
|
+
variants: z.array(ruleVariantSchema).min(1)
|
|
721
|
+
});
|
|
722
|
+
const resolvedPresetSchema = z.object({
|
|
723
|
+
kind: z.literal("preset"),
|
|
724
|
+
slug: z.string().min(1),
|
|
725
|
+
name: z.string().min(1),
|
|
726
|
+
title: z.string().min(1),
|
|
727
|
+
description: z.string(),
|
|
728
|
+
tags: z.array(z.string()),
|
|
729
|
+
license: z.string(),
|
|
730
|
+
features: z.array(z.string()),
|
|
731
|
+
versions: z.array(presetVersionSchema).min(1)
|
|
732
|
+
});
|
|
733
|
+
const resolvedRuleSchema = z.object({
|
|
734
|
+
kind: z.literal("rule"),
|
|
735
|
+
slug: z.string().min(1),
|
|
736
|
+
name: z.string().min(1),
|
|
737
|
+
title: z.string().min(1),
|
|
738
|
+
description: z.string(),
|
|
739
|
+
tags: z.array(z.string()),
|
|
740
|
+
versions: z.array(ruleVersionSchema).min(1)
|
|
741
|
+
});
|
|
742
|
+
const resolveResponseSchema = z.discriminatedUnion("kind", [resolvedPresetSchema, resolvedRuleSchema]);
|
|
743
|
+
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/resolve/utils.ts
|
|
746
|
+
/**
|
|
747
|
+
* Type guard for preset
|
|
748
|
+
*/
|
|
749
|
+
function isPreset(item) {
|
|
750
|
+
return item.kind === "preset";
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Type guard for rule
|
|
754
|
+
*/
|
|
755
|
+
function isRule(item) {
|
|
756
|
+
return item.kind === "rule";
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Type guard for preset variant with bundleUrl
|
|
760
|
+
*/
|
|
761
|
+
function hasBundle(variant) {
|
|
762
|
+
return "bundleUrl" in variant;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Type guard for preset variant with inline content
|
|
766
|
+
*/
|
|
767
|
+
function hasInlineContent(variant) {
|
|
768
|
+
return "content" in variant;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get the latest version from a resolved preset
|
|
772
|
+
*/
|
|
773
|
+
function getLatestPresetVersion(item) {
|
|
774
|
+
return item.versions.find((v) => v.isLatest);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get the latest version from a resolved rule
|
|
778
|
+
*/
|
|
779
|
+
function getLatestRuleVersion(item) {
|
|
780
|
+
return item.versions.find((v) => v.isLatest);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get a specific version from a resolved preset
|
|
784
|
+
*/
|
|
785
|
+
function getPresetVersion(item, version) {
|
|
786
|
+
return item.versions.find((v) => v.version === version);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Get a specific version from a resolved rule
|
|
790
|
+
*/
|
|
791
|
+
function getRuleVersion(item, version) {
|
|
792
|
+
return item.versions.find((v) => v.version === version);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Get a specific platform variant from a preset version
|
|
796
|
+
*/
|
|
797
|
+
function getPresetVariant(version, platform) {
|
|
798
|
+
return version.variants.find((v) => v.platform === platform);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Get a specific platform variant from a rule version
|
|
802
|
+
*/
|
|
803
|
+
function getRuleVariant(version, platform) {
|
|
804
|
+
return version.variants.find((v) => v.platform === platform);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get all available platforms for a preset version
|
|
808
|
+
*/
|
|
809
|
+
function getPresetPlatforms(version) {
|
|
810
|
+
return version.variants.map((v) => v.platform);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Get all available platforms for a rule version
|
|
814
|
+
*/
|
|
815
|
+
function getRulePlatforms(version) {
|
|
816
|
+
return version.variants.map((v) => v.platform);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Check if a platform is available in any version of a preset
|
|
820
|
+
*/
|
|
821
|
+
function presetHasPlatform(item, platform) {
|
|
822
|
+
return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Check if a platform is available in any version of a rule
|
|
826
|
+
*/
|
|
827
|
+
function ruleHasPlatform(item, platform) {
|
|
828
|
+
return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/rule/schema.ts
|
|
833
|
+
/**
|
|
834
|
+
* Rule-specific schema aliases.
|
|
835
|
+
* All use shared schemas for consistency with presets:
|
|
836
|
+
* - name: max 64 chars, lowercase kebab-case
|
|
837
|
+
* - title: max 80 chars
|
|
838
|
+
* - description: max 500 chars (optional for rules)
|
|
839
|
+
* - tags: max 35 chars each, 1-10 required, platform names blocked
|
|
840
|
+
*/
|
|
841
|
+
const ruleNameSchema = nameSchema;
|
|
842
|
+
const ruleTitleSchema = titleSchema;
|
|
843
|
+
const ruleDescriptionSchema = descriptionSchema;
|
|
844
|
+
const ruleTagSchema = tagSchema;
|
|
845
|
+
const ruleTagsSchema = tagsSchema;
|
|
846
|
+
const rulePlatformSchema = z.enum(PLATFORM_IDS);
|
|
847
|
+
const ruleTypeSchema = z.string().trim().min(1).max(32);
|
|
848
|
+
const ruleContentSchema = z.string().min(1, "Content is required").max(1e5, "Content must be 100KB or less");
|
|
849
|
+
/** Common fields shared across all platform-type combinations */
|
|
850
|
+
const ruleCommonFields = {
|
|
851
|
+
name: ruleNameSchema,
|
|
852
|
+
title: ruleTitleSchema,
|
|
853
|
+
description: ruleDescriptionSchema.optional(),
|
|
854
|
+
content: ruleContentSchema,
|
|
855
|
+
tags: ruleTagsSchema
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Discriminated union schema for platform + type combinations.
|
|
859
|
+
* Each platform has its own set of valid types.
|
|
860
|
+
*/
|
|
861
|
+
const rulePlatformTypeSchema = z.discriminatedUnion("platform", [
|
|
862
|
+
z.object({
|
|
863
|
+
platform: z.literal("opencode"),
|
|
864
|
+
type: z.enum(PLATFORM_RULE_TYPES.opencode)
|
|
865
|
+
}),
|
|
866
|
+
z.object({
|
|
867
|
+
platform: z.literal("claude"),
|
|
868
|
+
type: z.enum(PLATFORM_RULE_TYPES.claude)
|
|
869
|
+
}),
|
|
870
|
+
z.object({
|
|
871
|
+
platform: z.literal("cursor"),
|
|
872
|
+
type: z.enum(PLATFORM_RULE_TYPES.cursor)
|
|
873
|
+
}),
|
|
874
|
+
z.object({
|
|
875
|
+
platform: z.literal("codex"),
|
|
876
|
+
type: z.enum(PLATFORM_RULE_TYPES.codex)
|
|
877
|
+
})
|
|
878
|
+
]);
|
|
879
|
+
/** Schema for rule creation with discriminated union for platform+type */
|
|
880
|
+
const ruleCreateInputSchema = z.object(ruleCommonFields).and(rulePlatformTypeSchema);
|
|
473
881
|
|
|
474
882
|
//#endregion
|
|
475
883
|
//#region src/utils/diff.ts
|
|
@@ -495,4 +903,4 @@ function normalizeBundlePath(value) {
|
|
|
495
903
|
}
|
|
496
904
|
|
|
497
905
|
//#endregion
|
|
498
|
-
export { AGENT_RULES_DIR, API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PLATFORM_ID_TUPLE, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema,
|
|
906
|
+
export { AGENT_RULES_DIR, API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PLATFORM_ID_TUPLE, PLATFORM_RULE_TYPES, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeUtf8, fetchBundle, getInstallPath, getLatestPresetVersion, getLatestRuleVersion, getPlatformConfig, getPlatformFromDir, getPresetPlatforms, getPresetVariant, getPresetVersion, getRulePlatforms, getRuleTypeConfig, getRuleVariant, getRuleVersion, getValidRuleTypes, hasBundle, hasInlineContent, isLikelyText, isPlatformDir, isPreset, isRule, isSupportedPlatform, isValidRuleType, licenseSchema, nameSchema, normalizeBundlePath, normalizePlatformEntry, normalizePlatformInput, platformIdSchema, presetBundleSchema, presetConfigSchema, presetHasPlatform, presetPublishInputSchema, presetVariantSchema, presetVersionSchema, publishVariantInputSchema, requiredDescriptionSchema, resolveResponseSchema, resolveSlug, resolvedPresetSchema, resolvedRuleSchema, ruleContentSchema, ruleCreateInputSchema, ruleDescriptionSchema, ruleHasPlatform, ruleNameSchema, rulePlatformSchema, rulePlatformTypeSchema, ruleTagSchema, ruleTagsSchema, ruleTitleSchema, ruleTypeSchema, ruleVariantSchema, ruleVersionSchema, tagSchema, tagsSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
|