@agentrules/core 0.1.0 → 0.2.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 +80 -54
- package/dist/index.d.ts +600 -786
- package/dist/index.js +302 -313
- package/package.json +15 -10
package/dist/index.js
CHANGED
|
@@ -11,172 +11,174 @@ 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
|
+
* Platform-specific type tuples for zod schema validation.
|
|
160
|
+
* Must be kept in sync with PLATFORMS types above.
|
|
161
|
+
*/
|
|
136
162
|
const PLATFORM_RULE_TYPES = {
|
|
137
163
|
opencode: [
|
|
138
164
|
"instruction",
|
|
139
|
-
"command",
|
|
140
165
|
"agent",
|
|
166
|
+
"command",
|
|
141
167
|
"tool"
|
|
142
168
|
],
|
|
143
169
|
claude: [
|
|
144
170
|
"instruction",
|
|
171
|
+
"rule",
|
|
145
172
|
"command",
|
|
146
173
|
"skill"
|
|
147
174
|
],
|
|
148
|
-
codex: ["instruction", "command"],
|
|
149
175
|
cursor: [
|
|
150
176
|
"instruction",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
]
|
|
177
|
+
"rule",
|
|
178
|
+
"command"
|
|
179
|
+
],
|
|
180
|
+
codex: ["instruction", "command"]
|
|
154
181
|
};
|
|
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
182
|
|
|
181
183
|
//#endregion
|
|
182
184
|
//#region src/platform/utils.ts
|
|
@@ -189,19 +191,89 @@ function normalizePlatformInput(value) {
|
|
|
189
191
|
throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
|
|
190
192
|
}
|
|
191
193
|
/**
|
|
192
|
-
* Check if a directory name matches a platform's
|
|
193
|
-
* Used to detect if a
|
|
194
|
+
* Check if a directory name matches a platform's platformDir.
|
|
195
|
+
* Used to detect if a rule config is inside a platform directory (in-project mode).
|
|
194
196
|
*/
|
|
195
197
|
function isPlatformDir(dirName) {
|
|
196
|
-
return PLATFORM_IDS.some((id) => PLATFORMS[id].
|
|
198
|
+
return PLATFORM_IDS.some((id) => PLATFORMS[id].platformDir === dirName);
|
|
197
199
|
}
|
|
198
200
|
/**
|
|
199
|
-
* Get the platform ID from a directory name, if it matches a platform's
|
|
201
|
+
* Get the platform ID from a directory name, if it matches a platform's platformDir.
|
|
200
202
|
*/
|
|
201
203
|
function getPlatformFromDir(dirName) {
|
|
202
|
-
for (const id of PLATFORM_IDS) if (PLATFORMS[id].
|
|
204
|
+
for (const id of PLATFORM_IDS) if (PLATFORMS[id].platformDir === dirName) return id;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
function normalizePathForInference(value) {
|
|
208
|
+
return value.replace(/\\/g, "/");
|
|
209
|
+
}
|
|
210
|
+
function getBasename(value) {
|
|
211
|
+
const normalized = normalizePathForInference(value);
|
|
212
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
213
|
+
return segments.at(-1) ?? "";
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Infer the platform from a file path by searching for platformDir segments.
|
|
217
|
+
*
|
|
218
|
+
* Example: "/repo/.claude/commands/foo.md" -> "claude"
|
|
219
|
+
*/
|
|
220
|
+
function inferPlatformFromPath(value) {
|
|
221
|
+
const normalized = normalizePathForInference(value);
|
|
222
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
223
|
+
const matches = PLATFORM_IDS.filter((id) => segments.includes(PLATFORMS[id].platformDir));
|
|
224
|
+
if (matches.length === 1) return matches[0];
|
|
203
225
|
return;
|
|
204
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Return all platforms whose instruction file matches this basename.
|
|
229
|
+
*
|
|
230
|
+
* Example: "CLAUDE.md" -> ["claude"], "AGENTS.md" -> ["opencode", "cursor", "codex"]
|
|
231
|
+
*/
|
|
232
|
+
function inferInstructionPlatformsFromFileName(fileName) {
|
|
233
|
+
const matches = [];
|
|
234
|
+
for (const id of PLATFORM_IDS) {
|
|
235
|
+
const instructionPath = getInstallPath({
|
|
236
|
+
platform: id,
|
|
237
|
+
type: "instruction",
|
|
238
|
+
scope: "project"
|
|
239
|
+
});
|
|
240
|
+
if (instructionPath === fileName) matches.push(id);
|
|
241
|
+
}
|
|
242
|
+
return matches;
|
|
243
|
+
}
|
|
244
|
+
function getProjectTypeDirMap(platform) {
|
|
245
|
+
const map = new Map();
|
|
246
|
+
for (const [type, cfg] of Object.entries(PLATFORMS[platform].types)) {
|
|
247
|
+
const template = cfg.project;
|
|
248
|
+
if (!template) continue;
|
|
249
|
+
if (!template.startsWith("{platformDir}/")) continue;
|
|
250
|
+
const rest = template.slice(14);
|
|
251
|
+
const dir = rest.split("/")[0];
|
|
252
|
+
if (!dir || dir.includes("{")) continue;
|
|
253
|
+
map.set(dir, type);
|
|
254
|
+
}
|
|
255
|
+
return map;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Infer a rule type from a file path for a known platform.
|
|
259
|
+
* Uses PLATFORMS templates as source-of-truth.
|
|
260
|
+
*/
|
|
261
|
+
function inferTypeFromPath(platform, filePath) {
|
|
262
|
+
const base = getBasename(filePath);
|
|
263
|
+
const instructionPath = getInstallPath({
|
|
264
|
+
platform,
|
|
265
|
+
type: "instruction",
|
|
266
|
+
scope: "project"
|
|
267
|
+
});
|
|
268
|
+
if (instructionPath === base) return "instruction";
|
|
269
|
+
const normalized = normalizePathForInference(filePath);
|
|
270
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
271
|
+
const platformDirIndex = segments.lastIndexOf(PLATFORMS[platform].platformDir);
|
|
272
|
+
if (platformDirIndex < 0) return;
|
|
273
|
+
const nextDir = segments[platformDirIndex + 1];
|
|
274
|
+
if (!nextDir) return;
|
|
275
|
+
return getProjectTypeDirMap(platform).get(nextDir);
|
|
276
|
+
}
|
|
205
277
|
|
|
206
278
|
//#endregion
|
|
207
279
|
//#region src/utils/encoding.ts
|
|
@@ -215,7 +287,10 @@ function encodeUtf8(value) {
|
|
|
215
287
|
function decodeUtf8(payload) {
|
|
216
288
|
const bytes = toUint8Array(payload);
|
|
217
289
|
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("utf8");
|
|
218
|
-
return new TextDecoder("utf-8", {
|
|
290
|
+
return new TextDecoder("utf-8", {
|
|
291
|
+
fatal: false,
|
|
292
|
+
ignoreBOM: false
|
|
293
|
+
}).decode(bytes);
|
|
219
294
|
}
|
|
220
295
|
function toUint8Array(payload) {
|
|
221
296
|
if (payload instanceof Uint8Array) return payload;
|
|
@@ -237,13 +312,13 @@ const TAG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-tag)";
|
|
|
237
312
|
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
313
|
/**
|
|
239
314
|
* Schema for tags array.
|
|
240
|
-
* -
|
|
315
|
+
* - 0-10 tags allowed
|
|
241
316
|
*/
|
|
242
|
-
const tagsSchema = z.array(tagSchema).
|
|
317
|
+
const tagsSchema = z.array(tagSchema).max(10, "Maximum 10 tags allowed");
|
|
243
318
|
const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
244
|
-
const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-
|
|
319
|
+
const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-rule)";
|
|
245
320
|
/**
|
|
246
|
-
* Schema for
|
|
321
|
+
* Schema for rule name.
|
|
247
322
|
* - Max 64 characters
|
|
248
323
|
* - Lowercase alphanumeric with hyphens
|
|
249
324
|
*/
|
|
@@ -260,14 +335,11 @@ const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title
|
|
|
260
335
|
const descriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
|
|
261
336
|
|
|
262
337
|
//#endregion
|
|
263
|
-
//#region src/
|
|
338
|
+
//#region src/rule/schema.ts
|
|
264
339
|
const VERSION_REGEX$1 = /^[1-9]\d*\.\d+$/;
|
|
265
340
|
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");
|
|
341
|
+
const normalizedDescriptionSchema = descriptionSchema.optional().default("");
|
|
342
|
+
const normalizedTagsSchema = tagsSchema.optional().default([]);
|
|
271
343
|
const versionSchema = z.string().trim().regex(VERSION_REGEX$1, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
|
|
272
344
|
const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
|
|
273
345
|
const featureSchema = z.string().trim().min(1, "Feature cannot be empty").max(100, "Feature must be 100 characters or less");
|
|
@@ -287,12 +359,6 @@ const pathSchema = z.string().trim().min(1);
|
|
|
287
359
|
const ignorePatternSchema = z.string().trim().min(1, "Ignore pattern cannot be empty");
|
|
288
360
|
const ignoreSchema = z.array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
|
|
289
361
|
/**
|
|
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
362
|
* Platform entry - either a platform ID string or an object with optional path.
|
|
297
363
|
*
|
|
298
364
|
* Examples:
|
|
@@ -305,24 +371,32 @@ const platformEntryObjectSchema = z.object({
|
|
|
305
371
|
}).strict();
|
|
306
372
|
const platformEntrySchema = z.union([platformIdSchema, platformEntryObjectSchema]);
|
|
307
373
|
/**
|
|
308
|
-
*
|
|
374
|
+
* Schema for rule type.
|
|
375
|
+
* Valid types: instruction, rule, command, skill, agent, tool, multi.
|
|
376
|
+
* Optional - defaults to "multi" (freeform file structure).
|
|
377
|
+
* Platform-specific validation happens at publish time.
|
|
378
|
+
*/
|
|
379
|
+
const ruleTypeSchema = z.enum(RULE_TYPE_TUPLE);
|
|
380
|
+
const typeSchema = ruleTypeSchema.optional();
|
|
381
|
+
/**
|
|
382
|
+
* Rule config schema.
|
|
309
383
|
*
|
|
310
384
|
* Uses a unified `platforms` array that accepts either:
|
|
311
385
|
* - Platform ID strings: `["opencode", "claude"]`
|
|
312
386
|
* - Objects with optional path: `[{ platform: "opencode", path: "rules" }]`
|
|
313
387
|
* - Mixed: `["opencode", { platform: "claude", path: "my-claude" }]`
|
|
314
388
|
*/
|
|
315
|
-
const
|
|
389
|
+
const ruleConfigSchema = z.object({
|
|
316
390
|
$schema: z.string().optional(),
|
|
317
391
|
name: nameSchema,
|
|
392
|
+
type: typeSchema,
|
|
318
393
|
title: titleSchema,
|
|
319
394
|
version: majorVersionSchema.optional(),
|
|
320
|
-
description:
|
|
321
|
-
tags:
|
|
395
|
+
description: normalizedDescriptionSchema,
|
|
396
|
+
tags: normalizedTagsSchema,
|
|
322
397
|
features: featuresSchema.optional(),
|
|
323
398
|
license: licenseSchema,
|
|
324
399
|
ignore: ignoreSchema.optional(),
|
|
325
|
-
agentrulesDir: agentrulesPathSchema.optional(),
|
|
326
400
|
platforms: z.array(platformEntrySchema).min(1, "At least one platform is required")
|
|
327
401
|
}).strict();
|
|
328
402
|
const bundledFileSchema = z.object({
|
|
@@ -342,46 +416,74 @@ const publishVariantInputSchema = z.object({
|
|
|
342
416
|
installMessage: installMessageSchema.optional()
|
|
343
417
|
});
|
|
344
418
|
/**
|
|
345
|
-
* Schema for what clients send to publish a
|
|
419
|
+
* Schema for what clients send to publish a rule.
|
|
346
420
|
*
|
|
347
421
|
* One publish call creates ONE version with ALL platform variants.
|
|
348
422
|
* Version is optional major version. Registry assigns full MAJOR.MINOR.
|
|
349
423
|
*
|
|
350
|
-
* Note: Clients send `name` (e.g., "my-
|
|
351
|
-
* For example, a namespaced slug could be returned as "username/my-
|
|
424
|
+
* Note: Clients send `name` (e.g., "my-rule"), and the registry defines the format of the slug.
|
|
425
|
+
* For example, a namespaced slug could be returned as "username/my-rule"
|
|
352
426
|
*/
|
|
353
|
-
const
|
|
427
|
+
const rulePublishInputSchema = z.object({
|
|
354
428
|
name: nameSchema,
|
|
429
|
+
type: typeSchema,
|
|
355
430
|
title: titleSchema,
|
|
356
|
-
description:
|
|
357
|
-
tags:
|
|
431
|
+
description: normalizedDescriptionSchema,
|
|
432
|
+
tags: normalizedTagsSchema,
|
|
358
433
|
license: licenseSchema,
|
|
359
434
|
features: featuresSchema.optional(),
|
|
360
435
|
variants: z.array(publishVariantInputSchema).min(1, "At least one platform variant is required"),
|
|
361
436
|
version: majorVersionSchema.optional()
|
|
362
437
|
});
|
|
363
438
|
/**
|
|
364
|
-
* Schema for what registries store and return.
|
|
365
|
-
*
|
|
439
|
+
* Schema for what registries store and return for a single platform bundle.
|
|
440
|
+
* This is per-platform (flat structure), stored in R2 and fetched via bundleUrl.
|
|
366
441
|
*/
|
|
367
|
-
const
|
|
368
|
-
name:
|
|
369
|
-
|
|
370
|
-
|
|
442
|
+
const ruleBundleSchema = z.object({
|
|
443
|
+
name: nameSchema,
|
|
444
|
+
type: typeSchema,
|
|
445
|
+
platform: platformIdSchema,
|
|
446
|
+
title: titleSchema,
|
|
447
|
+
description: normalizedDescriptionSchema,
|
|
448
|
+
tags: normalizedTagsSchema,
|
|
449
|
+
license: licenseSchema,
|
|
450
|
+
features: featuresSchema.optional(),
|
|
451
|
+
files: z.array(bundledFileSchema).min(1, "At least one file is required"),
|
|
452
|
+
readmeContent: contentSchema.optional(),
|
|
453
|
+
licenseContent: contentSchema.optional(),
|
|
454
|
+
installMessage: installMessageSchema.optional(),
|
|
371
455
|
slug: z.string().trim().min(1),
|
|
372
456
|
version: versionSchema
|
|
373
457
|
});
|
|
374
458
|
|
|
375
459
|
//#endregion
|
|
376
|
-
//#region src/
|
|
377
|
-
/**
|
|
378
|
-
* Normalize a raw platform entry to the object form.
|
|
379
|
-
*/
|
|
460
|
+
//#region src/rule/types.ts
|
|
461
|
+
/** Normalize a raw platform entry to the object form */
|
|
380
462
|
function normalizePlatformEntry(entry) {
|
|
381
463
|
if (typeof entry === "string") return { platform: entry };
|
|
382
464
|
return entry;
|
|
383
465
|
}
|
|
384
466
|
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/rule/validate.ts
|
|
469
|
+
function validateRule(config) {
|
|
470
|
+
const errors = [];
|
|
471
|
+
const warnings = [];
|
|
472
|
+
if (!config.platforms || config.platforms.length === 0) errors.push("At least one platform is required.");
|
|
473
|
+
for (const entry of config.platforms) if (!isSupportedPlatform(entry.platform)) errors.push(`Unknown platform "${entry.platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
474
|
+
if (config.type) for (const entry of config.platforms) {
|
|
475
|
+
if (!isSupportedPlatform(entry.platform)) continue;
|
|
476
|
+
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}".`);
|
|
477
|
+
}
|
|
478
|
+
const hasPlaceholderFeatures = config.features?.some((feature) => feature.trim().startsWith("//"));
|
|
479
|
+
if (hasPlaceholderFeatures) errors.push("Replace placeholder comments in features before publishing.");
|
|
480
|
+
return {
|
|
481
|
+
valid: errors.length === 0,
|
|
482
|
+
errors,
|
|
483
|
+
warnings
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
385
487
|
//#endregion
|
|
386
488
|
//#region src/builder/utils.ts
|
|
387
489
|
function cleanInstallMessage(value) {
|
|
@@ -390,19 +492,19 @@ function cleanInstallMessage(value) {
|
|
|
390
492
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
391
493
|
}
|
|
392
494
|
/**
|
|
393
|
-
* Validate raw
|
|
495
|
+
* Validate raw rule config from JSON.
|
|
394
496
|
* Returns the raw config shape (before normalization).
|
|
395
497
|
*/
|
|
396
|
-
function
|
|
498
|
+
function validateConfig(config, slug) {
|
|
397
499
|
try {
|
|
398
|
-
return
|
|
500
|
+
return ruleConfigSchema.parse(config);
|
|
399
501
|
} catch (e) {
|
|
400
502
|
if (e instanceof ZodError) {
|
|
401
503
|
const messages = e.issues.map((issue) => {
|
|
402
504
|
const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
403
505
|
return `${path}${issue.message}`;
|
|
404
506
|
});
|
|
405
|
-
throw new Error(`Invalid
|
|
507
|
+
throw new Error(`Invalid rule config for ${slug}:\n - ${messages.join("\n - ")}`);
|
|
406
508
|
}
|
|
407
509
|
throw e;
|
|
408
510
|
}
|
|
@@ -425,34 +527,39 @@ async function sha256(data) {
|
|
|
425
527
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
426
528
|
}
|
|
427
529
|
/**
|
|
428
|
-
* Builds a
|
|
530
|
+
* Builds a RulePublishInput from rule input.
|
|
429
531
|
*
|
|
430
|
-
*
|
|
532
|
+
* RuleInput always has platformFiles array (unified format).
|
|
431
533
|
*/
|
|
432
|
-
async function
|
|
433
|
-
const {
|
|
434
|
-
if (!NAME_PATTERN.test(
|
|
435
|
-
const config =
|
|
534
|
+
async function buildPublishInput(options) {
|
|
535
|
+
const { rule, version } = options;
|
|
536
|
+
if (!NAME_PATTERN.test(rule.name)) throw new Error(`Invalid name "${rule.name}". Names must be lowercase kebab-case.`);
|
|
537
|
+
const config = validateConfig(rule.config, rule.name);
|
|
436
538
|
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,
|
|
539
|
+
const platforms = rule.config.platforms;
|
|
540
|
+
if (platforms.length === 0) throw new Error(`Rule ${rule.name} must specify at least one platform.`);
|
|
541
|
+
for (const entry of platforms) ensureKnownPlatform(entry.platform, rule.name);
|
|
542
|
+
const ruleType = config.type;
|
|
543
|
+
if (ruleType) {
|
|
544
|
+
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}".`);
|
|
545
|
+
}
|
|
440
546
|
const variants = [];
|
|
441
547
|
for (const entry of platforms) {
|
|
442
|
-
const platformData =
|
|
443
|
-
if (!platformData) throw new Error(`
|
|
444
|
-
if (platformData.files.length === 0) throw new Error(`
|
|
548
|
+
const platformData = rule.platformFiles.find((pf) => pf.platform === entry.platform);
|
|
549
|
+
if (!platformData) throw new Error(`Rule ${rule.name} is missing files for platform "${entry.platform}".`);
|
|
550
|
+
if (platformData.files.length === 0) throw new Error(`Rule ${rule.name} has no files for platform "${entry.platform}".`);
|
|
445
551
|
const files = await createBundledFilesFromInputs(platformData.files);
|
|
446
552
|
variants.push({
|
|
447
553
|
platform: entry.platform,
|
|
448
554
|
files,
|
|
449
|
-
readmeContent: platformData.readmeContent?.trim() ||
|
|
450
|
-
licenseContent: platformData.licenseContent?.trim() ||
|
|
451
|
-
installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(
|
|
555
|
+
readmeContent: platformData.readmeContent?.trim() || rule.readmeContent?.trim() || void 0,
|
|
556
|
+
licenseContent: platformData.licenseContent?.trim() || rule.licenseContent?.trim() || void 0,
|
|
557
|
+
installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(rule.installMessage)
|
|
452
558
|
});
|
|
453
559
|
}
|
|
454
560
|
return {
|
|
455
|
-
name:
|
|
561
|
+
name: rule.name,
|
|
562
|
+
...ruleType && { type: ruleType },
|
|
456
563
|
title: config.title,
|
|
457
564
|
description: config.description,
|
|
458
565
|
tags: config.tags ?? [],
|
|
@@ -466,28 +573,28 @@ async function buildPresetPublishInput(options) {
|
|
|
466
573
|
* Builds a static registry with items and bundles.
|
|
467
574
|
*
|
|
468
575
|
* Uses the same model as dynamic publishing:
|
|
469
|
-
* - Each
|
|
576
|
+
* - Each RuleInput (single or multi-platform) becomes one item
|
|
470
577
|
* - Each platform variant becomes one bundle
|
|
471
578
|
*/
|
|
472
|
-
async function
|
|
579
|
+
async function buildRegistry(options) {
|
|
473
580
|
const bundleBase = normalizeBundleBase(options.bundleBase);
|
|
474
|
-
const
|
|
581
|
+
const rules = [];
|
|
475
582
|
const bundles = [];
|
|
476
|
-
for (const
|
|
477
|
-
const publishInput = await
|
|
583
|
+
for (const ruleInput of options.rules) {
|
|
584
|
+
const publishInput = await buildPublishInput({ rule: ruleInput });
|
|
478
585
|
const slug = publishInput.name;
|
|
479
586
|
const version = `${publishInput.version ?? 1}.0`;
|
|
480
|
-
const
|
|
587
|
+
const ruleVariants = publishInput.variants.map((v) => ({
|
|
481
588
|
platform: v.platform,
|
|
482
589
|
bundleUrl: getBundlePath(bundleBase, slug, v.platform, version),
|
|
483
590
|
fileCount: v.files.length,
|
|
484
|
-
totalSize: v.files.reduce((sum, f) => sum + f.
|
|
591
|
+
totalSize: v.files.reduce((sum, f) => sum + f.size, 0)
|
|
485
592
|
}));
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
kind: "preset",
|
|
593
|
+
ruleVariants.sort((a, b) => a.platform.localeCompare(b.platform));
|
|
594
|
+
const rule = {
|
|
489
595
|
slug,
|
|
490
|
-
name: publishInput.
|
|
596
|
+
name: publishInput.name,
|
|
597
|
+
...publishInput.type && { type: publishInput.type },
|
|
491
598
|
title: publishInput.title,
|
|
492
599
|
description: publishInput.description,
|
|
493
600
|
tags: publishInput.tags,
|
|
@@ -496,13 +603,14 @@ async function buildPresetRegistry(options) {
|
|
|
496
603
|
versions: [{
|
|
497
604
|
version,
|
|
498
605
|
isLatest: true,
|
|
499
|
-
variants:
|
|
606
|
+
variants: ruleVariants
|
|
500
607
|
}]
|
|
501
608
|
};
|
|
502
|
-
|
|
609
|
+
rules.push(rule);
|
|
503
610
|
for (const variant of publishInput.variants) {
|
|
504
611
|
const bundle = {
|
|
505
612
|
name: publishInput.name,
|
|
613
|
+
...publishInput.type && { type: publishInput.type },
|
|
506
614
|
slug,
|
|
507
615
|
platform: variant.platform,
|
|
508
616
|
title: publishInput.title,
|
|
@@ -519,13 +627,13 @@ async function buildPresetRegistry(options) {
|
|
|
519
627
|
bundles.push(bundle);
|
|
520
628
|
}
|
|
521
629
|
}
|
|
522
|
-
|
|
630
|
+
rules.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
523
631
|
bundles.sort((a, b) => {
|
|
524
632
|
if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
|
|
525
633
|
return a.slug.localeCompare(b.slug);
|
|
526
634
|
});
|
|
527
635
|
return {
|
|
528
|
-
|
|
636
|
+
rules,
|
|
529
637
|
bundles
|
|
530
638
|
};
|
|
531
639
|
}
|
|
@@ -550,7 +658,10 @@ function normalizeFilePayload(content) {
|
|
|
550
658
|
return new Uint8Array(content);
|
|
551
659
|
}
|
|
552
660
|
function encodeFilePayload(data, filePath) {
|
|
553
|
-
const decoder = new TextDecoder("utf-8", {
|
|
661
|
+
const decoder = new TextDecoder("utf-8", {
|
|
662
|
+
fatal: true,
|
|
663
|
+
ignoreBOM: false
|
|
664
|
+
});
|
|
554
665
|
try {
|
|
555
666
|
return decoder.decode(data);
|
|
556
667
|
} catch {
|
|
@@ -602,40 +713,34 @@ async function sha256Hex(payload) {
|
|
|
602
713
|
//#endregion
|
|
603
714
|
//#region src/constants.ts
|
|
604
715
|
/**
|
|
605
|
-
* Shared constants for agentrules
|
|
716
|
+
* Shared constants for agentrules.
|
|
606
717
|
*/
|
|
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";
|
|
718
|
+
/** Filename for rule configuration */
|
|
719
|
+
const RULE_CONFIG_FILENAME = "agentrules.json";
|
|
720
|
+
/** JSON Schema URL for rule configuration */
|
|
721
|
+
const RULE_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
613
722
|
/** API root path segment */
|
|
614
723
|
const API_PATH = "api";
|
|
615
|
-
/** Default version identifier for latest
|
|
724
|
+
/** Default version identifier for latest rule version */
|
|
616
725
|
const LATEST_VERSION = "latest";
|
|
617
726
|
/**
|
|
618
727
|
* API endpoint paths (relative to registry base URL).
|
|
619
728
|
*
|
|
620
729
|
* Note on slug handling:
|
|
621
|
-
* - Slugs may contain slashes (e.g., "username/my-
|
|
730
|
+
* - Slugs may contain slashes (e.g., "username/my-rule") which flow through as path segments
|
|
622
731
|
* - The client is responsible for validating values before making requests
|
|
623
732
|
*/
|
|
624
733
|
const API_ENDPOINTS = {
|
|
625
|
-
|
|
626
|
-
base: `${API_PATH}/
|
|
627
|
-
|
|
734
|
+
rules: {
|
|
735
|
+
base: `${API_PATH}/rules`,
|
|
736
|
+
get: (slug) => `${API_PATH}/rules/${slug}`,
|
|
737
|
+
unpublish: (slug, version) => `${API_PATH}/rules/${slug}/${version}`
|
|
628
738
|
},
|
|
629
739
|
auth: {
|
|
630
740
|
session: `${API_PATH}/auth/get-session`,
|
|
631
741
|
deviceCode: `${API_PATH}/auth/device/code`,
|
|
632
742
|
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}` }
|
|
743
|
+
}
|
|
639
744
|
};
|
|
640
745
|
|
|
641
746
|
//#endregion
|
|
@@ -656,13 +761,13 @@ async function fetchBundle(bundleUrl) {
|
|
|
656
761
|
* Resolves a slug to get all versions and platform variants.
|
|
657
762
|
*
|
|
658
763
|
* @param baseUrl - Registry base URL
|
|
659
|
-
* @param slug - Content slug (may contain slashes, e.g., "username/my-
|
|
764
|
+
* @param slug - Content slug (may contain slashes, e.g., "username/my-rule")
|
|
660
765
|
* @param version - Optional version filter (server may ignore for static registries)
|
|
661
766
|
* @returns Resolved data, or null if not found
|
|
662
767
|
* @throws Error on network/server errors
|
|
663
768
|
*/
|
|
664
769
|
async function resolveSlug(baseUrl, slug, version) {
|
|
665
|
-
const url = new URL(API_ENDPOINTS.
|
|
770
|
+
const url = new URL(API_ENDPOINTS.rules.get(slug), baseUrl);
|
|
666
771
|
if (version) url.searchParams.set("version", version);
|
|
667
772
|
let response;
|
|
668
773
|
try {
|
|
@@ -680,47 +785,33 @@ async function resolveSlug(baseUrl, slug, version) {
|
|
|
680
785
|
throw new Error(errorMessage);
|
|
681
786
|
}
|
|
682
787
|
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
|
-
}
|
|
788
|
+
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
789
|
return data;
|
|
687
790
|
}
|
|
688
791
|
|
|
689
792
|
//#endregion
|
|
690
793
|
//#region src/resolve/schema.ts
|
|
691
794
|
const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
|
|
692
|
-
const
|
|
795
|
+
const ruleVariantBundleSchema = z.object({
|
|
693
796
|
platform: platformIdSchema,
|
|
694
797
|
bundleUrl: z.string().min(1),
|
|
695
798
|
fileCount: z.number().int().nonnegative(),
|
|
696
799
|
totalSize: z.number().int().nonnegative()
|
|
697
800
|
});
|
|
698
|
-
const
|
|
801
|
+
const ruleVariantInlineSchema = z.object({
|
|
699
802
|
platform: platformIdSchema,
|
|
700
803
|
content: z.string().min(1),
|
|
701
804
|
fileCount: z.number().int().nonnegative(),
|
|
702
805
|
totalSize: z.number().int().nonnegative()
|
|
703
806
|
});
|
|
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
|
-
});
|
|
807
|
+
const ruleVariantSchema = z.union([ruleVariantBundleSchema, ruleVariantInlineSchema]);
|
|
716
808
|
const ruleVersionSchema = z.object({
|
|
717
809
|
version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
|
|
718
810
|
isLatest: z.boolean(),
|
|
719
811
|
publishedAt: z.string().datetime().optional(),
|
|
720
812
|
variants: z.array(ruleVariantSchema).min(1)
|
|
721
813
|
});
|
|
722
|
-
const
|
|
723
|
-
kind: z.literal("preset"),
|
|
814
|
+
const resolvedRuleSchema = z.object({
|
|
724
815
|
slug: z.string().min(1),
|
|
725
816
|
name: z.string().min(1),
|
|
726
817
|
title: z.string().min(1),
|
|
@@ -728,157 +819,55 @@ const resolvedPresetSchema = z.object({
|
|
|
728
819
|
tags: z.array(z.string()),
|
|
729
820
|
license: z.string(),
|
|
730
821
|
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
822
|
versions: z.array(ruleVersionSchema).min(1)
|
|
741
823
|
});
|
|
742
|
-
const resolveResponseSchema =
|
|
824
|
+
const resolveResponseSchema = resolvedRuleSchema;
|
|
743
825
|
|
|
744
826
|
//#endregion
|
|
745
827
|
//#region src/resolve/utils.ts
|
|
746
828
|
/**
|
|
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
|
|
829
|
+
* Type guard for rule variant with bundleUrl
|
|
760
830
|
*/
|
|
761
831
|
function hasBundle(variant) {
|
|
762
832
|
return "bundleUrl" in variant;
|
|
763
833
|
}
|
|
764
834
|
/**
|
|
765
|
-
* Type guard for
|
|
835
|
+
* Type guard for rule variant with inline content
|
|
766
836
|
*/
|
|
767
837
|
function hasInlineContent(variant) {
|
|
768
838
|
return "content" in variant;
|
|
769
839
|
}
|
|
770
840
|
/**
|
|
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
841
|
* Get the latest version from a resolved rule
|
|
778
842
|
*/
|
|
779
|
-
function
|
|
843
|
+
function getLatestVersion(item) {
|
|
780
844
|
return item.versions.find((v) => v.isLatest);
|
|
781
845
|
}
|
|
782
846
|
/**
|
|
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
847
|
* Get a specific version from a resolved rule
|
|
790
848
|
*/
|
|
791
|
-
function
|
|
849
|
+
function getVersion(item, version) {
|
|
792
850
|
return item.versions.find((v) => v.version === version);
|
|
793
851
|
}
|
|
794
852
|
/**
|
|
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
853
|
* Get a specific platform variant from a rule version
|
|
802
854
|
*/
|
|
803
|
-
function
|
|
855
|
+
function getVariant(version, platform) {
|
|
804
856
|
return version.variants.find((v) => v.platform === platform);
|
|
805
857
|
}
|
|
806
858
|
/**
|
|
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
859
|
* Get all available platforms for a rule version
|
|
814
860
|
*/
|
|
815
|
-
function
|
|
861
|
+
function getPlatforms(version) {
|
|
816
862
|
return version.variants.map((v) => v.platform);
|
|
817
863
|
}
|
|
818
864
|
/**
|
|
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
865
|
* Check if a platform is available in any version of a rule
|
|
826
866
|
*/
|
|
827
|
-
function
|
|
867
|
+
function hasPlatform(item, platform) {
|
|
828
868
|
return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
|
|
829
869
|
}
|
|
830
870
|
|
|
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
871
|
//#endregion
|
|
883
872
|
//#region src/utils/diff.ts
|
|
884
873
|
const DEFAULT_CONTEXT = 2;
|
|
@@ -903,4 +892,4 @@ function normalizeBundlePath(value) {
|
|
|
903
892
|
}
|
|
904
893
|
|
|
905
894
|
//#endregion
|
|
906
|
-
export {
|
|
895
|
+
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, 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 };
|