@agentrules/core 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,140 @@
1
+ # @agentrules/core
2
+
3
+ Shared types and utilities for the AGENT_RULES ecosystem.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @agentrules/core
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Types** - TypeScript definitions for presets, bundles, and registry entries
14
+ - **Validation** - Zod schemas for validating `agentrules.json` configs
15
+ - **Registry Builder** - Transform preset inputs into registry JSON artifacts
16
+ - **Bundle Utilities** - Checksum verification, encoding/decoding helpers
17
+ - **Diff Utilities** - Generate previews for file conflicts
18
+
19
+ ## Usage
20
+
21
+ ### Building Registry Data
22
+
23
+ ```ts
24
+ import { buildRegistryData } from "@agentrules/core";
25
+
26
+ const result = buildRegistryData({
27
+ bundleBase: "/r",
28
+ presets: [
29
+ {
30
+ slug: "my-preset",
31
+ config: {
32
+ name: "my-preset",
33
+ title: "My Preset",
34
+ version: "1.0.0",
35
+ description: "A helpful preset",
36
+ platforms: {
37
+ opencode: { path: ".opencode" },
38
+ },
39
+ },
40
+ platforms: [
41
+ {
42
+ platform: "opencode",
43
+ files: [
44
+ { path: "AGENT_RULES.md", contents: "# Rules\n" },
45
+ { path: "config.json", contents: '{"key": "value"}' },
46
+ ],
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ });
52
+
53
+ // result.entries → array for registry.json
54
+ // result.index → object for registry.index.json
55
+ // result.bundles → per-platform bundle payloads
56
+ ```
57
+
58
+ ### Validating Preset Config
59
+
60
+ ```ts
61
+ import { validatePresetConfig, presetConfigSchema } from "@agentrules/core";
62
+
63
+ // Quick validation (throws on error)
64
+ const config = validatePresetConfig(jsonData, "my-preset");
65
+
66
+ // Zod schema for custom handling
67
+ const result = presetConfigSchema.safeParse(jsonData);
68
+ if (!result.success) {
69
+ console.error(result.error.issues);
70
+ }
71
+ ```
72
+
73
+ ### Fetching from Registry
74
+
75
+ ```ts
76
+ import {
77
+ fetchRegistryIndex,
78
+ fetchRegistryBundle,
79
+ resolveRegistryEntry,
80
+ } from "@agentrules/core";
81
+
82
+ const index = await fetchRegistryIndex("https://agentrules.directory/r/");
83
+ const entry = resolveRegistryEntry(index, "agentic-dev-starter", "opencode");
84
+ const { bundle } = await fetchRegistryBundle(
85
+ "https://agentrules.directory/r/",
86
+ entry.bundlePath
87
+ );
88
+ ```
89
+
90
+ ### Working with Bundles
91
+
92
+ ```ts
93
+ import {
94
+ decodeBundledFile,
95
+ verifyBundledFileChecksum,
96
+ isLikelyText,
97
+ } from "@agentrules/core";
98
+
99
+ for (const file of bundle.files) {
100
+ const data = decodeBundledFile(file);
101
+ await verifyBundledFileChecksum(file, data);
102
+
103
+ if (isLikelyText(data)) {
104
+ console.log(`Text file: ${file.path}`);
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Preset Config Format
110
+
111
+ Presets use `agentrules.json`:
112
+
113
+ ```json
114
+ {
115
+ "$schema": "https://agentrules.directory/schema/agentrules.json",
116
+ "name": "my-preset",
117
+ "title": "My Preset",
118
+ "version": "1.0.0",
119
+ "description": "Description here",
120
+ "author": { "name": "Your Name" },
121
+ "license": "MIT",
122
+ "tags": ["starter", "typescript"],
123
+ "platforms": {
124
+ "opencode": {
125
+ "path": "opencode/files/.opencode",
126
+ "features": ["Feature 1", "Feature 2"],
127
+ "installMessage": "Thanks for installing!"
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ bun install
137
+ bun run build # build with tsdown
138
+ bun run test # run tests
139
+ bun run typecheck # type checking
140
+ ```
package/dist/index.d.ts CHANGED
@@ -1,31 +1,62 @@
1
1
  import { z } from "zod";
2
2
 
3
- //#region src/types.d.ts
4
- type PlatformId = "opencode" | "codex" | "claude" | "cursor";
3
+ //#region src/types/platform.d.ts
4
+
5
+ /**
6
+ * Single source of truth for platform IDs.
7
+ * Add new platforms here - types and config will follow.
8
+ */
9
+ declare const PLATFORM_ID_TUPLE: readonly ["opencode", "codex", "claude", "cursor"];
10
+ /** Union type of supported platform IDs, derived from PLATFORM_ID_TUPLE */
11
+ type PlatformId = (typeof PLATFORM_ID_TUPLE)[number];
12
+ /** List of supported platform IDs as a readonly tuple */
13
+ declare const PLATFORM_IDS: readonly ["opencode", "codex", "claude", "cursor"];
14
+ type PlatformConfig = {
15
+ /** Directory name for project installs (e.g., ".opencode") */
16
+ projectDir: string;
17
+ /** Path for global installs (e.g., "~/.config/opencode") */
18
+ globalDir: string;
19
+ };
20
+ /**
21
+ * Platform-specific configuration.
22
+ * Single source of truth for all platform paths.
23
+ */
24
+ declare const PLATFORMS: Record<PlatformId, PlatformConfig>;
25
+ /**
26
+ * Convention: preset files under this directory map to the platform config directory.
27
+ * e.g., `config/agent.md` → `.opencode/agent.md` (project) or `agent.md` (global)
28
+ */
29
+ declare const CONFIG_DIR_NAME = "config";
30
+ declare function isSupportedPlatform(value: string): value is PlatformId;
31
+ declare function normalizePlatformInput(value: string): PlatformId;
32
+ //#endregion
33
+ //#region src/types/definitions.d.ts
5
34
  type AuthorInfo = {
6
35
  name: string;
7
36
  email?: string;
8
37
  url?: string;
9
38
  };
10
- type PlatformSpecificConfig = {
39
+ type PlatformPresetConfig = {
40
+ path: string;
11
41
  features?: string[];
12
42
  installMessage?: string;
13
43
  };
14
44
  type PresetConfig = {
45
+ $schema?: string;
46
+ name: string;
15
47
  title: string;
16
48
  version: string;
17
49
  description: string;
18
50
  tags?: string[];
19
51
  author?: AuthorInfo;
20
52
  license?: string;
21
- primary?: PlatformId;
22
- platforms: Partial<Record<PlatformId, PlatformSpecificConfig>>;
53
+ platforms: Partial<Record<PlatformId, PlatformPresetConfig>>;
23
54
  };
24
55
  type BundledFile = {
25
56
  path: string;
57
+ /** File size in bytes */
26
58
  size: number;
27
59
  checksum: string;
28
- encoding: "utf-8" | "base64";
29
60
  contents: string;
30
61
  };
31
62
  type RegistryBundle = {
@@ -39,6 +70,7 @@ type RegistryBundle = {
39
70
  license?: string;
40
71
  features?: string[];
41
72
  installMessage?: string;
73
+ readme?: string;
42
74
  files: BundledFile[];
43
75
  };
44
76
  type RegistryEntry = {
@@ -55,73 +87,32 @@ type RegistryEntry = {
55
87
  installMessage?: string;
56
88
  bundlePath: string;
57
89
  fileCount: number;
58
- totalBytes: number;
59
- isPrimary?: boolean;
90
+ /** Total size of all files in bytes */
91
+ totalSize: number;
92
+ /** Whether the preset has a README */
93
+ hasReadme?: boolean;
60
94
  };
61
95
  type RegistryData = {
62
96
  $schema: string;
63
97
  items: RegistryEntry[];
64
98
  };
65
99
  type RegistryIndex = Record<string, RegistryEntry>;
66
- type RegistryIndexItem = RegistryEntry; //#endregion
67
- //#region src/build-utils.d.ts
68
- declare function normalizeBundlePublicBase(value: string): string;
69
- declare function isAbsoluteUrl(value: string): boolean;
70
- declare function cleanInstallMessage(value: unknown): string | undefined;
71
- declare function encodeItemName(slug: string, platform: PlatformId): string;
72
- declare function validatePresetConfig(config: unknown, slug: string): PresetConfig;
73
- declare function collectBundledFiles(files: Record<string, string>): BundledFile[];
74
-
75
- //#endregion
76
- //#region src/bundle.d.ts
77
- declare function decodeBundledFile(file: BundledFile): Uint8Array;
78
- declare function verifyBundledFileChecksum(file: BundledFile, payload: ArrayBuffer | ArrayBufferView): Promise<void>;
79
- declare function isLikelyText(payload: ArrayBuffer | ArrayBufferView): boolean;
80
- declare function toUtf8String(payload: ArrayBuffer | ArrayBufferView): string;
81
-
82
- //#endregion
83
- //#region src/diff.d.ts
84
- type DiffPreviewOptions = {
85
- context?: number;
86
- maxLines?: number;
100
+ type RegistryIndexItem = RegistryEntry;
101
+ type RegistryFileInput = {
102
+ path: string;
103
+ contents: ArrayBuffer | ArrayBufferView | string;
87
104
  };
88
- declare function createDiffPreview(path: string, currentText: string, incomingText: string, options?: DiffPreviewOptions): string;
89
-
90
- //#endregion
91
- //#region src/encoding.d.ts
92
- declare function toPosixPath(pathValue: string): string;
93
- declare function encodeUtf8(value: string): Uint8Array<ArrayBuffer>;
94
- declare function decodeUtf8(payload: ArrayBuffer | ArrayBufferView): string;
95
- declare function toUint8Array(payload: ArrayBuffer | ArrayBufferView): Uint8Array<ArrayBufferLike>;
96
-
97
- //#endregion
98
- //#region src/paths.d.ts
99
- declare function normalizeBundlePath(value: string): string;
100
- declare function normalizePathFragment(value?: string): string | undefined;
101
- declare function maybeStripPrefix(pathInput: string, prefix?: string): string;
102
-
103
- //#endregion
104
- //#region src/platform.d.ts
105
- declare const PLATFORM_IDS: ["opencode", "codex", "claude", "cursor"];
106
- declare function isSupportedPlatform(value: string): value is PlatformId;
107
- declare function normalizePlatformInput(value: string): PlatformId;
108
-
109
- //#endregion
110
- //#region src/preset.d.ts
111
- declare function definePreset(config: PresetConfig): PresetConfig;
112
-
113
- //#endregion
114
- //#region src/registry.d.ts
115
- type FetchRegistryBundleResult = {
116
- bundle: RegistryBundle;
117
- etag: string | null;
105
+ type RegistryPlatformInput = {
106
+ platform: PlatformId;
107
+ files: RegistryFileInput[];
118
108
  };
119
- declare function fetchRegistryIndex(baseUrl: string): Promise<RegistryIndex>;
120
- declare function fetchRegistryBundle(baseUrl: string, bundlePath: string): Promise<FetchRegistryBundleResult>;
121
- declare function resolveRegistryEntry(index: RegistryIndex, input: string, explicitPlatform?: PlatformId): RegistryEntry;
122
-
123
- //#endregion
124
- //#region src/schema.d.ts
109
+ type RegistryPresetInput = {
110
+ slug: string;
111
+ config: PresetConfig;
112
+ platforms: RegistryPlatformInput[];
113
+ readme?: string;
114
+ }; //#endregion
115
+ //#region src/types/schema.d.ts
125
116
  declare const platformIdSchema: z.ZodEnum<{
126
117
  opencode: "opencode";
127
118
  codex: "codex";
@@ -133,7 +124,14 @@ declare const authorSchema: z.ZodObject<{
133
124
  email: z.ZodOptional<z.ZodEmail>;
134
125
  url: z.ZodOptional<z.ZodURL>;
135
126
  }, z.core.$strict>;
136
- declare const platformPresetSchema: z.ZodObject<{
127
+ declare const platformPresetConfigSchema: z.ZodObject<{
128
+ path: z.ZodString;
129
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
130
+ installMessage: z.ZodOptional<z.ZodString>;
131
+ }, z.core.$strict>;
132
+ declare const presetConfigSchema: z.ZodObject<{
133
+ $schema: z.ZodOptional<z.ZodString>;
134
+ name: z.ZodString;
137
135
  title: z.ZodString;
138
136
  version: z.ZodString;
139
137
  description: z.ZodString;
@@ -144,14 +142,18 @@ declare const platformPresetSchema: z.ZodObject<{
144
142
  url: z.ZodOptional<z.ZodURL>;
145
143
  }, z.core.$strict>>;
146
144
  license: z.ZodOptional<z.ZodString>;
147
- features: z.ZodOptional<z.ZodArray<z.ZodString>>;
148
- installMessage: z.ZodOptional<z.ZodString>;
145
+ platforms: z.ZodObject<{
146
+ [x: string]: z.ZodOptional<z.ZodObject<{
147
+ path: z.ZodString;
148
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
149
+ installMessage: z.ZodOptional<z.ZodString>;
150
+ }, z.core.$strict>>;
151
+ }, z.core.$strip>;
149
152
  }, z.core.$strict>;
150
153
  declare const bundledFileSchema: z.ZodObject<{
151
154
  path: z.ZodString;
152
155
  size: z.ZodNumber;
153
156
  checksum: z.ZodString;
154
- encoding: z.ZodUnion<readonly [z.ZodLiteral<"utf-8">, z.ZodLiteral<"base64">]>;
155
157
  contents: z.ZodString;
156
158
  }, z.core.$strip>;
157
159
  declare const registryBundleSchema: z.ZodObject<{
@@ -178,11 +180,12 @@ declare const registryBundleSchema: z.ZodObject<{
178
180
  path: z.ZodString;
179
181
  size: z.ZodNumber;
180
182
  checksum: z.ZodString;
181
- encoding: z.ZodUnion<readonly [z.ZodLiteral<"utf-8">, z.ZodLiteral<"base64">]>;
182
183
  contents: z.ZodString;
183
184
  }, z.core.$strip>>;
184
185
  }, z.core.$strip>;
185
186
  declare const registryEntrySchema: z.ZodObject<{
187
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
188
+ installMessage: z.ZodOptional<z.ZodString>;
186
189
  title: z.ZodString;
187
190
  version: z.ZodString;
188
191
  description: z.ZodString;
@@ -193,8 +196,6 @@ declare const registryEntrySchema: z.ZodObject<{
193
196
  url: z.ZodOptional<z.ZodURL>;
194
197
  }, z.core.$strict>>;
195
198
  license: z.ZodOptional<z.ZodString>;
196
- features: z.ZodOptional<z.ZodArray<z.ZodString>>;
197
- installMessage: z.ZodOptional<z.ZodString>;
198
199
  platform: z.ZodEnum<{
199
200
  opencode: "opencode";
200
201
  codex: "codex";
@@ -205,9 +206,11 @@ declare const registryEntrySchema: z.ZodObject<{
205
206
  name: z.ZodString;
206
207
  bundlePath: z.ZodString;
207
208
  fileCount: z.ZodNumber;
208
- totalBytes: z.ZodNumber;
209
+ totalSize: z.ZodNumber;
209
210
  }, z.core.$strip>;
210
211
  declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
212
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
213
+ installMessage: z.ZodOptional<z.ZodString>;
211
214
  title: z.ZodString;
212
215
  version: z.ZodString;
213
216
  description: z.ZodString;
@@ -218,8 +221,6 @@ declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
218
221
  url: z.ZodOptional<z.ZodURL>;
219
222
  }, z.core.$strict>>;
220
223
  license: z.ZodOptional<z.ZodString>;
221
- features: z.ZodOptional<z.ZodArray<z.ZodString>>;
222
- installMessage: z.ZodOptional<z.ZodString>;
223
224
  platform: z.ZodEnum<{
224
225
  opencode: "opencode";
225
226
  codex: "codex";
@@ -230,8 +231,68 @@ declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
230
231
  name: z.ZodString;
231
232
  bundlePath: z.ZodString;
232
233
  fileCount: z.ZodNumber;
233
- totalBytes: z.ZodNumber;
234
+ totalSize: z.ZodNumber;
234
235
  }, z.core.$strip>>;
235
236
 
236
237
  //#endregion
237
- export { AuthorInfo, BundledFile, DiffPreviewOptions, FetchRegistryBundleResult, PLATFORM_IDS, PlatformId, PlatformSpecificConfig, PresetConfig, RegistryBundle, RegistryData, RegistryEntry, RegistryIndex, RegistryIndexItem, authorSchema, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, definePreset, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, isAbsoluteUrl, isLikelyText, isSupportedPlatform, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
238
+ //#region src/builder/registry.d.ts
239
+ type BuildRegistryDataOptions = {
240
+ presets: RegistryPresetInput[];
241
+ bundleBase?: string;
242
+ };
243
+ type BuildRegistryDataResult = {
244
+ entries: RegistryEntry[];
245
+ index: RegistryIndex;
246
+ bundles: RegistryBundle[];
247
+ };
248
+ declare function buildRegistryData(options: BuildRegistryDataOptions): BuildRegistryDataResult;
249
+
250
+ //#endregion
251
+ //#region src/builder/utils.d.ts
252
+ declare function normalizeBundlePublicBase(value: string): string;
253
+ declare function isAbsoluteUrl(value: string): boolean;
254
+ declare function cleanInstallMessage(value: unknown): string | undefined;
255
+ declare function encodeItemName(slug: string, platform: PlatformId): string;
256
+ declare function validatePresetConfig(config: unknown, slug: string): PresetConfig;
257
+ declare function collectBundledFiles(files: Record<string, string>): BundledFile[];
258
+
259
+ //#endregion
260
+ //#region src/client/bundle.d.ts
261
+ declare function decodeBundledFile(file: BundledFile): Uint8Array;
262
+ declare function verifyBundledFileChecksum(file: BundledFile, payload: ArrayBuffer | ArrayBufferView): Promise<void>;
263
+ declare function isLikelyText(payload: ArrayBuffer | ArrayBufferView): boolean;
264
+ declare function toUtf8String(payload: ArrayBuffer | ArrayBufferView): string;
265
+
266
+ //#endregion
267
+ //#region src/client/registry.d.ts
268
+ type FetchRegistryBundleResult = {
269
+ bundle: RegistryBundle;
270
+ etag: string | null;
271
+ };
272
+ declare function fetchRegistryIndex(baseUrl: string): Promise<RegistryIndex>;
273
+ declare function fetchRegistryBundle(baseUrl: string, bundlePath: string): Promise<FetchRegistryBundleResult>;
274
+ declare function resolveRegistryEntry(index: RegistryIndex, input: string, explicitPlatform?: PlatformId): RegistryEntry;
275
+
276
+ //#endregion
277
+ //#region src/utils/diff.d.ts
278
+ type DiffPreviewOptions = {
279
+ context?: number;
280
+ maxLines?: number;
281
+ };
282
+ declare function createDiffPreview(path: string, currentText: string, incomingText: string, options?: DiffPreviewOptions): string;
283
+
284
+ //#endregion
285
+ //#region src/utils/encoding.d.ts
286
+ declare function toPosixPath(pathValue: string): string;
287
+ declare function encodeUtf8(value: string): Uint8Array<ArrayBuffer>;
288
+ declare function decodeUtf8(payload: ArrayBuffer | ArrayBufferView): string;
289
+ declare function toUint8Array(payload: ArrayBuffer | ArrayBufferView): Uint8Array<ArrayBufferLike>;
290
+
291
+ //#endregion
292
+ //#region src/utils/paths.d.ts
293
+ declare function normalizeBundlePath(value: string): string;
294
+ declare function normalizePathFragment(value?: string): string | undefined;
295
+ declare function maybeStripPrefix(pathInput: string, prefix?: string): string;
296
+
297
+ //#endregion
298
+ export { AuthorInfo, BuildRegistryDataOptions, BuildRegistryDataResult, BundledFile, CONFIG_DIR_NAME, DiffPreviewOptions, FetchRegistryBundleResult, PLATFORMS, PLATFORM_IDS, PlatformId, PlatformPresetConfig, PresetConfig, RegistryBundle, RegistryData, RegistryEntry, RegistryFileInput, RegistryIndex, RegistryIndexItem, RegistryPlatformInput, RegistryPresetInput, authorSchema, buildRegistryData, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, isAbsoluteUrl, isLikelyText, isSupportedPlatform, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetConfigSchema, presetConfigSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
package/dist/index.js CHANGED
@@ -1,7 +1,122 @@
1
- import { createTwoFilesPatch } from "diff";
1
+ import { createHash } from "crypto";
2
2
  import { z } from "zod";
3
+ import { createTwoFilesPatch } from "diff";
3
4
 
4
- //#region src/encoding.ts
5
+ //#region src/types/platform.ts
6
+ /**
7
+ * Single source of truth for platform IDs.
8
+ * Add new platforms here - types and config will follow.
9
+ */
10
+ const PLATFORM_ID_TUPLE = [
11
+ "opencode",
12
+ "codex",
13
+ "claude",
14
+ "cursor"
15
+ ];
16
+ /** List of supported platform IDs as a readonly tuple */
17
+ const PLATFORM_IDS = PLATFORM_ID_TUPLE;
18
+ /**
19
+ * Platform-specific configuration.
20
+ * Single source of truth for all platform paths.
21
+ */
22
+ const PLATFORMS = {
23
+ opencode: {
24
+ projectDir: ".opencode",
25
+ globalDir: "~/.config/opencode"
26
+ },
27
+ codex: {
28
+ projectDir: ".codex",
29
+ globalDir: "~/.codex"
30
+ },
31
+ claude: {
32
+ projectDir: ".claude",
33
+ globalDir: "~/.claude"
34
+ },
35
+ cursor: {
36
+ projectDir: ".cursor",
37
+ globalDir: "~/.cursor"
38
+ }
39
+ };
40
+ /**
41
+ * Convention: preset files under this directory map to the platform config directory.
42
+ * e.g., `config/agent.md` → `.opencode/agent.md` (project) or `agent.md` (global)
43
+ */
44
+ const CONFIG_DIR_NAME = "config";
45
+ function isSupportedPlatform(value) {
46
+ return PLATFORM_ID_TUPLE.includes(value);
47
+ }
48
+ function normalizePlatformInput(value) {
49
+ const normalized = value.toLowerCase();
50
+ if (isSupportedPlatform(normalized)) return normalized;
51
+ throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
52
+ }
53
+
54
+ //#endregion
55
+ //#region src/types/schema.ts
56
+ const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
57
+ const platformIdSchema = z.enum(PLATFORM_IDS);
58
+ const authorSchema = z.object({
59
+ name: z.string().trim().min(1),
60
+ email: z.email().trim().optional(),
61
+ url: z.url().trim().optional()
62
+ }).strict();
63
+ const titleSchema = z.string().trim().min(1).max(120);
64
+ const descriptionSchema = z.string().trim().min(1).max(500);
65
+ const versionSchema = z.string().trim().regex(SEMVER_REGEX, "Version must follow semantic versioning");
66
+ const tagSchema = z.string().trim().min(1).max(48);
67
+ const tagsSchema = z.array(tagSchema).max(10);
68
+ const featureSchema = z.string().trim().min(1).max(160);
69
+ const featuresSchema = z.array(featureSchema).max(10);
70
+ const installMessageSchema = z.string().trim().max(4e3);
71
+ const licenseSchema = z.string().trim().max(80);
72
+ const slugSchema = z.string().trim().min(1).max(64).regex(/^[a-z0-9-]+$/, "Name must be lowercase kebab-case");
73
+ const pathSchema = z.string().trim().min(1);
74
+ const platformPresetConfigSchema = z.object({
75
+ path: pathSchema,
76
+ features: featuresSchema.optional(),
77
+ installMessage: installMessageSchema.optional()
78
+ }).strict();
79
+ const platformsObjectSchema = z.object(Object.fromEntries(PLATFORM_IDS.map((id) => [id, platformPresetConfigSchema.optional()]))).refine((p) => Object.keys(p).length > 0, { message: "At least one platform must be configured" });
80
+ const presetConfigSchema = z.object({
81
+ $schema: z.string().optional(),
82
+ name: slugSchema,
83
+ title: titleSchema,
84
+ version: versionSchema,
85
+ description: descriptionSchema,
86
+ tags: tagsSchema.optional(),
87
+ author: authorSchema.optional(),
88
+ license: licenseSchema.optional(),
89
+ platforms: platformsObjectSchema
90
+ }).strict();
91
+ const bundledFileSchema = z.object({
92
+ path: z.string().min(1),
93
+ size: z.number().int().nonnegative(),
94
+ checksum: z.string().length(64),
95
+ contents: z.string()
96
+ });
97
+ const registryBundleSchema = z.object({
98
+ slug: z.string().trim().min(1),
99
+ platform: platformIdSchema,
100
+ title: titleSchema,
101
+ version: versionSchema,
102
+ description: descriptionSchema,
103
+ tags: tagsSchema,
104
+ author: authorSchema.optional(),
105
+ license: licenseSchema.optional(),
106
+ features: featuresSchema.optional(),
107
+ installMessage: installMessageSchema.optional(),
108
+ files: z.array(bundledFileSchema).min(1)
109
+ });
110
+ const registryEntrySchema = registryBundleSchema.omit({ files: true }).extend({
111
+ name: z.string().trim().min(1),
112
+ bundlePath: z.string().trim().min(1),
113
+ fileCount: z.number().int().nonnegative(),
114
+ totalSize: z.number().int().nonnegative()
115
+ });
116
+ const registryIndexSchema = z.record(z.string(), registryEntrySchema);
117
+
118
+ //#endregion
119
+ //#region src/utils/encoding.ts
5
120
  function toPosixPath(pathValue) {
6
121
  return pathValue.split("\\").join("/");
7
122
  }
@@ -21,7 +136,7 @@ function toUint8Array(payload) {
21
136
  }
22
137
 
23
138
  //#endregion
24
- //#region src/build-utils.ts
139
+ //#region src/builder/utils.ts
25
140
  function normalizeBundlePublicBase(value) {
26
141
  const trimmed = value.trim();
27
142
  if (!trimmed) throw new Error("--bundle-base must be a non-empty string");
@@ -43,8 +158,9 @@ function encodeItemName(slug, platform) {
43
158
  return `${slug}.${platform}`;
44
159
  }
45
160
  function validatePresetConfig(config, slug) {
46
- if (!config || typeof config !== "object") throw new Error(`Invalid preset config export for ${slug}`);
161
+ if (!config || typeof config !== "object") throw new Error(`Invalid preset config for ${slug}`);
47
162
  const preset = config;
163
+ if (!preset.name || typeof preset.name !== "string") throw new Error(`Preset ${slug} is missing a name`);
48
164
  if (!preset.title || typeof preset.title !== "string") throw new Error(`Preset ${slug} is missing a title`);
49
165
  if (!preset.version || typeof preset.version !== "string") throw new Error(`Preset ${slug} is missing a version`);
50
166
  if (!preset.description || typeof preset.description !== "string") throw new Error(`Preset ${slug} is missing a description`);
@@ -59,20 +175,122 @@ function collectBundledFiles(files) {
59
175
  path: normalizedPath,
60
176
  size: payload.length,
61
177
  checksum: "",
62
- encoding: "utf-8",
63
178
  contents
64
179
  };
65
180
  }).sort((a, b) => a.path.localeCompare(b.path));
66
181
  }
67
182
 
68
183
  //#endregion
69
- //#region src/bundle.ts
70
- const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
71
- const BASE64_LOOKUP = Object.fromEntries(Array.from(BASE64_ALPHABET).map((char, index) => [char, index]));
184
+ //#region src/builder/registry.ts
185
+ const NAME_PATTERN = /^[a-z0-9-]+$/;
186
+ function buildRegistryData(options) {
187
+ const bundleBase = normalizeBundlePublicBase(options.bundleBase ?? "/r");
188
+ const entries = [];
189
+ const bundles = [];
190
+ for (const presetInput of options.presets) {
191
+ if (!NAME_PATTERN.test(presetInput.slug)) throw new Error(`Invalid slug "${presetInput.slug}". Slugs must be lowercase kebab-case.`);
192
+ const presetConfig = validatePresetConfig(presetInput.config, presetInput.slug);
193
+ if (presetInput.platforms.length === 0) throw new Error(`Preset ${presetInput.slug} has no platform inputs.`);
194
+ for (const platformInput of presetInput.platforms) {
195
+ const platform = platformInput.platform;
196
+ ensureKnownPlatform(platform, presetInput.slug);
197
+ const platformConfig = presetConfig.platforms?.[platform];
198
+ if (!platformConfig) throw new Error(`Preset ${presetInput.slug} has files for platform "${platform}" but no config entry.`);
199
+ if (platformInput.files.length === 0) throw new Error(`Preset ${presetInput.slug}/${platform} does not include any files.`);
200
+ const files = createBundledFilesFromInputs(platformInput.files);
201
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
202
+ const installMessage = cleanInstallMessage(platformConfig.installMessage);
203
+ const features = platformConfig.features ?? [];
204
+ const readme = presetInput.readme?.trim() || void 0;
205
+ const entry = {
206
+ name: encodeItemName(presetInput.slug, platform),
207
+ slug: presetInput.slug,
208
+ platform,
209
+ title: presetConfig.title,
210
+ version: presetConfig.version,
211
+ description: presetConfig.description,
212
+ tags: presetConfig.tags ?? [],
213
+ author: presetConfig.author,
214
+ license: presetConfig.license,
215
+ features,
216
+ installMessage,
217
+ bundlePath: getBundlePublicPath(bundleBase, presetInput.slug, platform),
218
+ fileCount: files.length,
219
+ totalSize,
220
+ hasReadme: Boolean(readme)
221
+ };
222
+ const bundle = {
223
+ slug: presetInput.slug,
224
+ platform,
225
+ title: presetConfig.title,
226
+ version: presetConfig.version,
227
+ description: presetConfig.description,
228
+ tags: presetConfig.tags ?? [],
229
+ author: presetConfig.author,
230
+ license: presetConfig.license,
231
+ features,
232
+ installMessage,
233
+ readme,
234
+ files
235
+ };
236
+ entries.push(entry);
237
+ bundles.push(bundle);
238
+ }
239
+ }
240
+ sortBySlugAndPlatform(entries);
241
+ sortBySlugAndPlatform(bundles);
242
+ const index = entries.reduce((acc, entry) => {
243
+ acc[entry.name] = entry;
244
+ return acc;
245
+ }, {});
246
+ return {
247
+ entries,
248
+ index,
249
+ bundles
250
+ };
251
+ }
252
+ function createBundledFilesFromInputs(files) {
253
+ return files.map((file) => {
254
+ const payload = normalizeFilePayload(file.contents);
255
+ const contents = encodeFilePayload(payload, file.path);
256
+ const checksum = createHash("sha256").update(payload).digest("hex");
257
+ return {
258
+ path: toPosixPath(file.path),
259
+ size: payload.length,
260
+ checksum,
261
+ contents
262
+ };
263
+ }).sort((a, b) => a.path.localeCompare(b.path));
264
+ }
265
+ function normalizeFilePayload(contents) {
266
+ if (typeof contents === "string") return Buffer.from(contents, "utf8");
267
+ if (contents instanceof ArrayBuffer) return Buffer.from(contents);
268
+ if (ArrayBuffer.isView(contents)) return Buffer.from(contents.buffer, contents.byteOffset, contents.byteLength);
269
+ return Buffer.from(contents);
270
+ }
271
+ function encodeFilePayload(buffer, filePath) {
272
+ const utf8 = buffer.toString("utf8");
273
+ if (!Buffer.from(utf8, "utf8").equals(buffer)) throw new Error(`Binary files are not supported: "${filePath}". Only UTF-8 text files are allowed.`);
274
+ return utf8;
275
+ }
276
+ function getBundlePublicPath(base, slug, platform) {
277
+ const prefix = base === "/" ? "" : base;
278
+ return `${prefix}/${slug}/${platform}.json`;
279
+ }
280
+ function ensureKnownPlatform(platform, slug) {
281
+ if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
282
+ }
283
+ function sortBySlugAndPlatform(items) {
284
+ items.sort((a, b) => {
285
+ if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
286
+ return a.slug.localeCompare(b.slug);
287
+ });
288
+ }
289
+
290
+ //#endregion
291
+ //#region src/client/bundle.ts
72
292
  function decodeBundledFile(file) {
73
- if (file.encoding === "utf-8") return encodeUtf8(file.contents);
74
- if (file.encoding === "base64") return decodeBase64(file.contents);
75
- throw new Error(`Unsupported encoding "${file.encoding}" for ${file.path}.`);
293
+ return encodeUtf8(file.contents);
76
294
  }
77
295
  async function verifyBundledFileChecksum(file, payload) {
78
296
  const bytes = toUint8Array(payload);
@@ -92,39 +310,6 @@ function isLikelyText(payload) {
92
310
  function toUtf8String(payload) {
93
311
  return decodeUtf8(payload);
94
312
  }
95
- function decodeBase64(input) {
96
- const sanitized = input.replace(/[^A-Za-z0-9+/=]/g, "");
97
- if (sanitized.length % 4 !== 0) throw new Error("Invalid base64 payload length.");
98
- let outputLength = sanitized.length / 4 * 3;
99
- if (sanitized.endsWith("==")) outputLength -= 2;
100
- else if (sanitized.endsWith("=")) outputLength -= 1;
101
- const bytes = new Uint8Array(outputLength);
102
- let byteIndex = 0;
103
- for (let i = 0; i < sanitized.length; i += 4) {
104
- const chunk = sanitized.slice(i, i + 4);
105
- const enc1 = decodeBase64Char(chunk[0]);
106
- const enc2 = decodeBase64Char(chunk[1]);
107
- const enc3 = chunk[2] === "=" ? 0 : decodeBase64Char(chunk[2]);
108
- const enc4 = chunk[3] === "=" ? 0 : decodeBase64Char(chunk[3]);
109
- const combined = enc1 * 262144 + enc2 * 4096 + enc3 * 64 + enc4;
110
- bytes[byteIndex] = Math.floor(combined / 65536) % 256;
111
- byteIndex += 1;
112
- if (chunk[2] !== "=") {
113
- bytes[byteIndex] = Math.floor(combined / 256) % 256;
114
- byteIndex += 1;
115
- }
116
- if (chunk[3] !== "=") {
117
- bytes[byteIndex] = combined % 256;
118
- byteIndex += 1;
119
- }
120
- }
121
- return bytes;
122
- }
123
- function decodeBase64Char(char) {
124
- const value = BASE64_LOOKUP[char];
125
- if (value === void 0) throw new Error(`Invalid base64 character "${char}".`);
126
- return value;
127
- }
128
313
  async function sha256Hex(payload) {
129
314
  const crypto = globalThis.crypto;
130
315
  if (!crypto?.subtle) throw new Error("SHA-256 hashing requires Web Crypto API support.");
@@ -133,60 +318,7 @@ async function sha256Hex(payload) {
133
318
  }
134
319
 
135
320
  //#endregion
136
- //#region src/diff.ts
137
- const DEFAULT_CONTEXT = 2;
138
- const DEFAULT_MAX_LINES = 40;
139
- function createDiffPreview(path, currentText, incomingText, options = {}) {
140
- const patch = createTwoFilesPatch(`${path} (current)`, `${path} (incoming)`, currentText, incomingText, void 0, void 0, { context: options.context ?? DEFAULT_CONTEXT });
141
- const lines = patch.trim().split("\n");
142
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
143
- const limited = lines.slice(0, maxLines);
144
- if (lines.length > maxLines) limited.push("...");
145
- return limited.join("\n");
146
- }
147
-
148
- //#endregion
149
- //#region src/paths.ts
150
- function normalizeBundlePath(value) {
151
- return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
152
- }
153
- function normalizePathFragment(value) {
154
- if (!value) return;
155
- const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
156
- return normalized.replace(/\/+$/, "");
157
- }
158
- function maybeStripPrefix(pathInput, prefix) {
159
- if (!prefix) return pathInput;
160
- if (pathInput === prefix) return "";
161
- if (pathInput.startsWith(`${prefix}/`)) return pathInput.slice(prefix.length + 1);
162
- return pathInput;
163
- }
164
-
165
- //#endregion
166
- //#region src/platform.ts
167
- const PLATFORM_IDS = [
168
- "opencode",
169
- "codex",
170
- "claude",
171
- "cursor"
172
- ];
173
- function isSupportedPlatform(value) {
174
- return PLATFORM_IDS.includes(value);
175
- }
176
- function normalizePlatformInput(value) {
177
- const normalized = value.toLowerCase();
178
- if (isSupportedPlatform(normalized)) return normalized;
179
- throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
180
- }
181
-
182
- //#endregion
183
- //#region src/preset.ts
184
- function definePreset(config) {
185
- return config;
186
- }
187
-
188
- //#endregion
189
- //#region src/registry.ts
321
+ //#region src/client/registry.ts
190
322
  async function fetchRegistryIndex(baseUrl) {
191
323
  const indexUrl = new URL("registry.index.json", baseUrl);
192
324
  const response = await fetch(indexUrl);
@@ -222,7 +354,7 @@ function resolveRegistryEntry(index, input, explicitPlatform) {
222
354
  if (!platform) {
223
355
  const parts = normalizedInput.split(".");
224
356
  const maybePlatform = parts.at(-1);
225
- if (maybePlatform && PLATFORM_IDS.includes(maybePlatform)) {
357
+ if (maybePlatform && isSupportedPlatform(maybePlatform)) {
226
358
  platform = maybePlatform;
227
359
  slugHint = parts.slice(0, -1).join(".");
228
360
  }
@@ -242,65 +374,34 @@ function resolveRegistryEntry(index, input, explicitPlatform) {
242
374
  }
243
375
 
244
376
  //#endregion
245
- //#region src/schema.ts
246
- const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
247
- const platformIdSchema = z.enum([
248
- "opencode",
249
- "codex",
250
- "claude",
251
- "cursor"
252
- ]);
253
- const authorSchema = z.object({
254
- name: z.string().trim().min(1),
255
- email: z.email().trim().optional(),
256
- url: z.url().trim().optional()
257
- }).strict();
258
- const titleSchema = z.string().trim().min(1).max(120);
259
- const descriptionSchema = z.string().trim().min(1).max(500);
260
- const versionSchema = z.string().trim().regex(SEMVER_REGEX, "Version must follow semantic versioning");
261
- const tagSchema = z.string().trim().min(1).max(48);
262
- const tagsSchema = z.array(tagSchema).max(10);
263
- const featureSchema = z.string().trim().min(1).max(160);
264
- const featuresSchema = z.array(featureSchema).max(10);
265
- const installMessageSchema = z.string().trim().max(4e3);
266
- const licenseSchema = z.string().trim().max(80);
267
- const platformPresetSchema = z.object({
268
- title: titleSchema,
269
- version: versionSchema,
270
- description: descriptionSchema,
271
- tags: tagsSchema.optional(),
272
- author: authorSchema.optional(),
273
- license: licenseSchema.optional(),
274
- features: featuresSchema.optional(),
275
- installMessage: installMessageSchema.optional()
276
- }).strict();
277
- const bundledFileSchema = z.object({
278
- path: z.string().min(1),
279
- size: z.number().int().nonnegative(),
280
- checksum: z.string().length(64),
281
- encoding: z.union([z.literal("utf-8"), z.literal("base64")]),
282
- contents: z.string()
283
- });
284
- const registryBundleSchema = z.object({
285
- slug: z.string().trim().min(1),
286
- platform: platformIdSchema,
287
- title: titleSchema,
288
- version: versionSchema,
289
- description: descriptionSchema,
290
- tags: tagsSchema,
291
- author: authorSchema.optional(),
292
- license: licenseSchema.optional(),
293
- features: featuresSchema.optional(),
294
- installMessage: installMessageSchema.optional(),
295
- files: z.array(bundledFileSchema).min(1)
296
- });
297
- const registryEntrySchema = registryBundleSchema.omit({ files: true }).extend({
298
- name: z.string().trim().min(1),
299
- bundlePath: z.string().trim().min(1),
300
- fileCount: z.number().int().nonnegative(),
301
- totalBytes: z.number().int().nonnegative()
302
- });
303
- const registryIndexSchema = z.record(z.string(), registryEntrySchema);
377
+ //#region src/utils/diff.ts
378
+ const DEFAULT_CONTEXT = 2;
379
+ const DEFAULT_MAX_LINES = 40;
380
+ function createDiffPreview(path, currentText, incomingText, options = {}) {
381
+ const patch = createTwoFilesPatch(`${path} (current)`, `${path} (incoming)`, currentText, incomingText, void 0, void 0, { context: options.context ?? DEFAULT_CONTEXT });
382
+ const lines = patch.trim().split("\n");
383
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
384
+ const limited = lines.slice(0, maxLines);
385
+ if (lines.length > maxLines) limited.push("...");
386
+ return limited.join("\n");
387
+ }
388
+
389
+ //#endregion
390
+ //#region src/utils/paths.ts
391
+ function normalizeBundlePath(value) {
392
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
393
+ }
394
+ function normalizePathFragment(value) {
395
+ if (!value) return;
396
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
397
+ return normalized.replace(/\/+$/, "");
398
+ }
399
+ function maybeStripPrefix(pathInput, prefix) {
400
+ if (!prefix) return pathInput;
401
+ if (pathInput === prefix) return "";
402
+ if (pathInput.startsWith(`${prefix}/`)) return pathInput.slice(prefix.length + 1);
403
+ return pathInput;
404
+ }
304
405
 
305
406
  //#endregion
306
- export { PLATFORM_IDS, authorSchema, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, definePreset, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, isAbsoluteUrl, isLikelyText, isSupportedPlatform, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
407
+ export { CONFIG_DIR_NAME, PLATFORMS, PLATFORM_IDS, authorSchema, buildRegistryData, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, isAbsoluteUrl, isLikelyText, isSupportedPlatform, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetConfigSchema, presetConfigSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "author": "Brian Cheung <bcheung.dev@gmail.com> (https://github.com/bcheung)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.agentrules.directory",
@@ -51,4 +51,4 @@
51
51
  "tsdown": "^0.9.0",
52
52
  "typescript": "5.7.2"
53
53
  }
54
- }
54
+ }