@agentrules/core 0.0.3 → 0.0.5

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 CHANGED
@@ -23,28 +23,22 @@ npm install @agentrules/core
23
23
  ```ts
24
24
  import { buildRegistryData } from "@agentrules/core";
25
25
 
26
- const result = buildRegistryData({
27
- bundleBase: "/r",
26
+ const result = await buildRegistryData({
28
27
  presets: [
29
28
  {
30
29
  slug: "my-preset",
31
30
  config: {
32
31
  name: "my-preset",
33
32
  title: "My Preset",
34
- version: "1.0.0",
33
+ version: 1,
35
34
  description: "A helpful preset",
36
- platforms: {
37
- opencode: { path: ".opencode" },
38
- },
35
+ license: "MIT",
36
+ platform: "opencode",
37
+ path: ".opencode",
39
38
  },
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
- },
39
+ files: [
40
+ { path: "AGENT_RULES.md", contents: "# Rules\n" },
41
+ { path: "config.json", contents: '{"key": "value"}' },
48
42
  ],
49
43
  },
50
44
  ],
@@ -52,7 +46,7 @@ const result = buildRegistryData({
52
46
 
53
47
  // result.entries → array for registry.json
54
48
  // result.index → object for registry.index.json
55
- // result.bundles → per-platform bundle payloads
49
+ // result.bundles → bundle payloads
56
50
  ```
57
51
 
58
52
  ### Validating Preset Config
@@ -73,18 +67,14 @@ if (!result.success) {
73
67
  ### Fetching from Registry
74
68
 
75
69
  ```ts
76
- import {
77
- fetchRegistryIndex,
78
- fetchRegistryBundle,
79
- resolveRegistryEntry,
80
- } from "@agentrules/core";
70
+ import { resolvePreset, fetchBundle } from "@agentrules/core";
81
71
 
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
72
+ const { entry, bundleUrl } = await resolvePreset(
73
+ "https://agentrules.directory/",
74
+ "agentic-dev-starter",
75
+ "opencode"
87
76
  );
77
+ const bundle = await fetchBundle(bundleUrl);
88
78
  ```
89
79
 
90
80
  ### Working with Bundles
@@ -115,21 +105,22 @@ Presets use `agentrules.json`:
115
105
  "$schema": "https://agentrules.directory/schema/agentrules.json",
116
106
  "name": "my-preset",
117
107
  "title": "My Preset",
118
- "version": "1.0.0",
108
+ "version": 1,
119
109
  "description": "Description here",
120
- "author": { "name": "Your Name" },
121
110
  "license": "MIT",
122
111
  "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
- }
112
+ "features": ["Feature 1", "Feature 2"],
113
+ "platform": "opencode",
114
+ "path": "files"
130
115
  }
131
116
  ```
132
117
 
118
+ ### Versioning
119
+
120
+ Presets use two-segment versioning (`MAJOR.MINOR`):
121
+ - **Major version**: Set by the publisher in config (defaults to 1)
122
+ - **Minor version**: Auto-incremented by the registry on each publish
123
+
133
124
  ## Development
134
125
 
135
126
  ```bash
package/dist/index.d.ts CHANGED
@@ -31,26 +31,18 @@ declare function isSupportedPlatform(value: string): value is PlatformId;
31
31
  declare function normalizePlatformInput(value: string): PlatformId;
32
32
  //#endregion
33
33
  //#region src/types/definitions.d.ts
34
- type AuthorInfo = {
35
- name: string;
36
- email?: string;
37
- url?: string;
38
- };
39
- type PlatformPresetConfig = {
40
- path: string;
41
- features?: string[];
42
- installMessage?: string;
43
- };
44
34
  type PresetConfig = {
45
35
  $schema?: string;
46
36
  name: string;
47
37
  title: string;
48
- version: string;
38
+ version?: number;
49
39
  description: string;
50
40
  tags?: string[];
51
- author?: AuthorInfo;
52
- license?: string;
53
- platforms: Partial<Record<PlatformId, PlatformPresetConfig>>;
41
+ features?: string[];
42
+ license: string;
43
+ platform: PlatformId;
44
+ /** Path to config files. Defaults to platform's projectDir (e.g., ".claude") */
45
+ path?: string;
54
46
  };
