@agentrules/core 0.1.0 → 0.2.1
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 +80 -54
- package/dist/index.d.ts +613 -786
- package/dist/index.js +318 -314
- package/package.json +15 -10
package/dist/index.js
CHANGED
|
@@ -11,172 +11,189 @@ const PLATFORM_ID_TUPLE = [
|
|
|
11
11
|
"claude",
|
|
12
12
|
"cursor"
|
|
13
13
|
];
|
|
14
|
+
/** Tuple of all rule types for schema validation */
|
|
15
|
+
const RULE_TYPE_TUPLE = [
|
|
16
|
+
"instruction",
|
|
17
|
+
"rule",
|
|
18
|
+
"command",
|
|
19
|
+
"skill",
|
|
20
|
+
"agent",
|
|
21
|
+
"tool"
|
|
22
|
+
];
|
|
14
23
|
|
|
15
24
|
//#endregion
|
|
16
25
|
//#region src/platform/config.ts
|
|
17
26
|
const PLATFORM_IDS = PLATFORM_ID_TUPLE;
|
|
18
27
|
/**
|
|
19
|
-
* Platform configuration including supported
|
|
28
|
+
* Platform configuration including supported types and install paths.
|
|
20
29
|
*/
|
|
21
30
|
const PLATFORMS = {
|
|
22
31
|
opencode: {
|
|
23
32
|
label: "OpenCode",
|
|
24
|
-
|
|
33
|
+
platformDir: ".opencode",
|
|
25
34
|
globalDir: "~/.config/opencode",
|
|
26
35
|
types: {
|
|
27
36
|
instruction: {
|
|
28
37
|
description: "Project instructions",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
projectPath: "AGENTS.md",
|
|
32
|
-
globalPath: "~/.config/opencode/AGENTS.md"
|
|
38
|
+
project: "AGENTS.md",
|
|
39
|
+
global: "{platformDir}/AGENTS.md"
|
|
33
40
|
},
|
|
34
41
|
agent: {
|
|
35
42
|
description: "Specialized AI agent",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
projectPath: ".opencode/agent/{name}.md",
|
|
39
|
-
globalPath: "~/.config/opencode/agent/{name}.md"
|
|
43
|
+
project: "{platformDir}/agent/{name}.md",
|
|
44
|
+
global: "{platformDir}/agent/{name}.md"
|
|
40
45
|
},
|
|
41
46
|
command: {
|
|
42
47
|
description: "Custom slash command",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
projectPath: ".opencode/command/{name}.md",
|
|
46
|
-
globalPath: "~/.config/opencode/command/{name}.md"
|
|
48
|
+
project: "{platformDir}/command/{name}.md",
|
|
49
|
+
global: "{platformDir}/command/{name}.md"
|
|
47
50
|
},
|
|
48
51
|
tool: {
|
|
49
52
|
description: "Custom tool",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
projectPath: ".opencode/tool/{name}.ts",
|
|
53
|
-
globalPath: "~/.config/opencode/tool/{name}.ts"
|
|
53
|
+
project: "{platformDir}/tool/{name}.ts",
|
|
54
|
+
global: "{platformDir}/tool/{name}.ts"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
claude: {
|
|
58
59
|
label: "Claude Code",
|
|
59
|
-
|
|
60
|
+
platformDir: ".claude",
|
|
60
61
|
globalDir: "~/.claude",
|
|
61
62
|
types: {
|
|
62
63
|
instruction: {
|
|
63
64
|
description: "Project instructions",
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
project: "CLAUDE.md",
|
|
66
|
+
global: "{platformDir}/CLAUDE.md"
|
|
67
|
+
},
|
|
68
|
+
rule: {
|
|
69
|
+
description: "Project rule",
|
|
70
|
+
project: "{platformDir}/rules/{name}.md",
|
|
71
|
+
global: "{platformDir}/rules/{name}.md"
|
|
68
72
|
},
|
|
69
73
|
command: {
|
|
70
74
|
description: "Custom slash command",
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
projectPath: ".claude/commands/{name}.md",
|
|
74
|
-
globalPath: "~/.claude/commands/{name}.md"
|
|
75
|
+
project: "{platformDir}/commands/{name}.md",
|
|
76
|
+
global: "{platformDir}/commands/{name}.md"
|
|
75
77
|
},
|
|
76
78
|
skill: {
|
|
77
79
|
description: "Custom skill",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
projectPath: ".claude/skills/{name}/SKILL.md",
|
|
81
|
-
globalPath: "~/.claude/skills/{name}/SKILL.md"
|
|
80
|
+
project: "{platformDir}/skills/{name}/SKILL.md",
|
|
81
|
+
global: "{platformDir}/skills/{name}/SKILL.md"
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
cursor: {
|
|
86
86
|
label: "Cursor",
|
|
87
|
-
|
|
87
|
+
platformDir: ".cursor",
|
|
88
88
|
globalDir: "~/.cursor",
|
|
89
89
|
types: {
|
|
90
90
|
instruction: {
|
|
91
91
|
description: "Project instructions",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
projectPath: "AGENTS.md",
|
|
95
|
-
globalPath: null
|
|
92
|
+
project: "AGENTS.md",
|
|
93
|
+
global: null
|
|
96
94
|
},
|
|
97
95
|
rule: {
|
|
98
96
|
description: "Custom rule",
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
projectPath: ".cursor/rules/{name}.mdc",
|
|
102
|
-
globalPath: null
|
|
97
|
+
project: "{platformDir}/rules/{name}.mdc",
|
|
98
|
+
global: null
|
|
103
99
|
},
|
|
104
100
|
command: {
|
|
105
101
|
description: "Custom slash command",
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
projectPath: ".cursor/commands/{name}.md",
|
|
109
|
-
globalPath: "~/.cursor/commands/{name}.md"
|
|
102
|
+
project: "{platformDir}/commands/{name}.md",
|
|
103
|
+
global: "{platformDir}/commands/{name}.md"
|
|
110
104
|
}
|
|
111
105
|
}
|
|
112
106
|
},
|
|
113
107
|
codex: {
|
|
114
108
|
label: "Codex",
|
|
115
|
-
|
|
109
|
+
platformDir: ".codex",
|
|
116
110
|
globalDir: "~/.codex",
|
|
117
111
|
types: {
|
|
118
112
|
instruction: {
|
|
119
113
|
description: "Project instructions",
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
projectPath: "AGENTS.md",
|
|
123
|
-
globalPath: "~/.codex/AGENTS.md"
|
|
114
|
+
project: "AGENTS.md",
|
|
115
|
+
global: "{platformDir}/AGENTS.md"
|
|
124
116
|
},
|
|
125
117
|
command: {
|
|
126
118
|
description: "Custom prompt",
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
projectPath: null,
|
|
130
|
-
globalPath: "~/.codex/prompts/{name}.md"
|
|
119
|
+
project: null,
|
|
120
|
+
global: "{platformDir}/prompts/{name}.md"
|
|
131
121
|
}
|
|
132
122
|
}
|
|
133
123
|
}
|
|
134
124
|
};
|
|
135
|
-
/**
|
|
125
|
+
/** Get valid types for a specific platform */
|
|
126
|
+
function getValidTypes(platform) {
|
|
127
|
+
return Object.keys(PLATFORMS[platform].types);
|
|
128
|
+
}
|
|
129
|
+
/** Check if a type is valid for a given platform */
|
|
130
|
+
function isValidType(platform, type) {
|
|
131
|
+
return type in PLATFORMS[platform].types;
|
|
132
|
+
}
|
|
133
|
+
/** Get the configuration for a specific platform + type combination */
|
|
134
|
+
function getTypeConfig(platform, type) {
|
|
135
|
+
const platformConfig = PLATFORMS[platform];
|
|
136
|
+
return platformConfig.types[type];
|
|
137
|
+
}
|
|
138
|
+
function supportsInstallPath({ platform, type, scope = "project" }) {
|
|
139
|
+
const typeConfig = getTypeConfig(platform, type);
|
|
140
|
+
if (!typeConfig) return false;
|
|
141
|
+
const template = scope === "project" ? typeConfig.project : typeConfig.global;
|
|
142
|
+
return template !== null;
|
|
143
|
+
}
|
|
144
|
+
function getInstallPath({ platform, type, name, scope = "project" }) {
|
|
145
|
+
const platformConfig = PLATFORMS[platform];
|
|
146
|
+
const typeConfig = getTypeConfig(platform, type);
|
|
147
|
+
if (!typeConfig) return null;
|
|
148
|
+
const template = scope === "project" ? typeConfig.project : typeConfig.global;
|
|
149
|
+
if (!template) return null;
|
|
150
|
+
const rootDir = scope === "project" ? platformConfig.platformDir : platformConfig.globalDir;
|
|
151
|
+
if (template.includes("{name}") && !name) throw new Error(`Missing name for install path: platform="${platform}" type="${type}" scope="${scope}"`);
|
|
152
|
+
return template.replace("{platformDir}", rootDir).replace("{name}", name ?? "");
|
|
153
|
+
}
|
|
154
|
+
/** Get platform configuration */
|
|
155
|
+
function getPlatformConfig(platform) {
|
|
156
|
+
return PLATFORMS[platform];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get the relative install path for a type (without the root directory prefix).
|
|
160
|
+
* Returns the path relative to the platform's root directory.
|
|
161
|
+
*
|
|
162
|
+
* Example: For codex instruction with scope="global", returns "AGENTS.md"
|
|
163
|
+
* (not "~/.codex/AGENTS.md")
|
|
164
|
+
*/
|
|
165
|
+
function getRelativeInstallPath({ platform, type, name, scope = "project" }) {
|
|
166
|
+
const typeConfig = getTypeConfig(platform, type);
|
|
167
|
+
if (!typeConfig) return null;
|
|
168
|
+
const template = scope === "project" ? typeConfig.project : typeConfig.global;
|
|
169
|
+
if (!template) return null;
|
|
170
|
+
if (template.includes("{name}") && !name) throw new Error(`Missing name for install path: platform="${platform}" type="${type}" scope="${scope}"`);
|
|
171
|
+
return template.replace("{platformDir}/", "").replace("{platformDir}", "").replace("{name}", name ?? "");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Platform-specific type tuples for zod schema validation.
|
|
175
|
+
* Must be kept in sync with PLATFORMS types above.
|
|
176
|
+
*/
|
|
136
177
|
const PLATFORM_RULE_TYPES = {
|
|
137
178
|
opencode: [
|
|
138
179
|
"instruction",
|
|
139
|
-
"command",
|
|
140
180
|
"agent",
|
|
181
|
+
"command",
|
|
141
182
|
"tool"
|
|
142
183
|
],
|
|
143
184
|
claude: [
|
|
144
185
|
"instruction",
|
|
186
|
+
"rule",
|
|
145
187
|
"command",
|
|
146
188
|
"skill"
|
|
147
189
|
],
|
|
148
|
-
codex: ["instruction", "command"],
|
|
149
190
|
cursor: [
|
|
150
191
|
"instruction",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
]
|
|
192
|
+
"rule",
|
|
193
|
+
"command"
|
|
194
|
+
],
|
|
195
|
+
codex: ["instruction", "command"]
|
|
154
196
|
};
|
|
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
|
-
}
|
|
180
197
|
|
|
181
198
|
//#endregion
|
|
182
199
|
//#region src/platform/utils.ts
|
|
@@ -189,19 +206,89 @@ function normalizePlatformInput(value) {
|
|
|
189
206
|
throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
|
|
190
207
|
}
|
|
191
208
|
/**
|
|
192
|
-
* Check if a directory name matches a platform's
|
|
193
|
-
* Used to detect if a
|
|
209
|
+
* Check if a directory name matches a platform's platformDir.
|
|
210
|
+
* Used to detect if a rule config is inside a platform directory (in-project mode).
|
|
194
211
|
*/
|
|
195
212
|
function isPlatformDir(dirName) {
|
|
196
|
-
return PLATFORM_IDS.some((id) => PLATFORMS[id].
|
|
213
|
+
return PLATFORM_IDS.some((id) => PLATFORMS[id].platformDir === dirName);
|
|
197
214
|
}
|
|
198
215
|
/**
|
|
199
|
-
* Get the platform ID from a directory name, if it matches a platform's
|
|
216
|
+
* Get the platform ID from a directory name, if it matches a platform's platformDir.
|
|
200
217
|
*/
|
|
201
218
|
function getPlatformFromDir(dirName) {
|
|
202
|
-
for (const id of PLATFORM_IDS) if (PLATFORMS[id].
|
|
219
|
+
for (const id of PLATFORM_IDS) if (PLATFORMS[id].platformDir === dirName) return id;
|
|
203
220
|
return;
|
|
204
221
|
}
|
|
222
|
+
function normalizePathForInference(value) {
|
|
223
|
+
return value.replace(/\\/g, "/");
|
|
224
|
+
}
|
|
225
|
+
function getBasename(value) {
|
|
226
|
+
const normalized = normalizePathForInference(value);
|
|
227
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
228
|
+
return segments.at(-1) ?? "";
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Infer the platform from a file path by searching for platformDir segments.
|
|
232
|
+
*
|
|
233
|
+
* Example: "/repo/.claude/commands/foo.md" -> "claude"
|
|
234
|
+
*/
|
|
235
|
+
function inferPlatformFromPath(value) {
|
|
236
|
+
const normalized = normalizePathForInference(value);
|
|
237
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
238
|
+
const matches = PLATFORM_IDS.filter((id) => segments.includes(PLATFORMS[id].platformDir));
|
|
239
|
+
if (matches.length === 1) return matches[0];
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Return all platforms whose instruction file matches this basename.
|
|
244
|
+
*
|
|
245
|
+
* Example: "CLAUDE.md" -> ["claude"], "AGENTS.md" -> ["opencode", "cursor", "codex"]
|
|
246
|
+
*/
|
|
247
|
+
function inferInstructionPlatformsFromFileName(fileName) {
|
|
248
|
+
const matches = [];
|
|
249
|
+
for (const id of PLATFORM_IDS) {
|
|
250
|
+
const instructionPath = getInstallPath({
|
|
251
|
+
platform: id,
|
|
252
|
+
type: "instruction",
|
|
253
|
+
scope: "project"
|
|
254
|
+
});
|
|
255
|
+
if (instructionPath === fileName) matches.push(id);
|
|
256
|
+
}
|
|
257
|
+
return matches;
|
|
258
|
+
}
|
|
259
|
+
function getProjectTypeDirMap(platform) {
|
|
260
|
+
const map = new Map();
|
|
261
|
+
for (const [type, cfg] of Object.entries(PLATFORMS[platform].types)) {
|
|
262
|
+
const template = cfg.project;
|
|
263
|
+
if (!template) continue;
|
|
264
|
+
if (!template.startsWith("{platformDir}/")) continue;
|
|
265
|
+
const rest = template.slice(14);
|
|
266
|
+
const dir = rest.split("/")[0];
|
|
267
|
+
if (!dir || dir.includes("{")) continue;
|
|
268
|
+
map.set(dir, type);
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Infer a rule type from a file path for a known platform.
|
|
274
|
+
* Uses PLATFORMS templates as source-of-truth.
|
|
275
|
+
*/
|
|
276
|
+
function inferTypeFromPath(platform, filePath) {
|
|
277
|
+
const base = getBasename(filePath);
|
|
278
|
+
const instructionPath = getInstallPath({
|
|
279
|
+
platform,
|
|
280
|
+
type: "instruction",
|
|
281
|
+
scope: "project"
|
|
282
|
+
});
|
|
283
|
+
if (instructionPath === base) return "instruction";
|
|
284
|
+
const normalized = normalizePathForInference(filePath);
|
|
285
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
286
|
+
const platformDirIndex = segments.lastIndexOf(PLATFORMS[platform].platformDir);
|
|
287
|
+
if (platformDirIndex < 0) return;
|
|
288
|
+
const nextDir = segments[platformDirIndex + 1];
|
|
289
|
+
if (!nextDir) return;
|
|
290
|
+
return getProjectTypeDirMap(platform).get(nextDir);
|
|
291
|
+
}
|
|
205
292
|
|
|
206
293
|
//#endregion
|
|
207
294
|
//#region src/utils/encoding.ts
|
|
@@ -215,7 +302,10 @@ function encodeUtf8(value) {
|
|
|
215
302
|
function decodeUtf8(payload) {
|
|
216
303
|
const bytes = toUint8Array(payload);
|
|
217
304
|
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("utf8");
|
|
218
|
-
return new TextDecoder("utf-8", {
|
|
305
|
+
return new TextDecoder("utf-8", {
|
|
306
|
+
fatal: false,
|
|
307
|
+
ignoreBOM: false
|
|
308
|
+
}).decode(bytes);
|
|
219
309
|
}
|
|
220
310
|
function toUint8Array(payload) {
|
|
221
311
|
if (payload instanceof Uint8Array) return payload;
|
|
@@ -237,13 +327,13 @@ const TAG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-tag)";
|
|
|
237
327
|
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
328
|
/**
|
|
239
329
|
* Schema for tags array.
|
|
240
|
-
* -
|
|
330
|
+
* - 0-10 tags allowed
|
|
241
331
|
*/
|
|
242
|
-
const tagsSchema = z.array(tagSchema).
|
|
332
|
+
const tagsSchema = z.array(tagSchema).max(10, "Maximum 10 tags allowed");
|
|
243
333
|
const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
244
|
-
const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-
|
|
334
|
+
const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-rule)";
|
|
245
335
|
/**
|
|
246
|
-
* Schema for
|
|
336
|
+
* Schema for rule name.
|
|
247
337
|
* - Max 64 characters
|
|
248
338
|
* - Lowercase alphanumeric with hyphens
|
|
249
339
|
*/
|
|
@@ -260,14 +350,11 @@ const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title
|
|
|
260
350
|
const descriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
|
|
261
351
|
|
|
262
352
|
//#endregion
|
|
263
|
-
//#region src/
|
|
353
|
+
//#region src/rule/schema.ts
|
|
264
354
|
const VERSION_REGEX$1 = /^[1-9]\d*\.\d+$/;
|
|
265
355
|
const platformIdSchema = z.enum(PLATFORM_IDS);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
* Extends base descriptionSchema with min(1) constraint.
|
|
269
|
-
*/
|
|
270
|
-
const requiredDescriptionSchema = descriptionSchema.min(1, "Description is required");
|
|
356
|
+
const normalizedDescriptionSchema = descriptionSchema.optional().default("");
|
|
357
|
+
const normalizedTagsSchema = tagsSchema.optional().default([]);
|
|
271
358
|
const versionSchema = z.string().trim().regex(VERSION_REGEX$1, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
|
|
272
359
|
const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
|
|
273
360
|
const featureSchema = z.string().trim().min(1, "Feature cannot be empty").max(100, "Feature must be 100 characters or less");
|
|
@@ -287,12 +374,6 @@ const pathSchema = z.string().trim().min(1);
|
|
|
287
374
|
const ignorePatternSchema = z.string().trim().min(1, "Ignore pattern cannot be empty");
|
|
288
375
|
const ignoreSchema = z.array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
|
|
289
376
|
/**
|
|
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
377
|
* Platform entry - either a platform ID string or an object with optional path.
|
|
297
378
|
*
|
|
298
379
|
* Examples:
|
|
@@ -305,24 +386,32 @@ const platformEntryObjectSchema = z.object({
|
|
|
305
386
|
}).strict();
|
|
306
387
|
const platformEntrySchema = z.union([platformIdSchema, platformEntryObjectSchema]);
|
|
307
388
|
/**
|
|
308
|
-
*
|
|
389
|
+
* Schema for rule type.
|
|
390
|
+
* Valid types: instruction, rule, command, skill, agent, tool, multi.
|
|
391
|
+
* Optional - defaults to "multi" (freeform file structure).
|
|
392
|
+
* Platform-specific validation happens at publish time.
|
|
393
|
+
*/
|
|
394
|
+
const ruleTypeSchema = z.enum(RULE_TYPE_TUPLE);
|
|
395
|
+
const typeSchema = ruleTypeSchema.optional();
|
|
396
|
+
/**
|
|
397
|
+
* Rule config schema.
|
|
309
398
|
*
|
|
310
399
|
* Uses a unified `platforms` array that accepts either:
|
|
311
400
|
* - Platform ID strings: `["opencode", "claude"]`
|
|
312
401
|
* - Objects with optional path: `[{ platform: "opencode", path: "rules" }]`
|
|
313
402
|
* - Mixed: `["opencode", { platform: "claude", path: "my-claude" }]`
|
|
314
403
|
*/
|
|
315
|
-
const
|
|
404
|
+
const ruleConfigSchema = z.object({
|
|
316
405
|
$schema: z.string().optional(),
|
|
317
406
|
name: nameSchema,
|
|
407
|
+
type: typeSchema,
|
|
318
408
|
title: titleSchema,
|
|
319
409
|
version: majorVersionSchema.optional(),
|
|
320
|
-
description:
|
|
321
|
-
tags:
|
|
410
|
+
description: normalizedDescriptionSchema,
|
|
411
|
+
tags: normalizedTagsSchema,
|
|
322
412
|
features: featuresSchema.optional(),
|
|
323
413
|
license: licenseSchema,
|
|
324
414
|
ignore: ignoreSchema.optional(),
|
|
325
|
-
agentrulesDir: agentrulesPathSchema.optional(),
|
|
326
415
|
platforms: z.array(platformEntrySchema).min(1, "At least one platform is required")
|
|
327
416
|
}).strict();
|
|
328
417
|
const bundledFileSchema = z.object({
|
|
@@ -342,46 +431,74 @@ const publishVariantInputSchema = z.object({
|
|
|
342
431
|
installMessage: installMessageSchema.optional()
|
|
343
432
|
});
|
|
344
433
|
/**
|
|
345
|
-
* Schema for what clients send to publish a
|
|
434
|
+
* Schema for what clients send to publish a rule.
|
|
346
435
|
*
|
|
347
436
|
* One publish call creates ONE version with ALL platform variants.
|
|
348
437
|
* Version is optional major version. Registry assigns full MAJOR.MINOR.
|
|
349
438
|
*
|
|
350
|
-
* Note: Clients send `name` (e.g., "my-
|
|
351
|
-
* For example, a namespaced slug could be returned as "username/my-
|
|
439
|
+
* Note: Clients send `name` (e.g., "my-rule"), and the registry defines the format of the slug.
|
|
440
|
+
* For example, a namespaced slug could be returned as "username/my-rule"
|
|
352
441
|
*/
|
|
353
|
-
const
|
|
442
|
+
const rulePublishInputSchema = z.object({
|
|
354
443
|
name: nameSchema,
|
|
444
|
+
type: typeSchema,
|
|
355
445
|
title: titleSchema,
|
|
356
|
-
description:
|
|
357
|
-
tags:
|
|
446
|
+
description: normalizedDescriptionSchema,
|
|
447
|
+
tags: normalizedTagsSchema,
|
|
358
448
|
license: licenseSchema,
|
|
359
449
|
features: featuresSchema.optional(),
|
|
360
450
|
variants: z.array(publishVariantInputSchema).min(1, "At least one platform variant is required"),
|
|
361
451
|
version: majorVersionSchema.optional()
|
|
362
452
|
});
|
|
363
453
|
/**
|
|
364
|
-
* Schema for what registries store and return.
|
|
365
|
-
*
|
|
454
|
+
* Schema for what registries store and return for a single platform bundle.
|
|
455
|
+
* This is per-platform (flat structure), stored in R2 and fetched via bundleUrl.
|
|
366
456
|
*/
|
|
367
|
-
const
|
|
368
|
-
name:
|
|
369
|
-
|
|
370
|
-
|
|
457
|
+
const ruleBundleSchema = z.object({
|
|
458
|
+
name: nameSchema,
|
|
459
|
+
type: typeSchema,
|
|
460
|
+
platform: platformIdSchema,
|
|
461
|
+
title: titleSchema,
|
|
462
|
+
description: normalizedDescriptionSchema,
|
|
463
|
+
tags: normalizedTagsSchema,
|
|
464
|
+
license: licenseSchema,
|
|
465
|
+
features: featuresSchema.optional(),
|
|
466
|
+
files: z.array(bundledFileSchema).min(1, "At least one file is required"),
|
|
467
|
+
readmeContent: contentSchema.optional(),
|
|
468
|
+
licenseContent: contentSchema.optional(),
|
|
469
|
+
installMessage: installMessageSchema.optional(),
|
|
371
470
|
slug: z.string().trim().min(1),
|
|
372
471
|
version: versionSchema
|
|
373
472
|
});
|
|
374
473
|
|
|
375
474
|
//#endregion
|
|
376
|
-
//#region src/
|
|
377
|
-
/**
|
|
378
|
-
* Normalize a raw platform entry to the object form.
|
|
379
|
-
*/
|
|
475
|
+
//#region src/rule/types.ts
|
|
476
|
+
/** Normalize a raw platform entry to the object form */
|
|
380
477
|
function normalizePlatformEntry(entry) {
|
|
381
478
|
if (typeof entry === "string") return { platform: entry };
|
|
382
479
|
return entry;
|
|
383
480
|
}
|
|
384
481
|
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/rule/validate.ts
|
|
484
|
+
function validateRule(config) {
|
|
485
|
+
const errors = [];
|
|
486
|
+
const warnings = [];
|
|
487
|
+
if (!config.platforms || config.platforms.length === 0) errors.push("At least one platform is required.");
|
|
488
|
+
for (const entry of config.platforms) if (!isSupportedPlatform(entry.platform)) errors.push(`Unknown platform "${entry.platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
489
|
+
if (config.type) for (const entry of config.platforms) {
|
|
490
|
+
if (!isSupportedPlatform(entry.platform)) continue;
|
|
491
|
+
if (!isValidType(entry.platform, config.type)) errors.push(`Platform "${entry.platform}" does not support type "${config.type}". Rule "${config.name}" cannot target this platform with type "${config.type}".`);
|
|
492
|
+
}
|
|
493
|
+
const hasPlaceholderFeatures = config.features?.some((feature) => feature.trim().startsWith("//"));
|
|
494
|
+
if (hasPlaceholderFeatures) errors.push("Replace placeholder comments in features before publishing.");
|
|
495
|
+
return {
|
|
496
|
+
valid: errors.length === 0,
|
|
497
|
+
errors,
|
|
498
|
+
warnings
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
385
502
|
//#endregion
|
|
386
503
|
//#region src/builder/utils.ts
|
|
387
504
|
function cleanInstallMessage(value) {
|
|
@@ -390,19 +507,19 @@ function cleanInstallMessage(value) {
|
|
|
390
507
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
391
508
|
}
|
|
392
509
|
/**
|
|
393
|
-
* Validate raw
|
|
510
|
+
* Validate raw rule config from JSON.
|
|
394
511
|
* Returns the raw config shape (before normalization).
|
|
395
512
|
*/
|
|
396
|
-
function
|
|
513
|
+
function validateConfig(config, slug) {
|
|
397
514
|
try {
|
|
398
|
-
return
|
|
515
|
+
return ruleConfigSchema.parse(config);
|
|
399
516
|
} catch (e) {
|
|
400
517
|
if (e instanceof ZodError) {
|
|
401
518
|
const messages = e.issues.map((issue) => {
|
|
402
519
|
const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
403
520
|
return `${path}${issue.message}`;
|
|
404
521
|
});
|
|
405
|
-
throw new Error(`Invalid
|
|
522
|
+
throw new Error(`Invalid rule config for ${slug}:\n - ${messages.join("\n - ")}`);
|
|
406
523
|
}
|
|
407
524
|
throw e;
|
|
408
525
|
}
|
|
@@ -425,34 +542,39 @@ async function sha256(data) {
|
|
|
425
542
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
426
543
|
}
|
|
427
544
|
/**
|
|
428
|
-
* Builds a
|
|
545
|
+
* Builds a RulePublishInput from rule input.
|
|
429
546
|
*
|
|
430
|
-
*
|
|
547
|
+
* RuleInput always has platformFiles array (unified format).
|
|
431
548
|
*/
|
|
432
|
-
async function
|
|
433
|
-
const {
|
|
434
|
-
if (!NAME_PATTERN.test(
|
|
435
|
-
const config =
|
|
549
|
+
async function buildPublishInput(options) {
|
|
550
|
+
const { rule, version } = options;
|
|
551
|
+
if (!NAME_PATTERN.test(rule.name)) throw new Error(`Invalid name "${rule.name}". Names must be lowercase kebab-case.`);
|
|
552
|
+
const config = validateConfig(rule.config, rule.name);
|
|
436
553
|
const majorVersion = version ?? config.version;
|
|
437
|
-
const platforms =
|
|
438
|
-
if (platforms.length === 0) throw new Error(`
|
|
439
|
-
for (const entry of platforms) ensureKnownPlatform(entry.platform,
|
|
554
|
+
const platforms = rule.config.platforms;
|
|
555
|
+
if (platforms.length === 0) throw new Error(`Rule ${rule.name} must specify at least one platform.`);
|
|
556
|
+
for (const entry of platforms) ensureKnownPlatform(entry.platform, rule.name);
|
|
557
|
+
const ruleType = config.type;
|
|
558
|
+
if (ruleType) {
|
|
559
|
+
for (const entry of platforms) if (!isValidType(entry.platform, ruleType)) throw new Error(`Platform "${entry.platform}" does not support type "${ruleType}". Rule "${rule.name}" cannot target this platform with type "${ruleType}".`);
|
|
560
|
+
}
|
|
440
561
|
const variants = [];
|
|
441
562
|
for (const entry of platforms) {
|
|
442
|
-
const platformData =
|
|
443
|
-
if (!platformData) throw new Error(`
|
|
444
|
-
if (platformData.files.length === 0) throw new Error(`
|
|
563
|
+
const platformData = rule.platformFiles.find((pf) => pf.platform === entry.platform);
|
|
564
|
+
if (!platformData) throw new Error(`Rule ${rule.name} is missing files for platform "${entry.platform}".`);
|
|
565
|
+
if (platformData.files.length === 0) throw new Error(`Rule ${rule.name} has no files for platform "${entry.platform}".`);
|
|
445
566
|
const files = await createBundledFilesFromInputs(platformData.files);
|
|
446
567
|
variants.push({
|
|
447
568
|
platform: entry.platform,
|
|
448
569
|
files,
|
|
449
|
-
readmeContent: platformData.readmeContent?.trim() ||
|
|
450
|
-
licenseContent: platformData.licenseContent?.trim() ||
|
|
451
|
-
installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(
|
|
570
|
+
readmeContent: platformData.readmeContent?.trim() || rule.readmeContent?.trim() || void 0,
|
|
571
|
+
licenseContent: platformData.licenseContent?.trim() || rule.licenseContent?.trim() || void 0,
|
|
572
|
+
installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(rule.installMessage)
|
|
452
573
|
});
|
|
453
574
|
}
|
|
454
575
|
return {
|
|
455
|
-
name:
|
|
576
|
+
name: rule.name,
|
|
577
|
+
...ruleType && { type: ruleType },
|
|
456
578
|
title: config.title,
|
|
457
579
|
description: config.description,
|
|
458
580
|
tags: config.tags ?? [],
|
|
@@ -466,28 +588,28 @@ async function buildPresetPublishInput(options) {
|
|
|
466
588
|
* Builds a static registry with items and bundles.
|
|
467
589
|
*
|
|
468
590
|
* Uses the same model as dynamic publishing:
|
|
469
|
-
* - Each
|
|
591
|
+
* - Each RuleInput (single or multi-platform) becomes one item
|
|
470
592
|
* - Each platform variant becomes one bundle
|
|
471
593
|
*/
|
|
472
|
-
async function
|
|
594
|
+
async function buildRegistry(options) {
|
|
473
595
|
const bundleBase = normalizeBundleBase(options.bundleBase);
|
|
474
|
-
const
|
|
596
|
+
const rules = [];
|
|
475
597
|
const bundles = [];
|
|
476
|
-
for (const
|
|
477
|
-
const publishInput = await
|
|
598
|
+
for (const ruleInput of options.rules) {
|
|
599
|
+
const publishInput = await buildPublishInput({ rule: ruleInput });
|
|
478
600
|
const slug = publishInput.name;
|
|
479
601
|
const version = `${publishInput.version ?? 1}.0`;
|
|
480
|
-
const
|
|
602
|
+
const ruleVariants = publishInput.variants.map((v) => ({
|
|
481
603
|
platform: v.platform,
|
|
482
604
|
bundleUrl: getBundlePath(bundleBase, slug, v.platform, version),
|
|
483
605
|
fileCount: v.files.length,
|
|
484
|
-
totalSize: v.files.reduce((sum, f) => sum + f.
|
|
606
|
+
totalSize: v.files.reduce((sum, f) => sum + f.size, 0)
|
|
485
607
|
}));
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
kind: "preset",
|
|
608
|
+
ruleVariants.sort((a, b) => a.platform.localeCompare(b.platform));
|
|
609
|
+
const rule = {
|
|
489
610
|
slug,
|
|
490
|
-
name: publishInput.
|
|
611
|
+
name: publishInput.name,
|
|
612
|
+
...publishInput.type && { type: publishInput.type },
|
|
491
613
|
title: publishInput.title,
|
|
492
614
|
description: publishInput.description,
|
|
493
615
|
tags: publishInput.tags,
|
|
@@ -496,13 +618,14 @@ async function buildPresetRegistry(options) {
|
|
|
496
618
|
versions: [{
|
|
497
619
|
version,
|
|
498
620
|
isLatest: true,
|
|
499
|
-
variants:
|
|
621
|
+
variants: ruleVariants
|
|
500
622
|
}]
|
|
501
623
|
};
|
|
502
|
-
|
|
624
|
+
rules.push(rule);
|
|
503
625
|
for (const variant of publishInput.variants) {
|
|
504
626
|
const bundle = {
|
|
505
627
|
name: publishInput.name,
|
|
628
|
+
...publishInput.type && { type: publishInput.type },
|
|
506
629
|
slug,
|
|
507
630
|
platform: variant.platform,
|
|
508
631
|
title: publishInput.title,
|
|
@@ -519,13 +642,13 @@ async function buildPresetRegistry(options) {
|
|
|
519
642
|
bundles.push(bundle);
|
|
520
643
|
}
|
|
521
644
|
}
|
|
522
|
-
|
|
645
|
+
rules.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
523
646
|
bundles.sort((a, b) => {
|
|
524
647
|
if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
|
|
525
648
|
return a.slug.localeCompare(b.slug);
|
|
526
649
|
});
|
|
527
650
|
return {
|
|
528
|
-
|
|
651
|
+
rules,
|
|
529
652
|
bundles
|
|
530
653
|
};
|
|
531
654
|
}
|
|
@@ -550,7 +673,10 @@ function normalizeFilePayload(content) {
|
|
|
550
673
|
return new Uint8Array(content);
|
|
551
674
|
}
|
|
552
675
|
function encodeFilePayload(data, filePath) {
|
|
553
|
-
const decoder = new TextDecoder("utf-8", {
|
|
676
|
+
const decoder = new TextDecoder("utf-8", {
|
|
677
|
+
fatal: true,
|
|
678
|
+
ignoreBOM: false
|
|
679
|
+
});
|
|
554
680
|
try {
|
|
555
681
|
return decoder.decode(data);
|
|
556
682
|
} catch {
|
|
@@ -563,7 +689,7 @@ function normalizeBundleBase(base) {
|
|
|
563
689
|
}
|
|
564
690
|
function getBundlePath(base, slug, platform, version) {
|
|
565
691
|
const prefix = base ? `${base}/` : "";
|
|
566
|
-
return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${
|
|
692
|
+
return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${version}/${platform}.json`;
|
|
567
693
|
}
|
|
568
694
|
function ensureKnownPlatform(platform, slug) {
|
|
569
695
|
if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
@@ -602,40 +728,34 @@ async function sha256Hex(payload) {
|
|
|
602
728
|
//#endregion
|
|
603
729
|
//#region src/constants.ts
|
|
604
730
|
/**
|
|
605
|
-
* Shared constants for agentrules
|
|
731
|
+
* Shared constants for agentrules.
|
|
606
732
|
*/
|
|
607
|
-
/** Filename for
|
|
608
|
-
const
|
|
609
|
-
/**
|
|
610
|
-
const
|
|
611
|
-
/** JSON Schema URL for preset configuration */
|
|
612
|
-
const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
733
|
+
/** Filename for rule configuration */
|
|
734
|
+
const RULE_CONFIG_FILENAME = "agentrules.json";
|
|
735
|
+
/** JSON Schema URL for rule configuration */
|
|
736
|
+
const RULE_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
613
737
|
/** API root path segment */
|
|
614
738
|
const API_PATH = "api";
|
|
615
|
-
/** Default version identifier for latest
|
|
739
|
+
/** Default version identifier for latest rule version */
|
|
616
740
|
const LATEST_VERSION = "latest";
|
|
617
741
|
/**
|
|
618
742
|
* API endpoint paths (relative to registry base URL).
|
|
619
743
|
*
|
|
620
744
|
* Note on slug handling:
|
|
621
|
-
* - Slugs may contain slashes (e.g., "username/my-
|
|
745
|
+
* - Slugs may contain slashes (e.g., "username/my-rule") which flow through as path segments
|
|
622
746
|
* - The client is responsible for validating values before making requests
|
|
623
747
|
*/
|
|
624
748
|
const API_ENDPOINTS = {
|
|
625
|
-
|
|
626
|
-
base: `${API_PATH}/
|
|
627
|
-
|
|
749
|
+
rules: {
|
|
750
|
+
base: `${API_PATH}/rules`,
|
|
751
|
+
get: (slug) => `${API_PATH}/rules/${slug}`,
|
|
752
|
+
unpublish: (slug, version) => `${API_PATH}/rules/${slug}/${version}`
|
|
628
753
|
},
|
|
629
754
|
auth: {
|
|
630
755
|
session: `${API_PATH}/auth/get-session`,
|
|
631
756
|
deviceCode: `${API_PATH}/auth/device/code`,
|
|
632
757
|
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}` }
|
|
758
|
+
}
|
|
639
759
|
};
|
|
640
760
|
|
|
641
761
|
//#endregion
|
|
@@ -656,13 +776,13 @@ async function fetchBundle(bundleUrl) {
|
|
|
656
776
|
* Resolves a slug to get all versions and platform variants.
|
|
657
777
|
*
|
|
658
778
|
* @param baseUrl - Registry base URL
|
|
659
|
-
* @param slug - Content slug (may contain slashes, e.g., "username/my-
|
|
779
|
+
* @param slug - Content slug (may contain slashes, e.g., "username/my-rule")
|
|
660
780
|
* @param version - Optional version filter (server may ignore for static registries)
|
|
661
781
|
* @returns Resolved data, or null if not found
|
|
662
782
|
* @throws Error on network/server errors
|
|
663
783
|
*/
|
|
664
784
|
async function resolveSlug(baseUrl, slug, version) {
|
|
665
|
-
const url = new URL(API_ENDPOINTS.
|
|
785
|
+
const url = new URL(API_ENDPOINTS.rules.get(slug), baseUrl);
|
|
666
786
|
if (version) url.searchParams.set("version", version);
|
|
667
787
|
let response;
|
|
668
788
|
try {
|
|
@@ -680,47 +800,33 @@ async function resolveSlug(baseUrl, slug, version) {
|
|
|
680
800
|
throw new Error(errorMessage);
|
|
681
801
|
}
|
|
682
802
|
const data = await response.json();
|
|
683
|
-
|
|
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
|
-
}
|
|
803
|
+
for (const ver of data.versions) for (const variant of ver.variants) if ("bundleUrl" in variant) variant.bundleUrl = new URL(variant.bundleUrl, baseUrl).toString();
|
|
686
804
|
return data;
|
|
687
805
|
}
|
|
688
806
|
|
|
689
807
|
//#endregion
|
|
690
808
|
//#region src/resolve/schema.ts
|
|
691
809
|
const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
|
|
692
|
-
const
|
|
810
|
+
const ruleVariantBundleSchema = z.object({
|
|
693
811
|
platform: platformIdSchema,
|
|
694
812
|
bundleUrl: z.string().min(1),
|
|
695
813
|
fileCount: z.number().int().nonnegative(),
|
|
696
814
|
totalSize: z.number().int().nonnegative()
|
|
697
815
|
});
|
|
698
|
-
const
|
|
816
|
+
const ruleVariantInlineSchema = z.object({
|
|
699
817
|
platform: platformIdSchema,
|
|
700
818
|
content: z.string().min(1),
|
|
701
819
|
fileCount: z.number().int().nonnegative(),
|
|
702
820
|
totalSize: z.number().int().nonnegative()
|
|
703
821
|
});
|
|
704
|
-
const
|
|
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
|
-
});
|
|
822
|
+
const ruleVariantSchema = z.union([ruleVariantBundleSchema, ruleVariantInlineSchema]);
|
|
716
823
|
const ruleVersionSchema = z.object({
|
|
717
824
|
version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
|
|
718
825
|
isLatest: z.boolean(),
|
|
719
826
|
publishedAt: z.string().datetime().optional(),
|
|
720
827
|
variants: z.array(ruleVariantSchema).min(1)
|
|
721
828
|
});
|
|
722
|
-
const
|
|
723
|
-
kind: z.literal("preset"),
|
|
829
|
+
const resolvedRuleSchema = z.object({
|
|
724
830
|
slug: z.string().min(1),
|
|
725
831
|
name: z.string().min(1),
|
|
726
832
|
title: z.string().min(1),
|
|
@@ -728,157 +834,55 @@ const resolvedPresetSchema = z.object({
|
|
|
728
834
|
tags: z.array(z.string()),
|
|
729
835
|
license: z.string(),
|
|
730
836
|
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
837
|
versions: z.array(ruleVersionSchema).min(1)
|
|
741
838
|
});
|
|
742
|
-
const resolveResponseSchema =
|
|
839
|
+
const resolveResponseSchema = resolvedRuleSchema;
|
|
743
840
|
|
|
744
841
|
//#endregion
|
|
745
842
|
//#region src/resolve/utils.ts
|
|
746
843
|
/**
|
|
747
|
-
* Type guard for
|
|
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
|
|
844
|
+
* Type guard for rule variant with bundleUrl
|
|
760
845
|
*/
|
|
761
846
|
function hasBundle(variant) {
|
|
762
847
|
return "bundleUrl" in variant;
|
|
763
848
|
}
|
|
764
849
|
/**
|
|
765
|
-
* Type guard for
|
|
850
|
+
* Type guard for rule variant with inline content
|
|
766
851
|
*/
|
|
767
852
|
function hasInlineContent(variant) {
|
|
768
853
|
return "content" in variant;
|
|
769
854
|
}
|
|
770
855
|
/**
|
|
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
856
|
* Get the latest version from a resolved rule
|
|
778
857
|
*/
|
|
779
|
-
function
|
|
858
|
+
function getLatestVersion(item) {
|
|
780
859
|
return item.versions.find((v) => v.isLatest);
|
|
781
860
|
}
|
|
782
861
|
/**
|
|
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
862
|
* Get a specific version from a resolved rule
|
|
790
863
|
*/
|
|
791
|
-
function
|
|
864
|
+
function getVersion(item, version) {
|
|
792
865
|
return item.versions.find((v) => v.version === version);
|
|
793
866
|
}
|
|
794
867
|
/**
|
|
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
868
|
* Get a specific platform variant from a rule version
|
|
802
869
|
*/
|
|
803
|
-
function
|
|
870
|
+
function getVariant(version, platform) {
|
|
804
871
|
return version.variants.find((v) => v.platform === platform);
|
|
805
872
|
}
|
|
806
873
|
/**
|
|
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
874
|
* Get all available platforms for a rule version
|
|
814
875
|
*/
|
|
815
|
-
function
|
|
876
|
+
function getPlatforms(version) {
|
|
816
877
|
return version.variants.map((v) => v.platform);
|
|
817
878
|
}
|
|
818
879
|
/**
|
|
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
880
|
* Check if a platform is available in any version of a rule
|
|
826
881
|
*/
|
|
827
|
-
function
|
|
882
|
+
function hasPlatform(item, platform) {
|
|
828
883
|
return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
|
|
829
884
|
}
|
|
830
885
|
|
|
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);
|
|
881
|
-
|
|
882
886
|
//#endregion
|
|
883
887
|
//#region src/utils/diff.ts
|
|
884
888
|
const DEFAULT_CONTEXT = 2;
|
|
@@ -903,4 +907,4 @@ function normalizeBundlePath(value) {
|
|
|
903
907
|
}
|
|
904
908
|
|
|
905
909
|
//#endregion
|
|
906
|
-
export {
|
|
910
|
+
export { API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PLATFORM_ID_TUPLE, PLATFORM_RULE_TYPES, RULE_CONFIG_FILENAME, RULE_SCHEMA_URL, RULE_TYPE_TUPLE, STATIC_BUNDLE_DIR, buildPublishInput, buildRegistry, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeUtf8, fetchBundle, getInstallPath, getLatestVersion, getPlatformConfig, getPlatformFromDir, getPlatforms, getRelativeInstallPath, getTypeConfig, getValidTypes, getVariant, getVersion, hasBundle, hasInlineContent, hasPlatform, inferInstructionPlatformsFromFileName, inferPlatformFromPath, inferTypeFromPath, isLikelyText, isPlatformDir, isSupportedPlatform, isValidType, licenseSchema, nameSchema, normalizeBundlePath, normalizePlatformEntry, normalizePlatformInput, platformIdSchema, publishVariantInputSchema, resolveResponseSchema, resolveSlug, resolvedRuleSchema, ruleBundleSchema, ruleConfigSchema, rulePublishInputSchema, ruleTypeSchema, ruleVariantSchema, ruleVersionSchema, supportsInstallPath, tagSchema, tagsSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateConfig, validateRule, verifyBundledFileChecksum };
|