@agentrules/core 0.0.2 → 0.0.4
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 +140 -0
- package/dist/index.d.ts +189 -88
- package/dist/index.js +334 -165
- package/package.json +2 -2
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,77 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
//#region src/types.d.ts
|
|
4
|
-
|
|
3
|
+
//#region src/types/constants.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shared constants for agentrules presets and registry.
|
|
6
|
+
*/
|
|
7
|
+
/** Filename for preset configuration */
|
|
8
|
+
/**
|
|
9
|
+
* Shared constants for agentrules presets and registry.
|
|
10
|
+
*/
|
|
11
|
+
/** Filename for preset configuration */
|
|
12
|
+
declare const PRESET_CONFIG_FILENAME = "agentrules.json";
|
|
13
|
+
/** JSON Schema URL for preset configuration */
|
|
14
|
+
declare const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/types/platform.d.ts
|
|
18
|
+
/**
|
|
19
|
+
* Single source of truth for platform IDs.
|
|
20
|
+
* Add new platforms here - types and config will follow.
|
|
21
|
+
*/
|
|
22
|
+
declare const PLATFORM_ID_TUPLE: readonly ["opencode", "codex", "claude", "cursor"];
|
|
23
|
+
/** Union type of supported platform IDs, derived from PLATFORM_ID_TUPLE */
|
|
24
|
+
type PlatformId = (typeof PLATFORM_ID_TUPLE)[number];
|
|
25
|
+
/** List of supported platform IDs as a readonly tuple */
|
|
26
|
+
declare const PLATFORM_IDS: readonly ["opencode", "codex", "claude", "cursor"];
|
|
27
|
+
type PlatformConfig = {
|
|
28
|
+
/** Directory name for project installs (e.g., ".opencode") */
|
|
29
|
+
projectDir: string;
|
|
30
|
+
/** Path for global installs (e.g., "~/.config/opencode") */
|
|
31
|
+
globalDir: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Platform-specific configuration.
|
|
35
|
+
* Single source of truth for all platform paths.
|
|
36
|
+
*/
|
|
37
|
+
declare const PLATFORMS: Record<PlatformId, PlatformConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Convention: preset files under this directory map to the platform config directory.
|
|
40
|
+
* e.g., `config/agent.md` → `.opencode/agent.md` (project) or `agent.md` (global)
|
|
41
|
+
*/
|
|
42
|
+
declare const CONFIG_DIR_NAME = "config";
|
|
43
|
+
declare function isSupportedPlatform(value: string): value is PlatformId;
|
|
44
|
+
declare function normalizePlatformInput(value: string): PlatformId;
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/types/definitions.d.ts
|
|
5
48
|
type AuthorInfo = {
|
|
6
49
|
name: string;
|
|
7
50
|
email?: string;
|
|
8
51
|
url?: string;
|
|
9
52
|
};
|
|
10
|
-
type
|
|
53
|
+
type PlatformPresetConfig = {
|
|
54
|
+
/** Path to platform config files. Defaults to platform's projectDir (e.g., ".opencode") */
|
|
55
|
+
path?: string;
|
|
11
56
|
features?: string[];
|
|
12
57
|
installMessage?: string;
|
|
13
58
|
};
|
|
14
59
|
type PresetConfig = {
|
|
60
|
+
$schema?: string;
|
|
61
|
+
name: string;
|
|
15
62
|
title: string;
|
|
16
|
-
version
|
|
63
|
+
version?: string;
|
|
17
64
|
description: string;
|
|
18
65
|
tags?: string[];
|
|
19
66
|
author?: AuthorInfo;
|
|
20
|
-
license
|
|
21
|
-
|
|
22
|
-
platforms: Partial<Record<PlatformId, PlatformSpecificConfig>>;
|
|
67
|
+
license: string;
|
|
68
|
+
platforms: Partial<Record<PlatformId, PlatformPresetConfig>>;
|
|
23
69
|
};
|
|
24
70
|
type BundledFile = {
|
|
25
71
|
path: string;
|
|
72
|
+
/** File size in bytes */
|
|
26
73
|
size: number;
|
|
27
74
|
checksum: string;
|
|
28
|
-
encoding: "utf-8" | "base64";
|
|
29
75
|
contents: string;
|
|
30
76
|
};
|
|
31
77
|
type RegistryBundle = {
|
|
@@ -36,7 +82,9 @@ type RegistryBundle = {
|
|
|
36
82
|
description: string;
|
|
37
83
|
tags: string[];
|
|
38
84
|
author?: AuthorInfo;
|
|
39
|
-
license
|
|
85
|
+
license: string;
|
|
86
|
+
licenseContent?: string;
|
|
87
|
+
readmeContent?: string;
|
|
40
88
|
features?: string[];
|
|
41
89
|
installMessage?: string;
|
|
42
90
|
files: BundledFile[];
|
|
@@ -50,78 +98,42 @@ type RegistryEntry = {
|
|
|
50
98
|
description: string;
|
|
51
99
|
tags: string[];
|
|
52
100
|
author?: AuthorInfo;
|
|
53
|
-
license
|
|
101
|
+
license: string;
|
|
54
102
|
features?: string[];
|
|
55
103
|
installMessage?: string;
|
|
56
104
|
bundlePath: string;
|
|
57
105
|
fileCount: number;
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
/** Total size of all files in bytes */
|
|
107
|
+
totalSize: number;
|
|
108
|
+
/** Whether the preset has a README.md */
|
|
109
|
+
hasReadmeContent?: boolean;
|
|
110
|
+
/** Whether the preset has a LICENSE.md */
|
|
111
|
+
hasLicenseContent?: boolean;
|
|
60
112
|
};
|
|
61
113
|
type RegistryData = {
|
|
62
114
|
$schema: string;
|
|
63
115
|
items: RegistryEntry[];
|
|
64
116
|
};
|
|
65
117
|
type RegistryIndex = Record<string, RegistryEntry>;
|
|
66
|
-
type RegistryIndexItem = RegistryEntry;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
|
118
|
+
type RegistryIndexItem = RegistryEntry;
|
|
119
|
+
type RegistryFileInput = {
|
|
120
|
+
path: string;
|
|
121
|
+
contents: ArrayBuffer | ArrayBufferView | string;
|
|
87
122
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
123
|
+
type RegistryPlatformInput = {
|
|
124
|
+
platform: PlatformId;
|
|
125
|
+
files: RegistryFileInput[];
|
|
126
|
+
/** Install message from INSTALL.txt file */
|
|
127
|
+
installMessage?: string;
|
|
118
128
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
type RegistryPresetInput = {
|
|
130
|
+
slug: string;
|
|
131
|
+
config: PresetConfig;
|
|
132
|
+
platforms: RegistryPlatformInput[];
|
|
133
|
+
readmeContent?: string;
|
|
134
|
+
licenseContent?: string;
|
|
135
|
+
}; //#endregion
|
|
136
|
+
//#region src/types/schema.d.ts
|
|
125
137
|
declare const platformIdSchema: z.ZodEnum<{
|
|
126
138
|
opencode: "opencode";
|
|
127
139
|
codex: "codex";
|
|
@@ -133,9 +145,29 @@ declare const authorSchema: z.ZodObject<{
|
|
|
133
145
|
email: z.ZodOptional<z.ZodEmail>;
|
|
134
146
|
url: z.ZodOptional<z.ZodURL>;
|
|
135
147
|
}, z.core.$strict>;
|
|
136
|
-
declare const
|
|
148
|
+
declare const titleSchema: z.ZodString;
|
|
149
|
+
declare const descriptionSchema: z.ZodString;
|
|
150
|
+
/** Validate a title string and return error message if invalid, undefined if valid */
|
|
151
|
+
declare function validateTitle(value: string): string | undefined;
|
|
152
|
+
/** Validate a description string and return error message if invalid, undefined if valid */
|
|
153
|
+
declare function validateDescription(value: string): string | undefined;
|
|
154
|
+
declare const slugSchema: z.ZodString;
|
|
155
|
+
/** Validate a slug string and return error message if invalid, undefined if valid */
|
|
156
|
+
declare function validateSlug(value: string): string | undefined;
|
|
157
|
+
declare const COMMON_LICENSES: readonly ["MIT", "Apache-2.0", "GPL-3.0-only", "BSD-3-Clause", "ISC", "Unlicense"];
|
|
158
|
+
type CommonLicense = (typeof COMMON_LICENSES)[number];
|
|
159
|
+
declare const licenseSchema: z.ZodString;
|
|
160
|
+
/** Validate a license string and return error message if invalid, undefined if valid */
|
|
161
|
+
declare function validateLicense(value: string): string | undefined;
|
|
162
|
+
declare const platformPresetConfigSchema: z.ZodObject<{
|
|
163
|
+
path: z.ZodOptional<z.ZodString>;
|
|
164
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
165
|
+
}, z.core.$strict>;
|
|
166
|
+
declare const presetConfigSchema: z.ZodObject<{
|
|
167
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
168
|
+
name: z.ZodString;
|
|
137
169
|
title: z.ZodString;
|
|
138
|
-
version: z.ZodString
|
|
170
|
+
version: z.ZodOptional<z.ZodString>;
|
|
139
171
|
description: z.ZodString;
|
|
140
172
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
141
173
|
author: z.ZodOptional<z.ZodObject<{
|
|
@@ -143,15 +175,18 @@ declare const platformPresetSchema: z.ZodObject<{
|
|
|
143
175
|
email: z.ZodOptional<z.ZodEmail>;
|
|
144
176
|
url: z.ZodOptional<z.ZodURL>;
|
|
145
177
|
}, z.core.$strict>>;
|
|
146
|
-
license: z.
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
license: z.ZodString;
|
|
179
|
+
platforms: z.ZodObject<{
|
|
180
|
+
[x: string]: z.ZodOptional<z.ZodObject<{
|
|
181
|
+
path: z.ZodOptional<z.ZodString>;
|
|
182
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
183
|
+
}, z.core.$strict>>;
|
|
184
|
+
}, z.core.$strip>;
|
|
149
185
|
}, z.core.$strict>;
|
|
150
186
|
declare const bundledFileSchema: z.ZodObject<{
|
|
151
187
|
path: z.ZodString;
|
|
152
188
|
size: z.ZodNumber;
|
|
153
189
|
checksum: z.ZodString;
|
|
154
|
-
encoding: z.ZodUnion<readonly [z.ZodLiteral<"utf-8">, z.ZodLiteral<"base64">]>;
|
|
155
190
|
contents: z.ZodString;
|
|
156
191
|
}, z.core.$strip>;
|
|
157
192
|
declare const registryBundleSchema: z.ZodObject<{
|
|
@@ -171,30 +206,30 @@ declare const registryBundleSchema: z.ZodObject<{
|
|
|
171
206
|
email: z.ZodOptional<z.ZodEmail>;
|
|
172
207
|
url: z.ZodOptional<z.ZodURL>;
|
|
173
208
|
}, z.core.$strict>>;
|
|
174
|
-
license: z.
|
|
209
|
+
license: z.ZodString;
|
|
210
|
+
licenseContent: z.ZodOptional<z.ZodString>;
|
|
211
|
+
readmeContent: z.ZodOptional<z.ZodString>;
|
|
175
212
|
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
176
213
|
installMessage: z.ZodOptional<z.ZodString>;
|
|
177
214
|
files: z.ZodArray<z.ZodObject<{
|
|
178
215
|
path: z.ZodString;
|
|
179
216
|
size: z.ZodNumber;
|
|
180
217
|
checksum: z.ZodString;
|
|
181
|
-
encoding: z.ZodUnion<readonly [z.ZodLiteral<"utf-8">, z.ZodLiteral<"base64">]>;
|
|
182
218
|
contents: z.ZodString;
|
|
183
219
|
}, z.core.$strip>>;
|
|
184
220
|
}, z.core.$strip>;
|
|
185
221
|
declare const registryEntrySchema: z.ZodObject<{
|
|
222
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
186
223
|
title: z.ZodString;
|
|
187
|
-
version: z.ZodString;
|
|
188
224
|
description: z.ZodString;
|
|
225
|
+
license: z.ZodString;
|
|
226
|
+
version: z.ZodString;
|
|
189
227
|
tags: z.ZodArray<z.ZodString>;
|
|
190
228
|
author: z.ZodOptional<z.ZodObject<{
|
|
191
229
|
name: z.ZodString;
|
|
192
230
|
email: z.ZodOptional<z.ZodEmail>;
|
|
193
231
|
url: z.ZodOptional<z.ZodURL>;
|
|
194
232
|
}, z.core.$strict>>;
|
|
195
|
-
license: z.ZodOptional<z.ZodString>;
|
|
196
|
-
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
197
|
-
installMessage: z.ZodOptional<z.ZodString>;
|
|
198
233
|
platform: z.ZodEnum<{
|
|
199
234
|
opencode: "opencode";
|
|
200
235
|
codex: "codex";
|
|
@@ -205,21 +240,22 @@ declare const registryEntrySchema: z.ZodObject<{
|
|
|
205
240
|
name: z.ZodString;
|
|
206
241
|
bundlePath: z.ZodString;
|
|
207
242
|
fileCount: z.ZodNumber;
|
|
208
|
-
|
|
243
|
+
totalSize: z.ZodNumber;
|
|
244
|
+
hasReadmeContent: z.ZodOptional<z.ZodBoolean>;
|
|
245
|
+
hasLicenseContent: z.ZodOptional<z.ZodBoolean>;
|
|
209
246
|
}, z.core.$strip>;
|
|
210
247
|
declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
248
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
211
249
|
title: z.ZodString;
|
|
212
|
-
version: z.ZodString;
|
|
213
250
|
description: z.ZodString;
|
|
251
|
+
license: z.ZodString;
|
|
252
|
+
version: z.ZodString;
|
|
214
253
|
tags: z.ZodArray<z.ZodString>;
|
|
215
254
|
author: z.ZodOptional<z.ZodObject<{
|
|
216
255
|
name: z.ZodString;
|
|
217
256
|
email: z.ZodOptional<z.ZodEmail>;
|
|
218
257
|
url: z.ZodOptional<z.ZodURL>;
|
|
219
258
|
}, z.core.$strict>>;
|
|
220
|
-
license: z.ZodOptional<z.ZodString>;
|
|
221
|
-
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
222
|
-
installMessage: z.ZodOptional<z.ZodString>;
|
|
223
259
|
platform: z.ZodEnum<{
|
|
224
260
|
opencode: "opencode";
|
|
225
261
|
codex: "codex";
|
|
@@ -230,8 +266,73 @@ declare const registryIndexSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
|
230
266
|
name: z.ZodString;
|
|
231
267
|
bundlePath: z.ZodString;
|
|
232
268
|
fileCount: z.ZodNumber;
|
|
233
|
-
|
|
269
|
+
totalSize: z.ZodNumber;
|
|
270
|
+
hasReadmeContent: z.ZodOptional<z.ZodBoolean>;
|
|
271
|
+
hasLicenseContent: z.ZodOptional<z.ZodBoolean>;
|
|
234
272
|
}, z.core.$strip>>;
|
|
235
273
|
|
|
236
274
|
//#endregion
|
|
237
|
-
|
|
275
|
+
//#region src/builder/registry.d.ts
|
|
276
|
+
type BuildRegistryDataOptions = {
|
|
277
|
+
presets: RegistryPresetInput[];
|
|
278
|
+
bundleBase?: string;
|
|
279
|
+
/** Override the auto-generated version. If not provided, uses current UTC date. */
|
|
280
|
+
version?: string;
|
|
281
|
+
};
|
|
282
|
+
type BuildRegistryDataResult = {
|
|
283
|
+
entries: RegistryEntry[];
|
|
284
|
+
index: RegistryIndex;
|
|
285
|
+
bundles: RegistryBundle[];
|
|
286
|
+
};
|
|
287
|
+
declare function buildRegistryData(options: BuildRegistryDataOptions): BuildRegistryDataResult;
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/builder/utils.d.ts
|
|
291
|
+
/**
|
|
292
|
+
* Generates a date-based version string in format YYYY.MM.DD
|
|
293
|
+
* Uses UTC to ensure consistent versioning across timezones
|
|
294
|
+
*/
|
|
295
|
+
declare function generateDateVersion(date?: Date): string;
|
|
296
|
+
declare function normalizeBundlePublicBase(value: string): string;
|
|
297
|
+
declare function isAbsoluteUrl(value: string): boolean;
|
|
298
|
+
declare function cleanInstallMessage(value: unknown): string | undefined;
|
|
299
|
+
declare function encodeItemName(slug: string, platform: PlatformId): string;
|
|
300
|
+
declare function validatePresetConfig(config: unknown, slug: string): PresetConfig;
|
|
301
|
+
declare function collectBundledFiles(files: Record<string, string>): BundledFile[];
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/client/bundle.d.ts
|
|
305
|
+
declare function decodeBundledFile(file: BundledFile): Uint8Array;
|
|
306
|
+
declare function verifyBundledFileChecksum(file: BundledFile, payload: ArrayBuffer | ArrayBufferView): Promise<void>;
|
|
307
|
+
declare function isLikelyText(payload: ArrayBuffer | ArrayBufferView): boolean;
|
|
308
|
+
declare function toUtf8String(payload: ArrayBuffer | ArrayBufferView): string;
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/client/registry.d.ts
|
|
312
|
+
declare function fetchRegistryIndex(baseUrl: string): Promise<RegistryIndex>;
|
|
313
|
+
declare function fetchRegistryBundle(baseUrl: string, bundlePath: string): Promise<RegistryBundle>;
|
|
314
|
+
declare function resolveRegistryEntry(index: RegistryIndex, input: string, explicitPlatform?: PlatformId): RegistryEntry;
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/utils/diff.d.ts
|
|
318
|
+
type DiffPreviewOptions = {
|
|
319
|
+
context?: number;
|
|
320
|
+
maxLines?: number;
|
|
321
|
+
};
|
|
322
|
+
declare function createDiffPreview(path: string, currentText: string, incomingText: string, options?: DiffPreviewOptions): string;
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/utils/encoding.d.ts
|
|
326
|
+
declare function toPosixPath(pathValue: string): string;
|
|
327
|
+
declare function encodeUtf8(value: string): Uint8Array<ArrayBuffer>;
|
|
328
|
+
declare function decodeUtf8(payload: ArrayBuffer | ArrayBufferView): string;
|
|
329
|
+
declare function toUint8Array(payload: ArrayBuffer | ArrayBufferView): Uint8Array<ArrayBufferLike>;
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/utils/paths.d.ts
|
|
333
|
+
declare function normalizeBundlePath(value: string): string;
|
|
334
|
+
declare function normalizePathFragment(value?: string): string | undefined;
|
|
335
|
+
declare function maybeStripPrefix(pathInput: string, prefix?: string): string;
|
|
336
|
+
|
|
337
|
+
//#endregion
|
|
338
|
+
export { AuthorInfo, BuildRegistryDataOptions, BuildRegistryDataResult, BundledFile, COMMON_LICENSES, CONFIG_DIR_NAME, CommonLicense, DiffPreviewOptions, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, PlatformId, PlatformPresetConfig, PresetConfig, RegistryBundle, RegistryData, RegistryEntry, RegistryFileInput, RegistryIndex, RegistryIndexItem, RegistryPlatformInput, RegistryPresetInput, authorSchema, buildRegistryData, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, generateDateVersion, isAbsoluteUrl, isLikelyText, isSupportedPlatform, licenseSchema, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetConfigSchema, presetConfigSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, slugSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateDescription, validateLicense, validatePresetConfig, validateSlug, validateTitle, verifyBundledFileChecksum };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,180 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
4
|
|
|
4
|
-
//#region src/
|
|
5
|
+
//#region src/types/constants.ts
|
|
6
|
+
/**
|
|
7
|
+
* Shared constants for agentrules presets and registry.
|
|
8
|
+
*/
|
|
9
|
+
/** Filename for preset configuration */
|
|
10
|
+
const PRESET_CONFIG_FILENAME = "agentrules.json";
|
|
11
|
+
/** JSON Schema URL for preset configuration */
|
|
12
|
+
const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/types/platform.ts
|
|
16
|
+
/**
|
|
17
|
+
* Single source of truth for platform IDs.
|
|
18
|
+
* Add new platforms here - types and config will follow.
|
|
19
|
+
*/
|
|
20
|
+
const PLATFORM_ID_TUPLE = [
|
|
21
|
+
"opencode",
|
|
22
|
+
"codex",
|
|
23
|
+
"claude",
|
|
24
|
+
"cursor"
|
|
25
|
+
];
|
|
26
|
+
/** List of supported platform IDs as a readonly tuple */
|
|
27
|
+
const PLATFORM_IDS = PLATFORM_ID_TUPLE;
|
|
28
|
+
/**
|
|
29
|
+
* Platform-specific configuration.
|
|
30
|
+
* Single source of truth for all platform paths.
|
|
31
|
+
*/
|
|
32
|
+
const PLATFORMS = {
|
|
33
|
+
opencode: {
|
|
34
|
+
projectDir: ".opencode",
|
|
35
|
+
globalDir: "~/.config/opencode"
|
|
36
|
+
},
|
|
37
|
+
codex: {
|
|
38
|
+
projectDir: ".codex",
|
|
39
|
+
globalDir: "~/.codex"
|
|
40
|
+
},
|
|
41
|
+
claude: {
|
|
42
|
+
projectDir: ".claude",
|
|
43
|
+
globalDir: "~/.claude"
|
|
44
|
+
},
|
|
45
|
+
cursor: {
|
|
46
|
+
projectDir: ".cursor",
|
|
47
|
+
globalDir: "~/.cursor"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Convention: preset files under this directory map to the platform config directory.
|
|
52
|
+
* e.g., `config/agent.md` → `.opencode/agent.md` (project) or `agent.md` (global)
|
|
53
|
+
*/
|
|
54
|
+
const CONFIG_DIR_NAME = "config";
|
|
55
|
+
function isSupportedPlatform(value) {
|
|
56
|
+
return PLATFORM_ID_TUPLE.includes(value);
|
|
57
|
+
}
|
|
58
|
+
function normalizePlatformInput(value) {
|
|
59
|
+
const normalized = value.toLowerCase();
|
|
60
|
+
if (isSupportedPlatform(normalized)) return normalized;
|
|
61
|
+
throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/types/schema.ts
|
|
66
|
+
const DATE_VERSION_REGEX = /^\d{4}\.(0[1-9]|1[0-2])\.(0[1-9]|[12]\d|3[01])(-\d+)?$/;
|
|
67
|
+
const platformIdSchema = z.enum(PLATFORM_IDS);
|
|
68
|
+
const authorSchema = z.object({
|
|
69
|
+
name: z.string().trim().min(1),
|
|
70
|
+
email: z.email().trim().optional(),
|
|
71
|
+
url: z.url().trim().optional()
|
|
72
|
+
}).strict();
|
|
73
|
+
const titleSchema = z.string().trim().min(1).max(120);
|
|
74
|
+
const descriptionSchema = z.string().trim().min(1).max(500);
|
|
75
|
+
/** Validate a title string and return error message if invalid, undefined if valid */
|
|
76
|
+
function validateTitle(value) {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!trimmed) return "Title is required";
|
|
79
|
+
if (trimmed.length > 120) return "Title must be 120 characters or less";
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
/** Validate a description string and return error message if invalid, undefined if valid */
|
|
83
|
+
function validateDescription(value) {
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (!trimmed) return "Description is required";
|
|
86
|
+
if (trimmed.length > 500) return "Description must be 500 characters or less";
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const versionSchema = z.string().trim().regex(DATE_VERSION_REGEX, "Version must be date-based (YYYY.MM.DD or YYYY.MM.DD-N)");
|
|
90
|
+
const tagSchema = z.string().trim().min(1).max(48);
|
|
91
|
+
const tagsSchema = z.array(tagSchema).max(10);
|
|
92
|
+
const featureSchema = z.string().trim().min(1).max(160);
|
|
93
|
+
const featuresSchema = z.array(featureSchema).max(10);
|
|
94
|
+
const installMessageSchema = z.string().trim().max(4e3);
|
|
95
|
+
const contentSchema = z.string();
|
|
96
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
97
|
+
const SLUG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
|
|
98
|
+
const slugSchema = z.string().trim().min(1).max(64).regex(SLUG_REGEX, SLUG_ERROR);
|
|
99
|
+
/** Validate a slug string and return error message if invalid, undefined if valid */
|
|
100
|
+
function validateSlug(value) {
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
if (!trimmed) return "Name is required";
|
|
103
|
+
if (trimmed.length > 64) return "Name must be 64 characters or less";
|
|
104
|
+
if (!SLUG_REGEX.test(trimmed)) return SLUG_ERROR;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const COMMON_LICENSES = [
|
|
108
|
+
"MIT",
|
|
109
|
+
"Apache-2.0",
|
|
110
|
+
"GPL-3.0-only",
|
|
111
|
+
"BSD-3-Clause",
|
|
112
|
+
"ISC",
|
|
113
|
+
"Unlicense"
|
|
114
|
+
];
|
|
115
|
+
const licenseSchema = z.string().trim().min(1).max(128);
|
|
116
|
+
/** Validate a license string and return error message if invalid, undefined if valid */
|
|
117
|
+
function validateLicense(value) {
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
if (!trimmed) return "License is required";
|
|
120
|
+
if (trimmed.length > 128) return "License must be 128 characters or less";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const pathSchema = z.string().trim().min(1);
|
|
124
|
+
const platformPresetConfigSchema = z.object({
|
|
125
|
+
path: pathSchema.optional(),
|
|
126
|
+
features: featuresSchema.optional()
|
|
127
|
+
}).strict();
|
|
128
|
+
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" });
|
|
129
|
+
const presetConfigSchema = z.object({
|
|
130
|
+
$schema: z.string().optional(),
|
|
131
|
+
name: slugSchema,
|
|
132
|
+
title: titleSchema,
|
|
133
|
+
version: versionSchema.optional(),
|
|
134
|
+
description: descriptionSchema,
|
|
135
|
+
tags: tagsSchema.optional(),
|
|
136
|
+
author: authorSchema.optional(),
|
|
137
|
+
license: licenseSchema,
|
|
138
|
+
platforms: platformsObjectSchema
|
|
139
|
+
}).strict();
|
|
140
|
+
const bundledFileSchema = z.object({
|
|
141
|
+
path: z.string().min(1),
|
|
142
|
+
size: z.number().int().nonnegative(),
|
|
143
|
+
checksum: z.string().length(64),
|
|
144
|
+
contents: z.string()
|
|
145
|
+
});
|
|
146
|
+
const registryBundleSchema = z.object({
|
|
147
|
+
slug: z.string().trim().min(1),
|
|
148
|
+
platform: platformIdSchema,
|
|
149
|
+
title: titleSchema,
|
|
150
|
+
version: versionSchema,
|
|
151
|
+
description: descriptionSchema,
|
|
152
|
+
tags: tagsSchema,
|
|
153
|
+
author: authorSchema.optional(),
|
|
154
|
+
license: licenseSchema,
|
|
155
|
+
licenseContent: contentSchema.optional(),
|
|
156
|
+
readmeContent: contentSchema.optional(),
|
|
157
|
+
features: featuresSchema.optional(),
|
|
158
|
+
installMessage: installMessageSchema.optional(),
|
|
159
|
+
files: z.array(bundledFileSchema).min(1)
|
|
160
|
+
});
|
|
161
|
+
const registryEntrySchema = registryBundleSchema.omit({
|
|
162
|
+
files: true,
|
|
163
|
+
readmeContent: true,
|
|
164
|
+
licenseContent: true,
|
|
165
|
+
installMessage: true
|
|
166
|
+
}).extend({
|
|
167
|
+
name: z.string().trim().min(1),
|
|
168
|
+
bundlePath: z.string().trim().min(1),
|
|
169
|
+
fileCount: z.number().int().nonnegative(),
|
|
170
|
+
totalSize: z.number().int().nonnegative(),
|
|
171
|
+
hasReadmeContent: z.boolean().optional(),
|
|
172
|
+
hasLicenseContent: z.boolean().optional()
|
|
173
|
+
});
|
|
174
|
+
const registryIndexSchema = z.record(z.string(), registryEntrySchema);
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/utils/encoding.ts
|
|
5
178
|
function toPosixPath(pathValue) {
|
|
6
179
|
return pathValue.split("\\").join("/");
|
|
7
180
|
}
|
|
@@ -21,7 +194,17 @@ function toUint8Array(payload) {
|
|
|
21
194
|
}
|
|
22
195
|
|
|
23
196
|
//#endregion
|
|
24
|
-
//#region src/
|
|
197
|
+
//#region src/builder/utils.ts
|
|
198
|
+
/**
|
|
199
|
+
* Generates a date-based version string in format YYYY.MM.DD
|
|
200
|
+
* Uses UTC to ensure consistent versioning across timezones
|
|
201
|
+
*/
|
|
202
|
+
function generateDateVersion(date = new Date()) {
|
|
203
|
+
const year = date.getUTCFullYear();
|
|
204
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
205
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
206
|
+
return `${year}.${month}.${day}`;
|
|
207
|
+
}
|
|
25
208
|
function normalizeBundlePublicBase(value) {
|
|
26
209
|
const trimmed = value.trim();
|
|
27
210
|
if (!trimmed) throw new Error("--bundle-base must be a non-empty string");
|
|
@@ -43,11 +226,13 @@ function encodeItemName(slug, platform) {
|
|
|
43
226
|
return `${slug}.${platform}`;
|
|
44
227
|
}
|
|
45
228
|
function validatePresetConfig(config, slug) {
|
|
46
|
-
if (!config || typeof config !== "object") throw new Error(`Invalid preset config
|
|
229
|
+
if (!config || typeof config !== "object") throw new Error(`Invalid preset config for ${slug}`);
|
|
47
230
|
const preset = config;
|
|
231
|
+
if (!preset.name || typeof preset.name !== "string") throw new Error(`Preset ${slug} is missing a name`);
|
|
48
232
|
if (!preset.title || typeof preset.title !== "string") throw new Error(`Preset ${slug} is missing a title`);
|
|
49
|
-
if (
|
|
233
|
+
if (preset.version !== void 0 && typeof preset.version !== "string") throw new Error(`Preset ${slug} has invalid version (must be string or omitted)`);
|
|
50
234
|
if (!preset.description || typeof preset.description !== "string") throw new Error(`Preset ${slug} is missing a description`);
|
|
235
|
+
if (!preset.license || typeof preset.license !== "string") throw new Error(`Preset ${slug} is missing a license (SPDX identifier required)`);
|
|
51
236
|
if (!preset.platforms || typeof preset.platforms !== "object") throw new Error(`Preset ${slug} is missing platforms map`);
|
|
52
237
|
return preset;
|
|
53
238
|
}
|
|
@@ -59,20 +244,125 @@ function collectBundledFiles(files) {
|
|
|
59
244
|
path: normalizedPath,
|
|
60
245
|
size: payload.length,
|
|
61
246
|
checksum: "",
|
|
62
|
-
encoding: "utf-8",
|
|
63
247
|
contents
|
|
64
248
|
};
|
|
65
249
|
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
66
250
|
}
|
|
67
251
|
|
|
68
252
|
//#endregion
|
|
69
|
-
//#region src/
|
|
70
|
-
const
|
|
71
|
-
|
|
253
|
+
//#region src/builder/registry.ts
|
|
254
|
+
const NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
255
|
+
function buildRegistryData(options) {
|
|
256
|
+
const bundleBase = normalizeBundlePublicBase(options.bundleBase ?? "/r");
|
|
257
|
+
const buildVersion = options.version ?? generateDateVersion();
|
|
258
|
+
const entries = [];
|
|
259
|
+
const bundles = [];
|
|
260
|
+
for (const presetInput of options.presets) {
|
|
261
|
+
if (!NAME_PATTERN.test(presetInput.slug)) throw new Error(`Invalid slug "${presetInput.slug}". Slugs must be lowercase kebab-case.`);
|
|
262
|
+
const presetConfig = validatePresetConfig(presetInput.config, presetInput.slug);
|
|
263
|
+
if (presetInput.platforms.length === 0) throw new Error(`Preset ${presetInput.slug} has no platform inputs.`);
|
|
264
|
+
for (const platformInput of presetInput.platforms) {
|
|
265
|
+
const platform = platformInput.platform;
|
|
266
|
+
ensureKnownPlatform(platform, presetInput.slug);
|
|
267
|
+
const platformConfig = presetConfig.platforms?.[platform];
|
|
268
|
+
if (!platformConfig) throw new Error(`Preset ${presetInput.slug} has files for platform "${platform}" but no config entry.`);
|
|
269
|
+
if (platformInput.files.length === 0) throw new Error(`Preset ${presetInput.slug}/${platform} does not include any files.`);
|
|
270
|
+
const files = createBundledFilesFromInputs(platformInput.files);
|
|
271
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
272
|
+
const installMessage = cleanInstallMessage(platformInput.installMessage);
|
|
273
|
+
const features = platformConfig.features ?? [];
|
|
274
|
+
const readmeContent = presetInput.readmeContent?.trim() || void 0;
|
|
275
|
+
const licenseContent = presetInput.licenseContent?.trim() || void 0;
|
|
276
|
+
const entry = {
|
|
277
|
+
name: encodeItemName(presetInput.slug, platform),
|
|
278
|
+
slug: presetInput.slug,
|
|
279
|
+
platform,
|
|
280
|
+
title: presetConfig.title,
|
|
281
|
+
version: buildVersion,
|
|
282
|
+
description: presetConfig.description,
|
|
283
|
+
tags: presetConfig.tags ?? [],
|
|
284
|
+
author: presetConfig.author,
|
|
285
|
+
license: presetConfig.license,
|
|
286
|
+
features,
|
|
287
|
+
bundlePath: getBundlePublicPath(bundleBase, presetInput.slug, platform, buildVersion),
|
|
288
|
+
fileCount: files.length,
|
|
289
|
+
totalSize,
|
|
290
|
+
hasReadmeContent: Boolean(readmeContent),
|
|
291
|
+
hasLicenseContent: Boolean(licenseContent)
|
|
292
|
+
};
|
|
293
|
+
const bundle = {
|
|
294
|
+
slug: presetInput.slug,
|
|
295
|
+
platform,
|
|
296
|
+
title: presetConfig.title,
|
|
297
|
+
version: buildVersion,
|
|
298
|
+
description: presetConfig.description,
|
|
299
|
+
tags: presetConfig.tags ?? [],
|
|
300
|
+
author: presetConfig.author,
|
|
301
|
+
license: presetConfig.license,
|
|
302
|
+
licenseContent,
|
|
303
|
+
readmeContent,
|
|
304
|
+
features,
|
|
305
|
+
installMessage,
|
|
306
|
+
files
|
|
307
|
+
};
|
|
308
|
+
entries.push(entry);
|
|
309
|
+
bundles.push(bundle);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
sortBySlugAndPlatform(entries);
|
|
313
|
+
sortBySlugAndPlatform(bundles);
|
|
314
|
+
const index = entries.reduce((acc, entry) => {
|
|
315
|
+
acc[entry.name] = entry;
|
|
316
|
+
return acc;
|
|
317
|
+
}, {});
|
|
318
|
+
return {
|
|
319
|
+
entries,
|
|
320
|
+
index,
|
|
321
|
+
bundles
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function createBundledFilesFromInputs(files) {
|
|
325
|
+
return files.map((file) => {
|
|
326
|
+
const payload = normalizeFilePayload(file.contents);
|
|
327
|
+
const contents = encodeFilePayload(payload, file.path);
|
|
328
|
+
const checksum = createHash("sha256").update(payload).digest("hex");
|
|
329
|
+
return {
|
|
330
|
+
path: toPosixPath(file.path),
|
|
331
|
+
size: payload.length,
|
|
332
|
+
checksum,
|
|
333
|
+
contents
|
|
334
|
+
};
|
|
335
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
336
|
+
}
|
|
337
|
+
function normalizeFilePayload(contents) {
|
|
338
|
+
if (typeof contents === "string") return Buffer.from(contents, "utf8");
|
|
339
|
+
if (contents instanceof ArrayBuffer) return Buffer.from(contents);
|
|
340
|
+
if (ArrayBuffer.isView(contents)) return Buffer.from(contents.buffer, contents.byteOffset, contents.byteLength);
|
|
341
|
+
return Buffer.from(contents);
|
|
342
|
+
}
|
|
343
|
+
function encodeFilePayload(buffer, filePath) {
|
|
344
|
+
const utf8 = buffer.toString("utf8");
|
|
345
|
+
if (!Buffer.from(utf8, "utf8").equals(buffer)) throw new Error(`Binary files are not supported: "${filePath}". Only UTF-8 text files are allowed.`);
|
|
346
|
+
return utf8;
|
|
347
|
+
}
|
|
348
|
+
function getBundlePublicPath(base, slug, platform, version) {
|
|
349
|
+
const prefix = base === "/" ? "" : base;
|
|
350
|
+
return `${prefix}/${slug}/${platform}.${version}.json`;
|
|
351
|
+
}
|
|
352
|
+
function ensureKnownPlatform(platform, slug) {
|
|
353
|
+
if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
function sortBySlugAndPlatform(items) {
|
|
356
|
+
items.sort((a, b) => {
|
|
357
|
+
if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
|
|
358
|
+
return a.slug.localeCompare(b.slug);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/client/bundle.ts
|
|
72
364
|
function decodeBundledFile(file) {
|
|
73
|
-
|
|
74
|
-
if (file.encoding === "base64") return decodeBase64(file.contents);
|
|
75
|
-
throw new Error(`Unsupported encoding "${file.encoding}" for ${file.path}.`);
|
|
365
|
+
return encodeUtf8(file.contents);
|
|
76
366
|
}
|
|
77
367
|
async function verifyBundledFileChecksum(file, payload) {
|
|
78
368
|
const bytes = toUint8Array(payload);
|
|
@@ -92,39 +382,6 @@ function isLikelyText(payload) {
|
|
|
92
382
|
function toUtf8String(payload) {
|
|
93
383
|
return decodeUtf8(payload);
|
|
94
384
|
}
|
|
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
385
|
async function sha256Hex(payload) {
|
|
129
386
|
const crypto = globalThis.crypto;
|
|
130
387
|
if (!crypto?.subtle) throw new Error("SHA-256 hashing requires Web Crypto API support.");
|
|
@@ -133,60 +390,7 @@ async function sha256Hex(payload) {
|
|
|
133
390
|
}
|
|
134
391
|
|
|
135
392
|
//#endregion
|
|
136
|
-
//#region src/
|
|
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
|
|
393
|
+
//#region src/client/registry.ts
|
|
190
394
|
async function fetchRegistryIndex(baseUrl) {
|
|
191
395
|
const indexUrl = new URL("registry.index.json", baseUrl);
|
|
192
396
|
const response = await fetch(indexUrl);
|
|
@@ -202,11 +406,7 @@ async function fetchRegistryBundle(baseUrl, bundlePath) {
|
|
|
202
406
|
const response = await fetch(bundleUrl);
|
|
203
407
|
if (!response.ok) throw new Error(`Failed to download bundle (${response.status} ${response.statusText}).`);
|
|
204
408
|
try {
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
bundle,
|
|
208
|
-
etag: response.headers.get("etag")
|
|
209
|
-
};
|
|
409
|
+
return await response.json();
|
|
210
410
|
} catch (error) {
|
|
211
411
|
throw new Error(`Unable to parse bundle JSON: ${error.message}`);
|
|
212
412
|
}
|
|
@@ -222,7 +422,7 @@ function resolveRegistryEntry(index, input, explicitPlatform) {
|
|
|
222
422
|
if (!platform) {
|
|
223
423
|
const parts = normalizedInput.split(".");
|
|
224
424
|
const maybePlatform = parts.at(-1);
|
|
225
|
-
if (maybePlatform &&
|
|
425
|
+
if (maybePlatform && isSupportedPlatform(maybePlatform)) {
|
|
226
426
|
platform = maybePlatform;
|
|
227
427
|
slugHint = parts.slice(0, -1).join(".");
|
|
228
428
|
}
|
|
@@ -242,65 +442,34 @@ function resolveRegistryEntry(index, input, explicitPlatform) {
|
|
|
242
442
|
}
|
|
243
443
|
|
|
244
444
|
//#endregion
|
|
245
|
-
//#region src/
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
445
|
+
//#region src/utils/diff.ts
|
|
446
|
+
const DEFAULT_CONTEXT = 2;
|
|
447
|
+
const DEFAULT_MAX_LINES = 40;
|
|
448
|
+
function createDiffPreview(path, currentText, incomingText, options = {}) {
|
|
449
|
+
const patch = createTwoFilesPatch(`${path} (current)`, `${path} (incoming)`, currentText, incomingText, void 0, void 0, { context: options.context ?? DEFAULT_CONTEXT });
|
|
450
|
+
const lines = patch.trim().split("\n");
|
|
451
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
452
|
+
const limited = lines.slice(0, maxLines);
|
|
453
|
+
if (lines.length > maxLines) limited.push("...");
|
|
454
|
+
return limited.join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/utils/paths.ts
|
|
459
|
+
function normalizeBundlePath(value) {
|
|
460
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
|
|
461
|
+
}
|
|
462
|
+
function normalizePathFragment(value) {
|
|
463
|
+
if (!value) return;
|
|
464
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
465
|
+
return normalized.replace(/\/+$/, "");
|
|
466
|
+
}
|
|
467
|
+
function maybeStripPrefix(pathInput, prefix) {
|
|
468
|
+
if (!prefix) return pathInput;
|
|
469
|
+
if (pathInput === prefix) return "";
|
|
470
|
+
if (pathInput.startsWith(`${prefix}/`)) return pathInput.slice(prefix.length + 1);
|
|
471
|
+
return pathInput;
|
|
472
|
+
}
|
|
304
473
|
|
|
305
474
|
//#endregion
|
|
306
|
-
export { PLATFORM_IDS, authorSchema, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8,
|
|
475
|
+
export { COMMON_LICENSES, CONFIG_DIR_NAME, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, authorSchema, buildRegistryData, bundledFileSchema, cleanInstallMessage, collectBundledFiles, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeItemName, encodeUtf8, fetchRegistryBundle, fetchRegistryIndex, generateDateVersion, isAbsoluteUrl, isLikelyText, isSupportedPlatform, licenseSchema, maybeStripPrefix, normalizeBundlePath, normalizeBundlePublicBase, normalizePathFragment, normalizePlatformInput, platformIdSchema, platformPresetConfigSchema, presetConfigSchema, registryBundleSchema, registryEntrySchema, registryIndexSchema, resolveRegistryEntry, slugSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateDescription, validateLicense, validatePresetConfig, validateSlug, validateTitle, verifyBundledFileChecksum };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentrules/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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
|
+
}
|