55
47
  type BundledFile = {
56
48
  path: string;
@@ -59,19 +51,32 @@ type BundledFile = {
59
51
  checksum: string;
60
52
  contents: string;
61
53
  };
62
- type RegistryBundle = {
54
+ /**
55
+ * What clients send to publish a preset.
56
+ * Version is optional major version. Registry assigns full MAJOR.MINOR.
57
+ */
58
+ type PublishInput = {
63
59
  slug: string;
64
60
  platform: PlatformId;
65
61
  title: string;
66
- version: string;
67
62
  description: string;
68
63
  tags: string[];
69
- author?: AuthorInfo;
70
- license?: string;
64
+ license: string;
65
+ licenseContent?: string;
66
+ readmeContent?: string;
71
67
  features?: string[];
72
68
  installMessage?: string;
73
- readme?: string;
74
69
  files: BundledFile[];
70
+ /** Major version. Defaults to 1 if not specified. */
71
+ version?: number;
72
+ };
73
+ /**
74
+ * What registries store and return.
75
+ * Includes version (required) - full MAJOR.MINOR format assigned by registry.
76
+ */
77
+ type RegistryBundle = Omit<PublishInput, "version"> & {
78
+ /** Full version in MAJOR.MINOR format (e.g., "1.3", "2.1") */
79
+ version: string;
75
80
  };
76
81
  type RegistryEntry = {
77
82
  name: string;
@@ -81,36 +86,25 @@ type RegistryEntry = {
81
86
  version: string;
82
87
  description: string;
83
88
  tags: string[];
84
- author?: AuthorInfo;
85
- license?: string;
89
+ license: string;
86
90
  features?: string[];
87
- installMessage?: string;
88
- bundlePath: string;
91
+ bundleUrl: string;
89
92
  fileCount: number;
90
- /** Total size of all files in bytes */
91
93
  totalSize: number;
92
- /** Whether the preset has a README */
93
- hasReadme?: boolean;
94
- };
95
- type RegistryData = {
96
- $schema: string;
97
- items: RegistryEntry[];
98
94
  };
99
95
  type RegistryIndex = Record<string, RegistryEntry>;
100
- type RegistryIndexItem = RegistryEntry;
101
96
  type RegistryFileInput = {
102
97
  path: string;
103
98
  contents: ArrayBuffer | ArrayBufferView | string;
104
99
  };
105
- type RegistryPlatformInput = {
106
- platform: PlatformId;
107
- files: RegistryFileInput[];
108
- };
109
100
  type RegistryPresetInput = {
110
101
  slug: string;
111
102
  config: PresetConfig;
112
- platforms: RegistryPlatformInput[];
113
- readme?: string;
103
+ files: RegistryFileInput[];
104
+ /** Install message from INSTALL.txt file */
105
+ installMessage?: string;
106
+ readmeContent?: string;
107
+ licenseContent?: string;
114
108
  }; //#endregion
115
109
  //#region src/types/schema.d.ts
116
110
  declare const platformIdSchema: z.ZodEnum<{
@@ -119,36 +113,36 @@ declare const platformIdSchema: z.ZodEnum<{
119
113
  claude: "claude";
120
114
  cursor: "cursor";
121
115
  }>;
122
- declare const authorSchema: z.ZodObject<{
123
- name: z.ZodString;
124
- email: z.ZodOptional<z.ZodEmail>;
125
- url: z.ZodOptional<z.ZodURL>;
126
- }, z.core.$strict>;
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>;
116
+ declare const titleSchema: z.ZodString;
117
+ declare const descriptionSchema: z.ZodString;
118
+ /** Validate a title string and return error message if invalid, undefined if valid */
119
+ declare function validateTitle(value: string): string | undefined;
120
+ /** Validate a description string and return error message if invalid, undefined if valid */
121
+ declare function validateDescription(value: string): string | undefined;
122
+ declare const slugSchema: z.ZodString;
123
+ /** Validate a slug string and return error message if invalid, undefined if valid */
124
+ declare function validateSlug(value: string): string | undefined;
125
+ declare const COMMON_LICENSES: readonly ["MIT", "Apache-2.0", "GPL-3.0-only", "BSD-3-Clause", "ISC", "Unlicense"];
126
+ type CommonLicense = (typeof COMMON_LICENSES)[number];
127
+ declare const licenseSchema: z.ZodString;
128
+ /** Validate a license string and return error message if invalid, undefined if valid */
129
+ declare function validateLicense(value: string): string | undefined;
132
130
  declare const presetConfigSchema: z.ZodObject<{
133
131
  $schema: z.ZodOptional<z.ZodString>;
134
132
  name: z.ZodString;
135
133
  title: z.ZodString;
136
- version: z.ZodString;
134
+ version: z.ZodOptional<z.ZodNumber>;
137
135
  description: z.ZodString;
138
136
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
139
- author: z.ZodOptional<z.ZodObject<{
140
- name: z.ZodString;
141
- email: z.ZodOptional<z.ZodEmail>;
142
- url: z.ZodOptional<z.ZodURL>;
143
- }, z.core.$strict>>;
144
- license: 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>;
137
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ license: z.ZodString;
139
+ platform: z.ZodEnum<{
140
+ opencode: "opencode";
141
+ codex: "codex";
142
+ claude: "claude";
143
+ cursor: "cursor";
144
+ }>;
145
+ path: z.ZodOptional<z.ZodString>;
152
146
  }, z.core.$strict>;
153
147
  declare const bundledFileSchema: z.ZodObject<{
154
148
  path: z.ZodString;
@@ -156,7 +150,11 @@ declare const bundledFileSchema: z.ZodObject<{
156
150
  checksum: z.ZodString;
157
151
  contents: z.ZodString;
158
152
  }, z.core.$strip>;
159
- declare const registryBundleSchema: z.ZodObject<{
153
+ /**
154
+ * Schema for what clients send to publish a preset.
155
+ * Version is optional major version. Registry assigns full MAJOR.MINOR.
156
+ */
157
+ declare const publishInputSchema: z.ZodObject<{
160
158
  slug: z.ZodString;
161
159
  platform: z.ZodEnum<{
162
160
  opencode: "opencode";
@@ -165,15 +163,11 @@ declare const registryBundleSchema: z.ZodObject<{
165
163
  cursor: "cursor";
166
164
  }>;
167
165
  title: z.ZodString;
168
- version: z.ZodString;
169
166
  description: z.ZodString;
170
167
  tags: z.ZodArray<z.ZodString>;
171
- author: z.ZodOptional<z.ZodObject<{
172
- name: z.ZodString;
173
- email: z.ZodOptional<z.ZodEmail>;
174
- url: z.ZodOptional<z.ZodURL>;
175
- }, z.core.$strict>>;
176
- license: z.ZodOptional<z.ZodString>;
168
+ license: z.ZodString;
169
+ licenseContent: z.ZodOptional<z.ZodString>;
170
+ readmeContent: z.ZodOptional<z.ZodString>;
177
171
  features: z.ZodOptional<z.ZodArray<z.ZodString>>;
178
172
  installMessage: z.ZodOptional<z.ZodString>;
179
173
  files: z.ZodArray<z.ZodObject<{
@@ -182,62 +176,105 @@ declare const registryBundleSchema: z.ZodObject<{
182
176
  checksum: z.ZodString;
183
177
  contents: z.ZodString;
184
178
  }, z.core.$strip>>;
179
+ version: z.ZodOptional<z.ZodNumber>;
185
180
  }, z.core.$strip>;
186
- declare const registryEntrySchema: z.ZodObject<{
181
+ /**
182
+ * Schema for what registries store and return.
183
+ * Includes version (required) - full MAJOR.MINOR format assigned by registry.
184
+ */
185
+ declare const registryBundleSchema: z.ZodObject<{
186
+ title: z.ZodString;
187
+ description: z.ZodString;
188
+ license: z.ZodString;
189
+ platform: z.ZodEnum<{
190
+ opencode: "opencode";
191
+ codex: "codex";
192
+ claude: "claude";
193
+ cursor: "cursor";
194
+ }>;
195
+ tags: z.ZodArray<z.ZodString>;
187
196
  features: z.ZodOptional<z.ZodArray<z.ZodString>>;
197
+ slug: z.ZodString;
198
+ licenseContent: z.ZodOptional<z.ZodString>;
199
+ readmeContent: z.ZodOptional<z.ZodString>;
188
200
  installMessage: z.ZodOptional<z.ZodString>;
189
- title: z.ZodString;
201
+ files: z.ZodArray<z.ZodObject<{
202
+ path: z.ZodString;
203
+ size: z.ZodNumber;
204
+ checksum: z.ZodString;
205
+ contents: z.ZodString;
206
+ }, z.core.$strip>>;
190
207
  version: z.ZodString;
208
+ }, z.core.$strip>;
209
+ declare const registryEntrySchema: z.ZodObject<{
210
+ title: z.ZodString;
191
211
  description: z.ZodString;
192
- tags: z.ZodArray<z.ZodString>;
193
- author: z.ZodOptional<z.ZodObject<{
194
- name: z.ZodString;
195
- email: z.ZodOptional<z.ZodEmail>;
196
- url: z.ZodOptional<z.ZodURL>;
197
- }, z.core.$strict>>;
198
- license: z.ZodOptional<z.ZodString>;
212
+ license: z.ZodString;
199
213
  platform: z.ZodEnum<{
200
214
  opencode: "opencode";
201
215
  codex: "codex";
202
216
  claude: "claude";
203
217
  cursor: "cursor";
204
218
  }>;
219
+ version: z.ZodString;
220
+ tags: z.ZodArray<z.ZodString>;
221
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
222
  slug: z.ZodString;
206
223
  name: z.ZodString;
207
- bundlePath: z.ZodString;
224
+ bundleUrl: z.ZodString;
208
225
  fileCount: z.ZodNumber;
209
226
  totalSize: z.ZodNumber;
210
227
  }, z.core.$strip>;
211
228
  declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
212
- features: z.ZodOptional<z.ZodArray<z.ZodString>>;
213
- installMessage: z.ZodOptional<z.ZodString>;
214
229
  title: z.ZodString;
215
- version: z.ZodString;
216
230
  description: z.ZodString;
217
- tags: z.ZodArray<z.ZodString>;
218
- author: z.ZodOptional<z.ZodObject<{
219
- name: z.ZodString;
220
- email: z.ZodOptional<z.ZodEmail>;
221
- url: z.ZodOptional<z.ZodURL>;
222
- }, z.core.$strict>>;
223
- license: z.ZodOptional<z.ZodString>;
231
+ license: z.ZodString;
224
232
  platform: z.ZodEnum<{
225
233
  opencode: "opencode";
226
234
  codex: "codex";
227
235
  claude: "claude";
228
236
  cursor: "cursor";
229
237
  }>;
238
+ version: z.ZodString;
239
+ tags: z.ZodArray<z.ZodString>;
240
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
230
241
  slug: z.ZodString;
231
242
  name: z.ZodString;
232
- bundlePath: z.ZodString;
243
+ bundleUrl: z.ZodString;
233
244
  fileCount: z.ZodNumber;
234
245
  totalSize: z.ZodNumber;
235
246
  }, z.core.$strip>>;
236
247
 
237
248
  //#endregion
238
249
  //#region src/builder/registry.d.ts
250
+ /**
251
+ * Directory name for bundle files in static registry output.
252
+ * Used by `agentrules registry build` to structure output.
253
+ */
254
+ declare const STATIC_BUNDLE_DIR = "r";
255
+ /**
256
+ * Options for building a PublishInput (for CLI publish command).
257
+ */
258
+ type BuildPublishInputOptions = {
259
+ preset: RegistryPresetInput;
260
+ /** Major version. Defaults to 1 if not specified. */
261
+ version?: number;
262
+ };
263
+ /**
264
+ * Builds a PublishInput from preset input.
265
+ * Used by CLI to prepare data for publishing to a registry.
266
+ */
267
+ declare function buildPublishInput(options: BuildPublishInputOptions): Promise<PublishInput>;
268
+ /**
269
+ * Options for building a static registry.
270
+ */
239
271
  type BuildRegistryDataOptions = {
240
272
  presets: RegistryPresetInput[];
273
+ /**
274
+ * Optional base path or URL prefix for bundle locations.
275
+ * Format: {bundleBase}/{STATIC_BUNDLE_DIR}/{slug}/{platform}
276
+ * Default: no prefix (bundleUrl starts with STATIC_BUNDLE_DIR)
277
+ */
241
278
  bundleBase?: string;
242
279
  };
243
280
  type BuildRegistryDataResult = {
@@ -245,16 +282,18 @@ type BuildRegistryDataResult = {
245
282
  index: RegistryIndex;
246
283
  bundles: RegistryBundle[];
247
284
  };
248
- declare function buildRegistryData(options: BuildRegistryDataOptions): BuildRegistryDataResult;
285
+ /**
286
+ * Builds a static registry with entries, index, and bundles.
287
+ * Used for building static registry files (e.g., community-presets).
288
+ * Each preset uses its version from config (default: major 1, minor 0).
289
+ */
290
+ declare function buildRegistryData(options: BuildRegistryDataOptions): Promise<BuildRegistryDataResult>;
249
291
 
250
292
  //#endregion
251
293
  //#region src/builder/utils.d.ts
252
- declare function normalizeBundlePublicBase(value: string): string;
253
- declare function isAbsoluteUrl(value: string): boolean;
254
294
  declare function cleanInstallMessage(value: unknown): string | undefined;
255
295
  declare function encodeItemName(slug: string, platform: PlatformId): string;
256
296
  declare function validatePresetConfig(config: unknown, slug: string): PresetConfig;
257
- declare function collectBundledFiles(files: Record<string, string>): BundledFile[];
258
297
 
259
298
  //#endregion
260
299
  //#region src/client/bundle.d.ts
@@ -265,13 +304,62 @@ declare function toUtf8String(payload: ArrayBuffer | ArrayBufferView): string;
265
304
 
266
305
  //#endregion
267
306
  //#region src/client/registry.d.ts
268
- type FetchRegistryBundleResult = {
269
- bundle: RegistryBundle;
270
- etag: string | null;
307
+ /**
308
+ * Resolved preset with absolute bundle URL
309
+ */
310
+ type ResolvedPreset = {
311
+ entry: RegistryEntry;
312
+ bundleUrl: string;
313
+ };
314
+ /**
315
+ * Resolves a preset from the registry via API endpoint.
316
+ * Returns the entry metadata and the absolute bundle URL.
317
+ *
318
+ * @param baseUrl - Registry base URL
319
+ * @param slug - Preset slug
320
+ * @param platform - Target platform
321
+ * @param version - Version to resolve (defaults to "latest")
322
+ */
323
+ declare function resolvePreset(baseUrl: string, slug: string, platform: PlatformId, version?: string): Promise<ResolvedPreset>;
324
+ /**
325
+ * Fetches a bundle from an absolute URL or resolves it relative to the registry.
326
+ */
327
+ declare function fetchBundle(bundleUrl: string): Promise<RegistryBundle>;
328
+
329
+ //#endregion
330
+ //#region src/constants.d.ts
331
+ /**
332
+ * Shared constants for agentrules presets and registry.
333
+ */
334
+ /** Filename for preset configuration */
335
+ declare const PRESET_CONFIG_FILENAME = "agentrules.json";
336
+ /** JSON Schema URL for preset configuration */
337
+ declare const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
338
+ /** Default version identifier for latest preset version */
339
+ declare const LATEST_VERSION = "latest";
340
+ /**
341
+ * API endpoint paths (relative to registry base URL).
342
+ */
343
+ declare const API_ENDPOINTS: {
344
+ /** Preset endpoints */
345
+ readonly presets: {
346
+ /** Base path for preset operations */
347
+ readonly base: "api/presets";
348
+ /** Get preset entry by version (defaults to "latest") */
349
+ readonly entry: (slug: string, platform: string, version?: string) => string;
350
+ /** Unpublish preset version */
351
+ readonly unpublish: (slug: string, platform: string, version: string) => string;
352
+ };
353
+ /** Auth endpoints */
354
+ readonly auth: {
355
+ /** Get current session */
356
+ readonly session: "api/auth/get-session";
357
+ /** Device authorization code request */
358
+ readonly deviceCode: "api/auth/device/code";
359
+ /** Device token exchange */
360
+ readonly deviceToken: "api/auth/device/token";
361
+ };
271
362
  };
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
363
 
276
364
  //#endregion
277
365
  //#region src/utils/diff.d.ts
@@ -295,4 +383,4 @@ declare function normalizePathFragment(value?: string): string | undefined;
295
383
  declare function maybeStripPrefix(pathInput: string, prefix?: string): string;
296
384
 
297
385
  //#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 };
386
+ export { API_ENDPOINTS, BuildPublishInputOptions, BuildRegistryDataOptions, BuildRegistryDataResult, BundledFile, COMMON_LICENSES, CONFIG_DIR_NAME, CommonLicense, DiffPreviewOptions, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, PlatformId, PresetConfig, PublishInput, RegistryBundle, RegistryEntry, RegistryFileInput, RegistryIndex, RegistryPresetInput, ResolvedPreset, STATIC_BUNDLE_DIR, buildPublishInput, buildRegistryData, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeItemName, encodeUtf8, fetchBundle, isLikelyText, isSupportedPlatform, licenseSchema, maybeStripPrefix, normalizeBundlePath, normalizePathFragment, normalizePlatformInput, platformIdSchema, presetConfigSchema, publishInputSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolvePreset, slugSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateDescription, validateLicense, validatePresetConfig, validateSlug, validateTitle, verifyBundledFileChecksum };
package/dist/index.js CHANGED
@@ -1,7 +1,35 @@
1
- import { createHash } from "crypto";
2
1
  import { z } from "zod";
3
2
  import { createTwoFilesPatch } from "diff";
4
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
+ /** JSON Schema URL for preset configuration */
11
+ const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
12
+ /** API root path segment */
13
+ const API_PATH = "api";
14
+ /** Default version identifier for latest preset version */
15
+ const LATEST_VERSION = "latest";
16
+ /**
17
+ * API endpoint paths (relative to registry base URL).
18
+ */
19
+ const API_ENDPOINTS = {
20
+ presets: {
21
+ base: `${API_PATH}/presets`,
22
+ entry: (slug, platform, version = LATEST_VERSION) => `${API_PATH}/presets/${encodeURIComponent(slug)}/${encodeURIComponent(platform)}/${encodeURIComponent(version)}`,
23
+ unpublish: (slug, platform, version) => `${API_PATH}/presets/${encodeURIComponent(slug)}/${encodeURIComponent(platform)}/${encodeURIComponent(version)}`
24
+ },
25
+ auth: {
26
+ session: `${API_PATH}/auth/get-session`,
27
+ deviceCode: `${API_PATH}/auth/device/code`,
28
+ deviceToken: `${API_PATH}/auth/device/token`
29
+ }
30
+ };
31
+
32
+ //#endregion
5
33
  //#region src/types/platform.ts
6
34
  /**
7
35
  * Single source of truth for platform IDs.
@@ -53,40 +81,71 @@ function normalizePlatformInput(value) {
53
81
 
54
82
  //#endregion
55
83
  //#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-]+)*)?$/;
84
+ const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
57
85
  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
86
  const titleSchema = z.string().trim().min(1).max(120);
64
87
  const descriptionSchema = z.string().trim().min(1).max(500);
65
- const versionSchema = z.string().trim().regex(SEMVER_REGEX, "Version must follow semantic versioning");
88
+ /** Validate a title string and return error message if invalid, undefined if valid */
89
+ function validateTitle(value) {
90
+ const trimmed = value.trim();
91
+ if (!trimmed) return "Title is required";
92
+ if (trimmed.length > 120) return "Title must be 120 characters or less";
93
+ return;
94
+ }
95
+ /** Validate a description string and return error message if invalid, undefined if valid */
96
+ function validateDescription(value) {
97
+ const trimmed = value.trim();
98
+ if (!trimmed) return "Description is required";
99
+ if (trimmed.length > 500) return "Description must be 500 characters or less";
100
+ return;
101
+ }
102
+ const versionSchema = z.string().trim().regex(VERSION_REGEX, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
103
+ const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
66
104
  const tagSchema = z.string().trim().min(1).max(48);
67
105
  const tagsSchema = z.array(tagSchema).max(10);
68
106
  const featureSchema = z.string().trim().min(1).max(160);
69
107
  const featuresSchema = z.array(featureSchema).max(10);
70
108
  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");
109
+ const contentSchema = z.string();
110
+ const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
111
+ const SLUG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
112
+ const slugSchema = z.string().trim().min(1).max(64).regex(SLUG_REGEX, SLUG_ERROR);
113
+ /** Validate a slug string and return error message if invalid, undefined if valid */
114
+ function validateSlug(value) {
115
+ const trimmed = value.trim();
116
+ if (!trimmed) return "Name is required";
117
+ if (trimmed.length > 64) return "Name must be 64 characters or less";
118
+ if (!SLUG_REGEX.test(trimmed)) return SLUG_ERROR;
119
+ return;
120
+ }
121
+ const COMMON_LICENSES = [
122
+ "MIT",
123
+ "Apache-2.0",
124
+ "GPL-3.0-only",
125
+ "BSD-3-Clause",
126
+ "ISC",
127
+ "Unlicense"
128
+ ];
129
+ const licenseSchema = z.string().trim().min(1).max(128);
130
+ /** Validate a license string and return error message if invalid, undefined if valid */
131
+ function validateLicense(value) {
132
+ const trimmed = value.trim();
133
+ if (!trimmed) return "License is required";
134
+ if (trimmed.length > 128) return "License must be 128 characters or less";
135
+ return;
136
+ }
73
137
  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
138
  const presetConfigSchema = z.object({
81
139
  $schema: z.string().optional(),
82
140
  name: slugSchema,
83
141
  title: titleSchema,
84
- version: versionSchema,
142
+ version: majorVersionSchema.optional(),
85
143
  description: descriptionSchema,
86
144
  tags: tagsSchema.optional(),
87
- author: authorSchema.optional(),
88
- license: licenseSchema.optional(),
89
- platforms: platformsObjectSchema
145
+ features: featuresSchema.optional(),
146
+ license: licenseSchema,
147
+ platform: platformIdSchema,
148
+ path: pathSchema.optional()
90
149
  }).strict();
91
150
  const bundledFileSchema = z.object({
92
151
  path: z.string().min(1),
@@ -94,22 +153,37 @@ const bundledFileSchema = z.object({
94
153
  checksum: z.string().length(64),
95
154
  contents: z.string()
96
155
  });
97
- const registryBundleSchema = z.object({
156
+ /**
157
+ * Schema for what clients send to publish a preset.
158
+ * Version is optional major version. Registry assigns full MAJOR.MINOR.
159
+ */
160
+ const publishInputSchema = z.object({
98
161
  slug: z.string().trim().min(1),
99
162
  platform: platformIdSchema,
100
163
  title: titleSchema,
101
- version: versionSchema,
102
164
  description: descriptionSchema,
103
165
  tags: tagsSchema,
104
- author: authorSchema.optional(),
105
- license: licenseSchema.optional(),
166
+ license: licenseSchema,
167
+ licenseContent: contentSchema.optional(),
168
+ readmeContent: contentSchema.optional(),
106
169
  features: featuresSchema.optional(),
107
170
  installMessage: installMessageSchema.optional(),
108
- files: z.array(bundledFileSchema).min(1)
171
+ files: z.array(bundledFileSchema).min(1),
172
+ version: majorVersionSchema.optional()
109
173
  });
110
- const registryEntrySchema = registryBundleSchema.omit({ files: true }).extend({
174
+ /**
175
+ * Schema for what registries store and return.
176
+ * Includes version (required) - full MAJOR.MINOR format assigned by registry.
177
+ */
178
+ const registryBundleSchema = publishInputSchema.omit({ version: true }).extend({ version: versionSchema });
179
+ const registryEntrySchema = registryBundleSchema.omit({
180
+ files: true,
181
+ readmeContent: true,
182
+ licenseContent: true,
183
+ installMessage: true
184
+ }).extend({
111
185
  name: z.string().trim().min(1),
112
- bundlePath: z.string().trim().min(1),
186
+ bundleUrl: z.string().trim().min(1),
113
187
  fileCount: z.number().int().nonnegative(),
114
188
  totalSize: z.number().int().nonnegative()
115
189
  });
@@ -137,18 +211,6 @@ function toUint8Array(payload) {
137
211
 
138
212
  //#endregion
139
213
  //#region src/builder/utils.ts
140
- function normalizeBundlePublicBase(value) {
141
- const trimmed = value.trim();
142
- if (!trimmed) throw new Error("--bundle-base must be a non-empty string");
143
- if (isAbsoluteUrl(trimmed)) return trimmed.replace(/\/+$/, "");
144
- let normalized = trimmed;
145
- if (!normalized.startsWith("/")) normalized = `/${normalized}`;
146
- normalized = normalized.replace(/\/+$/, "");
147
- return normalized === "" ? "/" : normalized;
148
- }
149
- function isAbsoluteUrl(value) {
150
- return /^[a-zA-Z][a-zA-Z\d+-.]*:/.test(value);
151
- }
152
214
  function cleanInstallMessage(value) {
153
215
  if (typeof value !== "string") return;
154
216
  const trimmed = value.trim();
@@ -162,80 +224,115 @@ function validatePresetConfig(config, slug) {
162
224
  const preset = config;
163
225
  if (!preset.name || typeof preset.name !== "string") throw new Error(`Preset ${slug} is missing a name`);
164
226
  if (!preset.title || typeof preset.title !== "string") throw new Error(`Preset ${slug} is missing a title`);
165
- if (!preset.version || typeof preset.version !== "string") throw new Error(`Preset ${slug} is missing a version`);
227
+ if (preset.version !== void 0 && typeof preset.version !== "number") throw new Error(`Preset ${slug} has invalid version (must be a positive integer or omitted)`);
166
228
  if (!preset.description || typeof preset.description !== "string") throw new Error(`Preset ${slug} is missing a description`);
167
- if (!preset.platforms || typeof preset.platforms !== "object") throw new Error(`Preset ${slug} is missing platforms map`);
229
+ if (!preset.license || typeof preset.license !== "string") throw new Error(`Preset ${slug} is missing a license (SPDX identifier required)`);
230
+ if (!preset.platform || typeof preset.platform !== "string") throw new Error(`Preset ${slug} is missing a platform`);
231
+ if (!PLATFORM_IDS.includes(preset.platform)) throw new Error(`Preset ${slug} has unknown platform: ${preset.platform}. Supported platforms: ${PLATFORM_IDS.join(", ")}`);
168
232
  return preset;
169
233
  }
170
- function collectBundledFiles(files) {
171
- return Object.entries(files).map(([path, contents]) => {
172
- const normalizedPath = toPosixPath(path);
173
- const payload = encodeUtf8(contents);
174
- return {
175
- path: normalizedPath,
176
- size: payload.length,
177
- checksum: "",
178
- contents
179
- };
180
- }).sort((a, b) => a.path.localeCompare(b.path));
181
- }
182
234
 
183
235
  //#endregion
184
236
  //#region src/builder/registry.ts
185
237
  const NAME_PATTERN = /^[a-z0-9-]+$/;
186
- function buildRegistryData(options) {
187
- const bundleBase = normalizeBundlePublicBase(options.bundleBase ?? "/r");
238
+ /**
239
+ * Directory name for bundle files in static registry output.
240
+ * Used by `agentrules registry build` to structure output.
241
+ */
242
+ const STATIC_BUNDLE_DIR = "r";
243
+ /**
244
+ * Compute SHA-256 hash using Web Crypto API (works in browser and Node.js 15+)
245
+ */
246
+ async function sha256(data) {
247
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
248
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
249
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
250
+ }
251
+ /**
252
+ * Builds a PublishInput from preset input.
253
+ * Used by CLI to prepare data for publishing to a registry.
254
+ */
255
+ async function buildPublishInput(options) {
256
+ const { preset: presetInput, version } = options;
257
+ if (!NAME_PATTERN.test(presetInput.slug)) throw new Error(`Invalid slug "${presetInput.slug}". Slugs must be lowercase kebab-case.`);
258
+ const presetConfig = validatePresetConfig(presetInput.config, presetInput.slug);
259
+ const platform = presetConfig.platform;
260
+ ensureKnownPlatform(platform, presetInput.slug);
261
+ if (presetInput.files.length === 0) throw new Error(`Preset ${presetInput.slug} does not include any files.`);
262
+ const files = await createBundledFilesFromInputs(presetInput.files);
263
+ const installMessage = cleanInstallMessage(presetInput.installMessage);
264
+ const features = presetConfig.features ?? [];
265
+ const readmeContent = presetInput.readmeContent?.trim() || void 0;
266
+ const licenseContent = presetInput.licenseContent?.trim() || void 0;
267
+ const majorVersion = version ?? presetConfig.version;
268
+ return {
269
+ slug: presetInput.slug,
270
+ platform,
271
+ title: presetConfig.title,
272
+ description: presetConfig.description,
273
+ tags: presetConfig.tags ?? [],
274
+ license: presetConfig.license,
275
+ licenseContent,
276
+ readmeContent,
277
+ features,
278
+ installMessage,
279
+ files,
280
+ ...majorVersion !== void 0 && { version: majorVersion }
281
+ };
282
+ }
283
+ /**
284
+ * Builds a static registry with entries, index, and bundles.
285
+ * Used for building static registry files (e.g., community-presets).
286
+ * Each preset uses its version from config (default: major 1, minor 0).
287
+ */
288
+ async function buildRegistryData(options) {
289
+ const bundleBase = normalizeBundleBase(options.bundleBase);
188
290
  const entries = [];
189
291
  const bundles = [];
190
292
  for (const presetInput of options.presets) {
191
293
  if (!NAME_PATTERN.test(presetInput.slug)) throw new Error(`Invalid slug "${presetInput.slug}". Slugs must be lowercase kebab-case.`);
192
294
  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
- }
295
+ const platform = presetConfig.platform;
296
+ ensureKnownPlatform(platform, presetInput.slug);
297
+ if (presetInput.files.length === 0) throw new Error(`Preset ${presetInput.slug} does not include any files.`);
298
+ const files = await createBundledFilesFromInputs(presetInput.files);
299
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
300
+ const installMessage = cleanInstallMessage(presetInput.installMessage);
301
+ const features = presetConfig.features ?? [];
302
+ const readmeContent = presetInput.readmeContent?.trim() || void 0;
303
+ const licenseContent = presetInput.licenseContent?.trim() || void 0;
304
+ const majorVersion = presetConfig.version ?? 1;
305
+ const version = `${majorVersion}.0`;
306
+ const entry = {
307
+ name: encodeItemName(presetInput.slug, platform),
308
+ slug: presetInput.slug,
309
+ platform,
310
+ title: presetConfig.title,
311
+ version,
312
+ description: presetConfig.description,
313
+ tags: presetConfig.tags ?? [],
314
+ license: presetConfig.license,
315
+ features,
316
+ bundleUrl: getBundlePath(bundleBase, presetInput.slug, platform, version),
317
+ fileCount: files.length,
318
+ totalSize
319
+ };
320
+ entries.push(entry);
321
+ const bundle = {
322
+ slug: presetInput.slug,
323
+ platform,
324
+ title: presetConfig.title,
325
+ version,
326
+ description: presetConfig.description,
327
+ tags: presetConfig.tags ?? [],
328
+ license: presetConfig.license,
329
+ licenseContent,
330
+ readmeContent,
331
+ features,
332
+ installMessage,
333
+ files
334
+ };
335
+ bundles.push(bundle);
239
336
  }
240
337
  sortBySlugAndPlatform(entries);
241
338
  sortBySlugAndPlatform(bundles);
@@ -249,33 +346,49 @@ function buildRegistryData(options) {
249
346
  bundles
250
347
  };
251
348
  }
252
- function createBundledFilesFromInputs(files) {
253
- return files.map((file) => {
349
+ async function createBundledFilesFromInputs(files) {
350
+ const results = await Promise.all(files.map(async (file) => {
254
351
  const payload = normalizeFilePayload(file.contents);
255
352
  const contents = encodeFilePayload(payload, file.path);
256
- const checksum = createHash("sha256").update(payload).digest("hex");
353
+ const checksum = await sha256(payload);
257
354
  return {
258
355
  path: toPosixPath(file.path),
259
356
  size: payload.length,
260
357
  checksum,
261
358
  contents
262
359
  };
263
- }).sort((a, b) => a.path.localeCompare(b.path));
360
+ }));
361
+ return results.sort((a, b) => a.path.localeCompare(b.path));
264
362
  }
265
363
  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);
364
+ if (typeof contents === "string") return new TextEncoder().encode(contents);
365
+ if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
366
+ if (ArrayBuffer.isView(contents)) return new Uint8Array(contents.buffer, contents.byteOffset, contents.byteLength);
367
+ return new Uint8Array(contents);
270
368
  }
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;
369
+ function encodeFilePayload(data, filePath) {
370
+ const decoder = new TextDecoder("utf-8", { fatal: true });
371
+ try {
372
+ return decoder.decode(data);
373
+ } catch {
374
+ throw new Error(`Binary files are not supported: "${filePath}". Only UTF-8 text files are allowed.`);
375
+ }
275
376
  }
276
- function getBundlePublicPath(base, slug, platform) {
277
- const prefix = base === "/" ? "" : base;
278
- return `${prefix}/${slug}/${platform}.json`;
377
+ /**
378
+ * Normalize bundle base by removing trailing slashes.
379
+ * Returns empty string if base is undefined/empty (use default relative path).
380
+ */
381
+ function normalizeBundleBase(base) {
382
+ if (!base) return "";
383
+ return base.replace(/\/+$/, "");
384
+ }
385
+ /**
386
+ * Returns the bundle URL/path for a preset.
387
+ * Format: {base}/{STATIC_BUNDLE_DIR}/{slug}/{platform}/{version}
388
+ */
389
+ function getBundlePath(base, slug, platform, version = LATEST_VERSION) {
390
+ const prefix = base ? `${base}/` : "";
391
+ return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${platform}/${version}`;
279
392
  }
280
393
  function ensureKnownPlatform(platform, slug) {
281
394
  if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
@@ -311,67 +424,56 @@ function toUtf8String(payload) {
311
424
  return decodeUtf8(payload);
312
425
  }
313
426
  async function sha256Hex(payload) {
314
- const crypto = globalThis.crypto;
315
- if (!crypto?.subtle) throw new Error("SHA-256 hashing requires Web Crypto API support.");
316
- const digest = await crypto.subtle.digest("SHA-256", payload);
427
+ const crypto$1 = globalThis.crypto;
428
+ if (!crypto$1?.subtle) throw new Error("SHA-256 hashing requires Web Crypto API support.");
429
+ const digest = await crypto$1.subtle.digest("SHA-256", payload);
317
430
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
318
431
  }
319
432
 
320
433
  //#endregion
321
434
  //#region src/client/registry.ts
322
- async function fetchRegistryIndex(baseUrl) {
323
- const indexUrl = new URL("registry.index.json", baseUrl);
324
- const response = await fetch(indexUrl);
325
- if (!response.ok) throw new Error(`Failed to load registry index (${response.status} ${response.statusText}).`);
435
+ /**
436
+ * Resolves a preset from the registry via API endpoint.
437
+ * Returns the entry metadata and the absolute bundle URL.
438
+ *
439
+ * @param baseUrl - Registry base URL
440
+ * @param slug - Preset slug
441
+ * @param platform - Target platform
442
+ * @param version - Version to resolve (defaults to "latest")
443
+ */
444
+ async function resolvePreset(baseUrl, slug, platform, version = LATEST_VERSION) {
445
+ const apiUrl = new URL(API_ENDPOINTS.presets.entry(slug, platform, version), baseUrl);
446
+ const response = await fetch(apiUrl);
447
+ if (!response.ok) {
448
+ if (response.status === 404) {
449
+ const versionInfo = version === LATEST_VERSION ? "" : ` version "${version}"`;
450
+ throw new Error(`Preset "${slug}"${versionInfo} for platform "${platform}" was not found in the registry.`);
451
+ }
452
+ throw new Error(`Failed to resolve preset (${response.status} ${response.statusText}).`);
453
+ }
326
454
  try {
327
- return await response.json();
455
+ const entry = await response.json();
456
+ const resolvedBundleUrl = new URL(entry.bundleUrl, baseUrl).toString();
457
+ return {
458
+ entry,
459
+ bundleUrl: resolvedBundleUrl
460
+ };
328
461
  } catch (error) {
329
- throw new Error(`Unable to parse registry index JSON: ${error.message}`);
462
+ throw new Error(`Unable to parse preset response: ${error.message}`);
330
463
  }
331
464
  }
332
- async function fetchRegistryBundle(baseUrl, bundlePath) {
333
- const bundleUrl = new URL(bundlePath, baseUrl);
465
+ /**
466
+ * Fetches a bundle from an absolute URL or resolves it relative to the registry.
467
+ */
468
+ async function fetchBundle(bundleUrl) {
334
469
  const response = await fetch(bundleUrl);
335
470
  if (!response.ok) throw new Error(`Failed to download bundle (${response.status} ${response.statusText}).`);
336
471
  try {
337
- const bundle = await response.json();
338
- return {
339
- bundle,
340
- etag: response.headers.get("etag")
341
- };
472
+ return await response.json();
342
473
  } catch (error) {
343
474
  throw new Error(`Unable to parse bundle JSON: ${error.message}`);
344
475
  }
345
476
  }
346
- function resolveRegistryEntry(index, input, explicitPlatform) {
347
- const map = new Map();
348
- for (const [key, value] of Object.entries(index)) map.set(key.toLowerCase(), value);
349
- const normalizedInput = input.toLowerCase();
350
- const direct = map.get(normalizedInput);
351
- if (direct) return direct;
352
- let slugHint = normalizedInput;
353
- let platform = explicitPlatform;
354
- if (!platform) {
355
- const parts = normalizedInput.split(".");
356
- const maybePlatform = parts.at(-1);
357
- if (maybePlatform && isSupportedPlatform(maybePlatform)) {
358
- platform = maybePlatform;
359
- slugHint = parts.slice(0, -1).join(".");
360
- }
361
- }
362
- const matches = Object.values(index).filter((entry) => entry.slug.toLowerCase() === slugHint);
363
- if (platform) {
364
- const match = matches.find((entry) => entry.platform === platform);
365
- if (match) return match;
366
- throw new Error(`Preset "${input}" is not available for platform "${platform}".`);
367
- }
368
- if (matches.length === 1) return matches[0];
369
- if (matches.length > 1) {
370
- const platforms = matches.map((entry) => entry.platform).join(", ");
371
- throw new Error(`Preset "${input}" is available for multiple platforms (${platforms}). Use --platform to pick one or specify <slug>.<platform>.`);
372
- }
373
- throw new Error(`Preset "${input}" was not found in the active registry.`);
374
- }
375
477
 
376
478
  //#endregion
377
479
  //#region src/utils/diff.ts
@@ -404,4 +506,4 @@ function maybeStripPrefix(pathInput, prefix) {
404
506
  }
405
507
 
406
508
  //#endregion
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 };
509
+ export { API_ENDPOINTS, COMMON_LICENSES, CONFIG_DIR_NAME, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPublishInput, buildRegistryData, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeItemName, encodeUtf8, fetchBundle, isLikelyText, isSupportedPlatform, licenseSchema, maybeStripPrefix, normalizeBundlePath, normalizePathFragment, normalizePlatformInput, platformIdSchema, presetConfigSchema, publishInputSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolvePreset, slugSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateDescription, validateLicense, validatePresetConfig, validateSlug, validateTitle, verifyBundledFileChecksum };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@agentrules/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "author": "Brian Cheung <bcheung.dev@gmail.com> (https://github.com/bcheung)",
5
5
  "license": "MIT",
6
- "homepage": "https://docs.agentrules.directory",
6
+ "homepage": "https://agentrules.directory",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/agentrules-sh/agentrules",
@@ -41,14 +41,15 @@
41
41
  "build": "tsdown",
42
42
  "dev": "tsdown --watch",
43
43
  "typecheck": "tsc --noEmit",
44
- "test": "bun test"
44
+ "test": "bun test",
45
+ "clean": "rm -rf node_modules dist .turbo"
45
46
  },
46
47
  "dependencies": {
47
48
  "diff": "^8.0.2",
48
- "zod": "^4.1.12"
49
+ "zod": "catalog:"
49
50
  },
50
51
  "devDependencies": {
51
52
  "tsdown": "^0.9.0",
52
- "typescript": "5.7.2"
53
+ "typescript": "catalog:"
53
54
  }
54
- }
55
+ }