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