@agentrules/cli 0.0.10 → 0.0.11
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 +56 -15
- package/dist/index.js +281 -248
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ agentrules add agentic-dev-starter --platform opencode --dry-run
|
|
|
63
63
|
|
|
64
64
|
### `agentrules init [directory]`
|
|
65
65
|
|
|
66
|
-
Initialize a
|
|
66
|
+
Initialize a preset config in a platform directory. The command guides you through the required fields for publishing.
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
agentrules init [directory] [options]
|
|
@@ -73,7 +73,7 @@ agentrules init [directory] [options]
|
|
|
73
73
|
|
|
74
74
|
| Option | Description |
|
|
75
75
|
|--------|-------------|
|
|
76
|
-
| `-n, --name <name>` | Preset name (default:
|
|
76
|
+
| `-n, --name <name>` | Preset name (default: `my-preset`) |
|
|
77
77
|
| `-t, --title <title>` | Display title |
|
|
78
78
|
| `--description <text>` | Preset description |
|
|
79
79
|
| `-p, --platform <platform>` | Target platform |
|
|
@@ -84,30 +84,69 @@ agentrules init [directory] [options]
|
|
|
84
84
|
**Examples:**
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
cd my-preset
|
|
90
|
-
|
|
91
|
-
# Initialize in current directory
|
|
87
|
+
# Initialize in your existing platform directory
|
|
88
|
+
cd .opencode
|
|
92
89
|
agentrules init
|
|
93
90
|
|
|
94
|
-
#
|
|
95
|
-
agentrules init
|
|
91
|
+
# Initialize in a specific platform directory
|
|
92
|
+
agentrules init .claude
|
|
96
93
|
|
|
97
94
|
# Accept all defaults, skip prompts
|
|
98
|
-
agentrules init
|
|
95
|
+
agentrules init .opencode --yes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
After running `init`, your preset structure is:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
.opencode/
|
|
102
|
+
├── agentrules.json # Preset config (created by init)
|
|
103
|
+
├── AGENTS.md # Your config files (included in bundle)
|
|
104
|
+
├── commands/
|
|
105
|
+
│ └── review.md
|
|
106
|
+
└── .agentrules/ # Optional metadata folder
|
|
107
|
+
├── README.md # Shown on registry page
|
|
108
|
+
├── LICENSE.md # Full license text
|
|
109
|
+
└── INSTALL.txt # Shown after install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Preset Config Fields
|
|
113
|
+
|
|
114
|
+
| Field | Required | Description |
|
|
115
|
+
|-------|----------|-------------|
|
|
116
|
+
| `name` | Yes | URL-safe identifier (lowercase, hyphens) |
|
|
117
|
+
| `title` | Yes | Display name |
|
|
118
|
+
| `description` | Yes | Short description (max 500 chars) |
|
|
119
|
+
| `license` | Yes | SPDX license identifier (e.g., `MIT`) |
|
|
120
|
+
| `platform` | Yes | Target platform: `opencode`, `claude`, `cursor`, `codex` |
|
|
121
|
+
| `version` | No | Major version (default: 1) |
|
|
122
|
+
| `tags` | No | Up to 10 tags for discoverability |
|
|
123
|
+
| `features` | No | Up to 5 key features to highlight |
|
|
124
|
+
| `ignore` | No | Additional patterns to exclude from bundle |
|
|
125
|
+
|
|
126
|
+
### Auto-Excluded Files
|
|
127
|
+
|
|
128
|
+
These files are automatically excluded from bundles:
|
|
129
|
+
- `node_modules/`, `.git/`, `.DS_Store`
|
|
130
|
+
- Lock files: `package-lock.json`, `bun.lockb`, `pnpm-lock.yaml`, `*.lock`
|
|
131
|
+
|
|
132
|
+
Use the `ignore` field for additional exclusions:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"ignore": ["*.log", "test-fixtures", "*.tmp"]
|
|
137
|
+
}
|
|
99
138
|
```
|
|
100
139
|
|
|
101
140
|
### `agentrules validate [path]`
|
|
102
141
|
|
|
103
|
-
Validate a preset configuration.
|
|
142
|
+
Validate a preset configuration before publishing.
|
|
104
143
|
|
|
105
144
|
```bash
|
|
106
145
|
# Validate current directory
|
|
107
146
|
agentrules validate
|
|
108
147
|
|
|
109
148
|
# Validate a specific path
|
|
110
|
-
agentrules validate
|
|
149
|
+
agentrules validate .opencode
|
|
111
150
|
```
|
|
112
151
|
|
|
113
152
|
---
|
|
@@ -161,14 +200,16 @@ agentrules publish --dry-run
|
|
|
161
200
|
|
|
162
201
|
**Versioning:** Presets use `MAJOR.MINOR` versioning. You set the major version, and the registry auto-increments the minor version on each publish.
|
|
163
202
|
|
|
164
|
-
### `agentrules unpublish <
|
|
203
|
+
### `agentrules unpublish <slug> <platform> <version>`
|
|
165
204
|
|
|
166
|
-
Remove a preset from the registry. Requires authentication.
|
|
205
|
+
Remove a specific version of a preset from the registry. Requires authentication.
|
|
167
206
|
|
|
168
207
|
```bash
|
|
169
|
-
agentrules unpublish my-preset
|
|
208
|
+
agentrules unpublish my-preset opencode 1.0
|
|
170
209
|
```
|
|
171
210
|
|
|
211
|
+
**Note:** Unpublished versions cannot be republished with the same version number.
|
|
212
|
+
|
|
172
213
|
---
|
|
173
214
|
|
|
174
215
|
## Registry Management
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
+
import { AGENT_RULES_DIR, API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, createDiffPreview, decodeBundledFile, descriptionSchema, fetchBundle, getPlatformFromDir, isLikelyText, isPlatformDir, isSupportedPlatform, licenseSchema, normalizeBundlePath, normalizePlatformInput, resolvePreset, slugSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "path";
|
|
5
6
|
import { exec } from "child_process";
|
|
6
7
|
import { promisify } from "util";
|
|
7
|
-
import { API_ENDPOINTS, COMMON_LICENSES, CONFIG_DIR_NAME, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, createDiffPreview, decodeBundledFile, descriptionSchema, fetchBundle, isLikelyText, isSupportedPlatform, licenseSchema, normalizeBundlePath, normalizePlatformInput, resolvePreset, slugSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
|
|
8
8
|
import * as client from "openid-client";
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { chmod, constants } from "fs";
|
|
@@ -575,6 +575,19 @@ function formatPollingError(err) {
|
|
|
575
575
|
return "Lost connection to server. Please try again.";
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/lib/errors.ts
|
|
580
|
+
/**
|
|
581
|
+
* Error utilities
|
|
582
|
+
*/
|
|
583
|
+
/**
|
|
584
|
+
* Safely extract an error message from any error type.
|
|
585
|
+
*/
|
|
586
|
+
function getErrorMessage(error$2) {
|
|
587
|
+
if (error$2 instanceof Error) return error$2.message;
|
|
588
|
+
return String(error$2);
|
|
589
|
+
}
|
|
590
|
+
|
|
578
591
|
//#endregion
|
|
579
592
|
//#region src/lib/api/presets.ts
|
|
580
593
|
/**
|
|
@@ -608,10 +621,9 @@ async function publishPreset(baseUrl, token, input) {
|
|
|
608
621
|
data
|
|
609
622
|
};
|
|
610
623
|
} catch (error$2) {
|
|
611
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
612
624
|
return {
|
|
613
625
|
success: false,
|
|
614
|
-
error: `Failed to connect to registry: ${
|
|
626
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
615
627
|
};
|
|
616
628
|
}
|
|
617
629
|
}
|
|
@@ -640,10 +652,9 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
|
|
|
640
652
|
data
|
|
641
653
|
};
|
|
642
654
|
} catch (error$2) {
|
|
643
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
644
655
|
return {
|
|
645
656
|
success: false,
|
|
646
|
-
error: `Failed to connect to registry: ${
|
|
657
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
647
658
|
};
|
|
648
659
|
}
|
|
649
660
|
}
|
|
@@ -673,6 +684,7 @@ async function fetchSession(baseUrl, token) {
|
|
|
673
684
|
|
|
674
685
|
//#endregion
|
|
675
686
|
//#region src/lib/config.ts
|
|
687
|
+
/** Directory for CLI configuration and credentials (e.g., ~/.agentrules/) */
|
|
676
688
|
const CONFIG_DIRNAME = ".agentrules";
|
|
677
689
|
const CONFIG_FILENAME = "config.json";
|
|
678
690
|
const CONFIG_HOME_ENV = "AGENT_RULES_HOME";
|
|
@@ -867,7 +879,7 @@ async function loadStore() {
|
|
|
867
879
|
const store = JSON.parse(raw);
|
|
868
880
|
return store;
|
|
869
881
|
} catch (error$2) {
|
|
870
|
-
log.debug(`Failed to load credentials: ${
|
|
882
|
+
log.debug(`Failed to load credentials: ${getErrorMessage(error$2)}`);
|
|
871
883
|
return {};
|
|
872
884
|
}
|
|
873
885
|
}
|
|
@@ -981,7 +993,7 @@ async function createAppContext(options = {}) {
|
|
|
981
993
|
log.debug("Saved fetched user info to credentials");
|
|
982
994
|
}
|
|
983
995
|
} catch (error$2) {
|
|
984
|
-
log.debug(`Failed to fetch user info: ${
|
|
996
|
+
log.debug(`Failed to fetch user info: ${getErrorMessage(error$2)}`);
|
|
985
997
|
}
|
|
986
998
|
}
|
|
987
999
|
log.debug(`App context loaded: isLoggedIn=${isLoggedIn}, user=${user?.name ?? "none"}`);
|
|
@@ -1018,10 +1030,11 @@ function resolveRegistry(config, alias) {
|
|
|
1018
1030
|
}
|
|
1019
1031
|
let globalContext = null;
|
|
1020
1032
|
/**
|
|
1021
|
-
* Gets the global app context
|
|
1022
|
-
*
|
|
1033
|
+
* Gets the global app context.
|
|
1034
|
+
* Throws if context has not been initialized via initAppContext().
|
|
1023
1035
|
*/
|
|
1024
1036
|
function useAppContext() {
|
|
1037
|
+
if (!globalContext) throw new Error("App context not initialized");
|
|
1025
1038
|
return globalContext;
|
|
1026
1039
|
}
|
|
1027
1040
|
/**
|
|
@@ -1042,7 +1055,6 @@ const CLIENT_ID = "agentrules-cli";
|
|
|
1042
1055
|
async function login(options = {}) {
|
|
1043
1056
|
const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
|
|
1044
1057
|
const ctx = useAppContext();
|
|
1045
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1046
1058
|
const { url: registryUrl } = ctx.registry;
|
|
1047
1059
|
log.debug(`Authenticating with ${registryUrl}`);
|
|
1048
1060
|
try {
|
|
@@ -1106,10 +1118,9 @@ async function login(options = {}) {
|
|
|
1106
1118
|
} : void 0
|
|
1107
1119
|
};
|
|
1108
1120
|
} catch (error$2) {
|
|
1109
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
1110
1121
|
return {
|
|
1111
1122
|
success: false,
|
|
1112
|
-
error:
|
|
1123
|
+
error: getErrorMessage(error$2)
|
|
1113
1124
|
};
|
|
1114
1125
|
}
|
|
1115
1126
|
}
|
|
@@ -1152,7 +1163,6 @@ async function logout(options = {}) {
|
|
|
1152
1163
|
};
|
|
1153
1164
|
}
|
|
1154
1165
|
const ctx = useAppContext();
|
|
1155
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1156
1166
|
const { url: registryUrl } = ctx.registry;
|
|
1157
1167
|
const hadCredentials = ctx.credentials !== null;
|
|
1158
1168
|
if (hadCredentials) {
|
|
@@ -1172,7 +1182,6 @@ async function logout(options = {}) {
|
|
|
1172
1182
|
*/
|
|
1173
1183
|
async function whoami() {
|
|
1174
1184
|
const ctx = useAppContext();
|
|
1175
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1176
1185
|
const { url: registryUrl } = ctx.registry;
|
|
1177
1186
|
return {
|
|
1178
1187
|
success: true,
|
|
@@ -1187,7 +1196,6 @@ async function whoami() {
|
|
|
1187
1196
|
//#region src/commands/preset/add.ts
|
|
1188
1197
|
async function addPreset(options) {
|
|
1189
1198
|
const ctx = useAppContext();
|
|
1190
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1191
1199
|
const { alias: registryAlias, url: registryUrl } = ctx.registry;
|
|
1192
1200
|
const dryRun = Boolean(options.dryRun);
|
|
1193
1201
|
const { slug, platform, version: version$1 } = parsePresetInput(options.preset, options.platform, options.version);
|
|
@@ -1296,14 +1304,6 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1296
1304
|
const data = Buffer.from(decoded);
|
|
1297
1305
|
await verifyBundledFileChecksum(file, data);
|
|
1298
1306
|
const destResult = computeDestinationPath(file.path, target);
|
|
1299
|
-
if (destResult.skipped) {
|
|
1300
|
-
files.push({
|
|
1301
|
-
path: file.path,
|
|
1302
|
-
status: "skipped"
|
|
1303
|
-
});
|
|
1304
|
-
log.debug(`Skipped (root file): ${file.path}`);
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1307
1307
|
const destination = destResult.path;
|
|
1308
1308
|
if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
|
|
1309
1309
|
const existing = await readExistingFile(destination);
|
|
@@ -1349,27 +1349,23 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1349
1349
|
conflicts
|
|
1350
1350
|
};
|
|
1351
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Compute destination path for a bundled file.
|
|
1354
|
+
*
|
|
1355
|
+
* Bundle files are stored with paths relative to the platform directory
|
|
1356
|
+
* (e.g., "AGENTS.md", "commands/test.md") and installed to:
|
|
1357
|
+
* - Project/custom: <root>/<projectDir>/<path> (e.g., .opencode/AGENTS.md)
|
|
1358
|
+
* - Global: <root>/<path> (e.g., ~/.config/opencode/AGENTS.md)
|
|
1359
|
+
*/
|
|
1352
1360
|
function computeDestinationPath(pathInput, target) {
|
|
1353
1361
|
const normalized = normalizeBundlePath(pathInput);
|
|
1354
|
-
|
|
1355
|
-
const isConfigFile = normalized.startsWith(configPrefix);
|
|
1356
|
-
if (target.mode === "global" && !isConfigFile) return {
|
|
1357
|
-
skipped: true,
|
|
1358
|
-
path: null
|
|
1359
|
-
};
|
|
1362
|
+
if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
|
|
1360
1363
|
let relativePath;
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1363
|
-
if (target.mode === "global") relativePath = withoutConfigPrefix;
|
|
1364
|
-
else relativePath = `${target.projectDir}/${withoutConfigPrefix}`;
|
|
1365
|
-
} else relativePath = normalized;
|
|
1366
|
-
if (!relativePath) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
|
|
1364
|
+
if (target.mode === "global") relativePath = normalized;
|
|
1365
|
+
else relativePath = `${target.projectDir}/${normalized}`;
|
|
1367
1366
|
const destination = resolve(target.root, relativePath);
|
|
1368
1367
|
ensureWithinRoot(destination, target.root);
|
|
1369
|
-
return {
|
|
1370
|
-
skipped: false,
|
|
1371
|
-
path: destination
|
|
1372
|
-
};
|
|
1368
|
+
return { path: destination };
|
|
1373
1369
|
}
|
|
1374
1370
|
async function readExistingFile(pathname) {
|
|
1375
1371
|
try {
|
|
@@ -1431,6 +1427,22 @@ async function directoryExists(path$1) {
|
|
|
1431
1427
|
|
|
1432
1428
|
//#endregion
|
|
1433
1429
|
//#region src/lib/preset-utils.ts
|
|
1430
|
+
const INSTALL_FILENAME = "INSTALL.txt";
|
|
1431
|
+
const README_FILENAME = "README.md";
|
|
1432
|
+
const LICENSE_FILENAME = "LICENSE.md";
|
|
1433
|
+
/**
|
|
1434
|
+
* Files/directories that are always excluded from presets.
|
|
1435
|
+
* These are never useful in a preset bundle.
|
|
1436
|
+
*/
|
|
1437
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
1438
|
+
"node_modules",
|
|
1439
|
+
".git",
|
|
1440
|
+
".DS_Store",
|
|
1441
|
+
"*.lock",
|
|
1442
|
+
"package-lock.json",
|
|
1443
|
+
"bun.lockb",
|
|
1444
|
+
"pnpm-lock.yaml"
|
|
1445
|
+
];
|
|
1434
1446
|
/**
|
|
1435
1447
|
* Normalize a string to a valid preset slug (lowercase kebab-case)
|
|
1436
1448
|
*/
|
|
@@ -1454,6 +1466,128 @@ async function resolveConfigPath(inputPath) {
|
|
|
1454
1466
|
if (stats?.isDirectory()) return join(inputPath, PRESET_CONFIG_FILENAME);
|
|
1455
1467
|
return inputPath;
|
|
1456
1468
|
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Load a preset from a directory containing agentrules.json.
|
|
1471
|
+
*
|
|
1472
|
+
* Config in platform dir (e.g., .claude/agentrules.json):
|
|
1473
|
+
* - Preset files: siblings of config
|
|
1474
|
+
* - Metadata: .agentrules/ subfolder
|
|
1475
|
+
*
|
|
1476
|
+
* Config at repo root:
|
|
1477
|
+
* - Preset files: in .claude/ (or `path` from config)
|
|
1478
|
+
* - Metadata: .agentrules/ subfolder
|
|
1479
|
+
*/
|
|
1480
|
+
async function loadPreset(presetDir) {
|
|
1481
|
+
const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
|
|
1482
|
+
if (!await fileExists(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
1483
|
+
const configRaw = await readFile(configPath, "utf8");
|
|
1484
|
+
let configJson;
|
|
1485
|
+
try {
|
|
1486
|
+
configJson = JSON.parse(configRaw);
|
|
1487
|
+
} catch {
|
|
1488
|
+
throw new Error(`Invalid JSON in ${configPath}`);
|
|
1489
|
+
}
|
|
1490
|
+
const configObj = configJson;
|
|
1491
|
+
const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
|
|
1492
|
+
const config = validatePresetConfig(configJson, identifier);
|
|
1493
|
+
const slug = config.name;
|
|
1494
|
+
const dirName = basename(presetDir);
|
|
1495
|
+
const isConfigInPlatformDir = isPlatformDir(dirName);
|
|
1496
|
+
let filesDir;
|
|
1497
|
+
let metadataDir;
|
|
1498
|
+
if (isConfigInPlatformDir) {
|
|
1499
|
+
filesDir = presetDir;
|
|
1500
|
+
metadataDir = join(presetDir, AGENT_RULES_DIR);
|
|
1501
|
+
log.debug(`Config in platform dir: files in ${filesDir}, metadata in ${metadataDir}`);
|
|
1502
|
+
} else {
|
|
1503
|
+
const platformDir = config.path ?? PLATFORMS[config.platform].projectDir;
|
|
1504
|
+
filesDir = join(presetDir, platformDir);
|
|
1505
|
+
metadataDir = join(presetDir, AGENT_RULES_DIR);
|
|
1506
|
+
log.debug(`Config at repo root: files in ${filesDir}, metadata in ${metadataDir}`);
|
|
1507
|
+
if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in ${PRESET_CONFIG_FILENAME}.`);
|
|
1508
|
+
}
|
|
1509
|
+
let installMessage;
|
|
1510
|
+
let readmeContent;
|
|
1511
|
+
let licenseContent;
|
|
1512
|
+
if (await directoryExists(metadataDir)) {
|
|
1513
|
+
installMessage = await readFileIfExists(join(metadataDir, INSTALL_FILENAME));
|
|
1514
|
+
readmeContent = await readFileIfExists(join(metadataDir, README_FILENAME));
|
|
1515
|
+
licenseContent = await readFileIfExists(join(metadataDir, LICENSE_FILENAME));
|
|
1516
|
+
}
|
|
1517
|
+
const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...config.ignore ?? []];
|
|
1518
|
+
const rootExclude = [PRESET_CONFIG_FILENAME, AGENT_RULES_DIR];
|
|
1519
|
+
const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
|
|
1520
|
+
if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
|
|
1521
|
+
return {
|
|
1522
|
+
slug,
|
|
1523
|
+
config,
|
|
1524
|
+
files,
|
|
1525
|
+
installMessage,
|
|
1526
|
+
readmeContent,
|
|
1527
|
+
licenseContent
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Check if a filename matches an ignore pattern.
|
|
1532
|
+
* Supports:
|
|
1533
|
+
* - Exact match: "node_modules"
|
|
1534
|
+
* - Extension match: "*.lock"
|
|
1535
|
+
* - Prefix match: ".git*" (not implemented yet, keeping simple)
|
|
1536
|
+
*/
|
|
1537
|
+
function matchesPattern(name, pattern) {
|
|
1538
|
+
if (pattern.startsWith("*.")) {
|
|
1539
|
+
const ext = pattern.slice(1);
|
|
1540
|
+
return name.endsWith(ext);
|
|
1541
|
+
}
|
|
1542
|
+
return name === pattern;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Check if a filename should be ignored based on patterns.
|
|
1546
|
+
*/
|
|
1547
|
+
function shouldIgnore(name, patterns) {
|
|
1548
|
+
return patterns.some((pattern) => matchesPattern(name, pattern));
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Recursively collect all files from a directory.
|
|
1552
|
+
*
|
|
1553
|
+
* @param dir - Current directory being scanned
|
|
1554
|
+
* @param rootExclude - Entries to exclude at root level only (config, metadata dir)
|
|
1555
|
+
* @param ignorePatterns - Patterns to ignore at all levels
|
|
1556
|
+
* @param root - The root directory (for computing relative paths)
|
|
1557
|
+
*/
|
|
1558
|
+
async function collectFiles(dir, rootExclude, ignorePatterns, root) {
|
|
1559
|
+
const configRoot = root ?? dir;
|
|
1560
|
+
const isRoot = configRoot === dir;
|
|
1561
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1562
|
+
const files = [];
|
|
1563
|
+
for (const entry of entries) {
|
|
1564
|
+
if (isRoot && rootExclude.includes(entry.name)) continue;
|
|
1565
|
+
if (shouldIgnore(entry.name, ignorePatterns)) {
|
|
1566
|
+
log.debug(`Ignoring: ${entry.name}`);
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const fullPath = join(dir, entry.name);
|
|
1570
|
+
if (entry.isDirectory()) {
|
|
1571
|
+
const nested = await collectFiles(fullPath, rootExclude, ignorePatterns, configRoot);
|
|
1572
|
+
files.push(...nested);
|
|
1573
|
+
} else if (entry.isFile()) {
|
|
1574
|
+
const contents = await readFile(fullPath, "utf8");
|
|
1575
|
+
const relativePath = relative(configRoot, fullPath);
|
|
1576
|
+
files.push({
|
|
1577
|
+
path: relativePath,
|
|
1578
|
+
contents
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return files;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Read a file if it exists, otherwise return undefined.
|
|
1586
|
+
*/
|
|
1587
|
+
async function readFileIfExists(path$1) {
|
|
1588
|
+
if (await fileExists(path$1)) return await readFile(path$1, "utf8");
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1457
1591
|
|
|
1458
1592
|
//#endregion
|
|
1459
1593
|
//#region src/commands/preset/init.ts
|
|
@@ -1461,11 +1595,9 @@ async function resolveConfigPath(inputPath) {
|
|
|
1461
1595
|
const PLATFORM_DETECTION_PATHS = {
|
|
1462
1596
|
opencode: [".opencode"],
|
|
1463
1597
|
claude: [".claude"],
|
|
1464
|
-
cursor: [".cursor"
|
|
1598
|
+
cursor: [".cursor"],
|
|
1465
1599
|
codex: [".codex"]
|
|
1466
1600
|
};
|
|
1467
|
-
/** Default path for new preset authoring */
|
|
1468
|
-
const DEFAULT_FILES_PATH = "files";
|
|
1469
1601
|
/** Default preset name when none specified */
|
|
1470
1602
|
const DEFAULT_PRESET_NAME$1 = "my-preset";
|
|
1471
1603
|
/**
|
|
@@ -1488,49 +1620,45 @@ async function detectPlatforms(directory) {
|
|
|
1488
1620
|
}
|
|
1489
1621
|
return detected;
|
|
1490
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Initialize a preset in a platform directory.
|
|
1625
|
+
*
|
|
1626
|
+
* Structure:
|
|
1627
|
+
* - platformDir/agentrules.json - preset config
|
|
1628
|
+
* - platformDir/* - platform files (added by user)
|
|
1629
|
+
* - platformDir/.agentrules/ - optional metadata folder (README, LICENSE, etc.)
|
|
1630
|
+
*/
|
|
1491
1631
|
async function initPreset(options) {
|
|
1492
|
-
const
|
|
1493
|
-
log.debug(`Initializing preset in: ${
|
|
1632
|
+
const platformDir = options.directory ?? process.cwd();
|
|
1633
|
+
log.debug(`Initializing preset in: ${platformDir}`);
|
|
1634
|
+
const inferredPlatform = getPlatformFromDir(basename(platformDir));
|
|
1635
|
+
const platform = normalizePlatform(options.platform ?? inferredPlatform ?? "opencode");
|
|
1494
1636
|
const name = normalizeName(options.name ?? DEFAULT_PRESET_NAME$1);
|
|
1495
1637
|
const title = options.title ?? toTitleCase(name);
|
|
1496
1638
|
const description = options.description ?? `${title} preset`;
|
|
1497
|
-
const platform = normalizePlatform(options.platform ?? "opencode");
|
|
1498
|
-
const detectedPath = options.detectedPath;
|
|
1499
1639
|
const license = options.license ?? "MIT";
|
|
1500
1640
|
log.debug(`Preset name: ${name}, platform: ${platform}`);
|
|
1501
|
-
const configPath = join(
|
|
1641
|
+
const configPath = join(platformDir, PRESET_CONFIG_FILENAME);
|
|
1502
1642
|
if (!options.force && await fileExists(configPath)) throw new Error(`${PRESET_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
|
|
1503
|
-
const defaultPath = PLATFORMS[platform].projectDir;
|
|
1504
|
-
const effectivePath = detectedPath ?? DEFAULT_FILES_PATH;
|
|
1505
1643
|
const preset = {
|
|
1506
1644
|
$schema: PRESET_SCHEMA_URL,
|
|
1507
1645
|
name,
|
|
1508
1646
|
title,
|
|
1509
1647
|
version: 1,
|
|
1510
1648
|
description,
|
|
1511
|
-
tags: ["// TODO: Replace - Tags help users discover your preset (e.g., typescript, react)"],
|
|
1512
|
-
features: ["// TODO: Replace - Features describe what your preset does (e.g., Built-in commands for common workflows)"],
|
|
1513
1649
|
license,
|
|
1514
1650
|
platform
|
|
1515
1651
|
};
|
|
1516
|
-
if (effectivePath !== defaultPath) preset.path = effectivePath;
|
|
1517
|
-
await mkdir(directory, { recursive: true });
|
|
1518
|
-
log.debug(`Created/verified directory: ${directory}`);
|
|
1519
|
-
const content = `${JSON.stringify(preset, null, 2)}\n`;
|
|
1520
|
-
await writeFile(configPath, content, "utf8");
|
|
1521
|
-
log.debug(`Wrote config file: ${configPath}`);
|
|
1522
1652
|
let createdDir;
|
|
1523
|
-
if (
|
|
1653
|
+
if (await directoryExists(platformDir)) log.debug(`Platform directory exists: ${platformDir}`);
|
|
1524
1654
|
else {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
else {
|
|
1529
|
-
await mkdir(fullPath, { recursive: true });
|
|
1530
|
-
createdDir = filesPath;
|
|
1531
|
-
log.debug(`Created files directory: ${filesPath}`);
|
|
1532
|
-
}
|
|
1655
|
+
await mkdir(platformDir, { recursive: true });
|
|
1656
|
+
createdDir = platformDir;
|
|
1657
|
+
log.debug(`Created platform directory: ${platformDir}`);
|
|
1533
1658
|
}
|
|
1659
|
+
const content = `${JSON.stringify(preset, null, 2)}\n`;
|
|
1660
|
+
await writeFile(configPath, content, "utf8");
|
|
1661
|
+
log.debug(`Wrote config file: ${configPath}`);
|
|
1534
1662
|
log.debug("Preset initialization complete.");
|
|
1535
1663
|
return {
|
|
1536
1664
|
configPath,
|
|
@@ -1562,17 +1690,49 @@ function check(schema) {
|
|
|
1562
1690
|
//#region src/commands/preset/init-interactive.ts
|
|
1563
1691
|
const DEFAULT_PRESET_NAME = "my-preset";
|
|
1564
1692
|
/**
|
|
1565
|
-
* Run interactive init flow with clack prompts
|
|
1693
|
+
* Run interactive init flow with clack prompts.
|
|
1694
|
+
*
|
|
1695
|
+
* If platformDir is provided, init directly in that directory.
|
|
1696
|
+
* Otherwise, detect platform directories and prompt user to select one.
|
|
1566
1697
|
*/
|
|
1567
1698
|
async function initInteractive(options) {
|
|
1568
|
-
const {
|
|
1699
|
+
const { baseDir, platformDir: explicitPlatformDir, name: nameOption, title: titleOption, description: descriptionOption, platform: platformOption, license: licenseOption } = options;
|
|
1569
1700
|
let { force } = options;
|
|
1570
1701
|
const defaultName = nameOption ?? DEFAULT_PRESET_NAME;
|
|
1571
1702
|
p.intro("Create a new preset");
|
|
1572
|
-
|
|
1703
|
+
let targetPlatformDir;
|
|
1704
|
+
let selectedPlatform;
|
|
1705
|
+
if (explicitPlatformDir) {
|
|
1706
|
+
targetPlatformDir = explicitPlatformDir;
|
|
1707
|
+
const dirName = explicitPlatformDir.split("/").pop() ?? explicitPlatformDir;
|
|
1708
|
+
selectedPlatform = platformOption ?? getPlatformFromDir(dirName) ?? "opencode";
|
|
1709
|
+
} else {
|
|
1710
|
+
const detected = await detectPlatforms(baseDir);
|
|
1711
|
+
const detectedMap = new Map(detected.map((d) => [d.id, d]));
|
|
1712
|
+
if (detected.length > 0) p.note(detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
|
|
1713
|
+
const defaultPlatform = platformOption ?? (detected.length > 0 ? detected[0].id : "opencode");
|
|
1714
|
+
const platformChoice = await p.select({
|
|
1715
|
+
message: "Platform",
|
|
1716
|
+
options: PLATFORM_IDS.map((id) => ({
|
|
1717
|
+
value: id,
|
|
1718
|
+
label: detectedMap.has(id) ? `${id} (detected)` : id,
|
|
1719
|
+
hint: detectedMap.get(id)?.path
|
|
1720
|
+
})),
|
|
1721
|
+
initialValue: defaultPlatform
|
|
1722
|
+
});
|
|
1723
|
+
if (p.isCancel(platformChoice)) {
|
|
1724
|
+
p.cancel("Cancelled");
|
|
1725
|
+
process.exit(0);
|
|
1726
|
+
}
|
|
1727
|
+
selectedPlatform = platformChoice;
|
|
1728
|
+
const detectedInfo = detectedMap.get(selectedPlatform);
|
|
1729
|
+
if (detectedInfo) targetPlatformDir = join(baseDir, detectedInfo.path);
|
|
1730
|
+
else targetPlatformDir = join(baseDir, PLATFORMS[selectedPlatform].projectDir);
|
|
1731
|
+
}
|
|
1732
|
+
const configPath = join(targetPlatformDir, PRESET_CONFIG_FILENAME);
|
|
1573
1733
|
if (!force && await fileExists(configPath)) {
|
|
1574
1734
|
const overwrite = await p.confirm({
|
|
1575
|
-
message: `${PRESET_CONFIG_FILENAME} already exists. Overwrite?`,
|
|
1735
|
+
message: `${PRESET_CONFIG_FILENAME} already exists in ${targetPlatformDir}. Overwrite?`,
|
|
1576
1736
|
initialValue: false
|
|
1577
1737
|
});
|
|
1578
1738
|
if (p.isCancel(overwrite) || !overwrite) {
|
|
@@ -1581,9 +1741,6 @@ async function initInteractive(options) {
|
|
|
1581
1741
|
}
|
|
1582
1742
|
force = true;
|
|
1583
1743
|
}
|
|
1584
|
-
const detected = await detectPlatforms(directory);
|
|
1585
|
-
const detectedMap = new Map(detected.map((d) => [d.id, d]));
|
|
1586
|
-
if (detected.length > 0) p.note(detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
|
|
1587
1744
|
const result = await p.group({
|
|
1588
1745
|
name: () => p.text({
|
|
1589
1746
|
message: "Preset name (slug)",
|
|
@@ -1609,18 +1766,6 @@ async function initInteractive(options) {
|
|
|
1609
1766
|
validate: check(descriptionSchema)
|
|
1610
1767
|
});
|
|
1611
1768
|
},
|
|
1612
|
-
platform: () => {
|
|
1613
|
-
const defaultPlatform = platformOption ?? (detected.length > 0 ? detected[0].id : "opencode");
|
|
1614
|
-
return p.select({
|
|
1615
|
-
message: "Platform",
|
|
1616
|
-
options: PLATFORM_IDS.map((id) => ({
|
|
1617
|
-
value: id,
|
|
1618
|
-
label: detectedMap.has(id) ? `${id} (detected)` : id,
|
|
1619
|
-
hint: detectedMap.get(id)?.path
|
|
1620
|
-
})),
|
|
1621
|
-
initialValue: defaultPlatform
|
|
1622
|
-
});
|
|
1623
|
-
},
|
|
1624
1769
|
license: async () => {
|
|
1625
1770
|
const defaultLicense = licenseOption ?? "MIT";
|
|
1626
1771
|
const choice = await p.select({
|
|
@@ -1656,14 +1801,12 @@ async function initInteractive(options) {
|
|
|
1656
1801
|
p.cancel("Cancelled");
|
|
1657
1802
|
return process.exit(0);
|
|
1658
1803
|
} });
|
|
1659
|
-
const detectedPath = detectedMap.get(result.platform)?.path;
|
|
1660
1804
|
const initOptions = {
|
|
1661
|
-
directory,
|
|
1805
|
+
directory: targetPlatformDir,
|
|
1662
1806
|
name: result.name,
|
|
1663
1807
|
title: result.title,
|
|
1664
1808
|
description: result.description,
|
|
1665
|
-
platform:
|
|
1666
|
-
detectedPath,
|
|
1809
|
+
platform: selectedPlatform,
|
|
1667
1810
|
license: result.license,
|
|
1668
1811
|
force
|
|
1669
1812
|
};
|
|
@@ -1727,11 +1870,16 @@ async function validatePreset(options) {
|
|
|
1727
1870
|
const platform = preset.platform;
|
|
1728
1871
|
log.debug(`Checking platform: ${platform}`);
|
|
1729
1872
|
if (isSupportedPlatform(platform)) {
|
|
1730
|
-
const
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1873
|
+
const dirName = basename(presetDir);
|
|
1874
|
+
const isInProjectMode = isPlatformDir(dirName);
|
|
1875
|
+
if (isInProjectMode) log.debug(`In-project mode: files expected in ${presetDir}`);
|
|
1876
|
+
else {
|
|
1877
|
+
const filesPath = preset.path ?? PLATFORMS[platform].projectDir;
|
|
1878
|
+
const filesDir = join(presetDir, filesPath);
|
|
1879
|
+
const filesExists = await directoryExists(filesDir);
|
|
1880
|
+
log.debug(`Standalone mode: files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
|
|
1881
|
+
if (!filesExists) errors.push(`Files directory not found: ${filesPath}`);
|
|
1882
|
+
}
|
|
1735
1883
|
} else {
|
|
1736
1884
|
errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
1737
1885
|
log.debug(`Platform "${platform}" is not supported`);
|
|
@@ -1762,9 +1910,6 @@ async function validatePreset(options) {
|
|
|
1762
1910
|
|
|
1763
1911
|
//#endregion
|
|
1764
1912
|
//#region src/commands/publish.ts
|
|
1765
|
-
const INSTALL_FILENAME$1 = "INSTALL.txt";
|
|
1766
|
-
const README_FILENAME$1 = "README.md";
|
|
1767
|
-
const LICENSE_FILENAME$1 = "LICENSE.md";
|
|
1768
1913
|
/** Maximum size per bundle in bytes (1MB) */
|
|
1769
1914
|
const MAX_BUNDLE_SIZE_BYTES = 1 * 1024 * 1024;
|
|
1770
1915
|
/**
|
|
@@ -1782,12 +1927,12 @@ async function publish(options = {}) {
|
|
|
1782
1927
|
const { path: path$1, version: version$1, dryRun = false } = options;
|
|
1783
1928
|
log.debug(`Publishing preset from path: ${path$1 ?? process.cwd()}${dryRun ? " (dry run)" : ""}`);
|
|
1784
1929
|
const ctx = useAppContext();
|
|
1785
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1786
1930
|
if (!(dryRun || ctx.isLoggedIn && ctx.credentials)) {
|
|
1787
|
-
|
|
1931
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
1932
|
+
log.error(error$2);
|
|
1788
1933
|
return {
|
|
1789
1934
|
success: false,
|
|
1790
|
-
error:
|
|
1935
|
+
error: error$2
|
|
1791
1936
|
};
|
|
1792
1937
|
}
|
|
1793
1938
|
if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
|
|
@@ -1807,15 +1952,15 @@ async function publish(options = {}) {
|
|
|
1807
1952
|
spinner$1.update("Loading preset...");
|
|
1808
1953
|
let presetInput;
|
|
1809
1954
|
try {
|
|
1810
|
-
presetInput = await loadPreset
|
|
1955
|
+
presetInput = await loadPreset(presetDir);
|
|
1811
1956
|
log.debug(`Loaded preset "${presetInput.slug}" for platform ${presetInput.config.platform}`);
|
|
1812
1957
|
} catch (error$2) {
|
|
1813
|
-
const
|
|
1958
|
+
const message = getErrorMessage(error$2);
|
|
1814
1959
|
spinner$1.fail("Failed to load preset");
|
|
1815
|
-
log.error(
|
|
1960
|
+
log.error(message);
|
|
1816
1961
|
return {
|
|
1817
1962
|
success: false,
|
|
1818
|
-
error:
|
|
1963
|
+
error: message
|
|
1819
1964
|
};
|
|
1820
1965
|
}
|
|
1821
1966
|
spinner$1.update("Building bundle...");
|
|
@@ -1827,12 +1972,12 @@ async function publish(options = {}) {
|
|
|
1827
1972
|
});
|
|
1828
1973
|
log.debug(`Built publish input for ${publishInput.platform}`);
|
|
1829
1974
|
} catch (error$2) {
|
|
1830
|
-
const
|
|
1975
|
+
const message = getErrorMessage(error$2);
|
|
1831
1976
|
spinner$1.fail("Failed to build bundle");
|
|
1832
|
-
log.error(
|
|
1977
|
+
log.error(message);
|
|
1833
1978
|
return {
|
|
1834
1979
|
success: false,
|
|
1835
|
-
error:
|
|
1980
|
+
error: message
|
|
1836
1981
|
};
|
|
1837
1982
|
}
|
|
1838
1983
|
const inputJson = JSON.stringify(publishInput);
|
|
@@ -1912,79 +2057,9 @@ async function publish(options = {}) {
|
|
|
1912
2057
|
}
|
|
1913
2058
|
};
|
|
1914
2059
|
}
|
|
1915
|
-
/**
|
|
1916
|
-
* Load a preset from a directory
|
|
1917
|
-
*/
|
|
1918
|
-
async function loadPreset$1(presetDir) {
|
|
1919
|
-
const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
|
|
1920
|
-
if (!await fileExists(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
1921
|
-
const configRaw = await readFile(configPath, "utf8");
|
|
1922
|
-
let configJson;
|
|
1923
|
-
try {
|
|
1924
|
-
configJson = JSON.parse(configRaw);
|
|
1925
|
-
} catch {
|
|
1926
|
-
throw new Error(`Invalid JSON in ${configPath}`);
|
|
1927
|
-
}
|
|
1928
|
-
const configObj = configJson;
|
|
1929
|
-
const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
|
|
1930
|
-
const config = validatePresetConfig(configJson, identifier);
|
|
1931
|
-
const slug = config.name;
|
|
1932
|
-
const installPath = join(presetDir, INSTALL_FILENAME$1);
|
|
1933
|
-
const installMessage = await readFileIfExists$1(installPath);
|
|
1934
|
-
const readmePath = join(presetDir, README_FILENAME$1);
|
|
1935
|
-
const readmeContent = await readFileIfExists$1(readmePath);
|
|
1936
|
-
const licensePath = join(presetDir, LICENSE_FILENAME$1);
|
|
1937
|
-
const licenseContent = await readFileIfExists$1(licensePath);
|
|
1938
|
-
const filesPath = config.path ?? PLATFORMS[config.platform].projectDir;
|
|
1939
|
-
const filesDir = join(presetDir, filesPath);
|
|
1940
|
-
if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir} (referenced in ${configPath})`);
|
|
1941
|
-
const files = await collectFiles$1(filesDir);
|
|
1942
|
-
if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
|
|
1943
|
-
return {
|
|
1944
|
-
slug,
|
|
1945
|
-
config,
|
|
1946
|
-
files,
|
|
1947
|
-
installMessage,
|
|
1948
|
-
readmeContent,
|
|
1949
|
-
licenseContent
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
/**
|
|
1953
|
-
* Recursively collect all files from a directory
|
|
1954
|
-
*/
|
|
1955
|
-
async function collectFiles$1(dir, baseDir) {
|
|
1956
|
-
const root = baseDir ?? dir;
|
|
1957
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
1958
|
-
const files = [];
|
|
1959
|
-
for (const entry of entries) {
|
|
1960
|
-
const fullPath = join(dir, entry.name);
|
|
1961
|
-
if (entry.isDirectory()) {
|
|
1962
|
-
const nested = await collectFiles$1(fullPath, root);
|
|
1963
|
-
files.push(...nested);
|
|
1964
|
-
} else if (entry.isFile()) {
|
|
1965
|
-
const contents = await readFile(fullPath, "utf8");
|
|
1966
|
-
const relativePath = relative(root, fullPath);
|
|
1967
|
-
files.push({
|
|
1968
|
-
path: relativePath,
|
|
1969
|
-
contents
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
return files;
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Read a file if it exists, otherwise return undefined
|
|
1977
|
-
*/
|
|
1978
|
-
async function readFileIfExists$1(path$1) {
|
|
1979
|
-
if (await fileExists(path$1)) return await readFile(path$1, "utf8");
|
|
1980
|
-
return;
|
|
1981
|
-
}
|
|
1982
2060
|
|
|
1983
2061
|
//#endregion
|
|
1984
2062
|
//#region src/commands/registry/build.ts
|
|
1985
|
-
const INSTALL_FILENAME = "INSTALL.txt";
|
|
1986
|
-
const README_FILENAME = "README.md";
|
|
1987
|
-
const LICENSE_FILENAME = "LICENSE.md";
|
|
1988
2063
|
async function buildRegistry(options) {
|
|
1989
2064
|
const inputDir = options.input;
|
|
1990
2065
|
const outputDir = options.out ?? null;
|
|
@@ -2060,61 +2135,6 @@ async function discoverPresetDirs(inputDir) {
|
|
|
2060
2135
|
await searchDir(inputDir, 0);
|
|
2061
2136
|
return presetDirs.sort();
|
|
2062
2137
|
}
|
|
2063
|
-
async function loadPreset(presetDir) {
|
|
2064
|
-
const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
|
|
2065
|
-
const configRaw = await readFile(configPath, "utf8");
|
|
2066
|
-
let configJson;
|
|
2067
|
-
try {
|
|
2068
|
-
configJson = JSON.parse(configRaw);
|
|
2069
|
-
} catch {
|
|
2070
|
-
throw new Error(`Invalid JSON in ${configPath}`);
|
|
2071
|
-
}
|
|
2072
|
-
const config = validatePresetConfig(configJson, basename(presetDir));
|
|
2073
|
-
const slug = config.name;
|
|
2074
|
-
const installPath = join(presetDir, INSTALL_FILENAME);
|
|
2075
|
-
const installMessage = await readFileIfExists(installPath);
|
|
2076
|
-
const readmePath = join(presetDir, README_FILENAME);
|
|
2077
|
-
const readmeContent = await readFileIfExists(readmePath);
|
|
2078
|
-
const licensePath = join(presetDir, LICENSE_FILENAME);
|
|
2079
|
-
const licenseContent = await readFileIfExists(licensePath);
|
|
2080
|
-
const filesPath = config.path ?? PLATFORMS[config.platform].projectDir;
|
|
2081
|
-
const filesDir = join(presetDir, filesPath);
|
|
2082
|
-
if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir} (referenced in ${configPath})`);
|
|
2083
|
-
const files = await collectFiles(filesDir);
|
|
2084
|
-
if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
|
|
2085
|
-
return {
|
|
2086
|
-
slug,
|
|
2087
|
-
config,
|
|
2088
|
-
files,
|
|
2089
|
-
installMessage,
|
|
2090
|
-
readmeContent,
|
|
2091
|
-
licenseContent
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2094
|
-
async function collectFiles(dir, baseDir) {
|
|
2095
|
-
const root = baseDir ?? dir;
|
|
2096
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
2097
|
-
const files = [];
|
|
2098
|
-
for (const entry of entries) {
|
|
2099
|
-
const fullPath = join(dir, entry.name);
|
|
2100
|
-
if (entry.isDirectory()) {
|
|
2101
|
-
const nested = await collectFiles(fullPath, root);
|
|
2102
|
-
files.push(...nested);
|
|
2103
|
-
} else if (entry.isFile()) {
|
|
2104
|
-
const contents = await readFile(fullPath, "utf8");
|
|
2105
|
-
const relativePath = relative(root, fullPath);
|
|
2106
|
-
files.push({
|
|
2107
|
-
path: relativePath,
|
|
2108
|
-
contents
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
return files;
|
|
2113
|
-
}
|
|
2114
|
-
async function readFileIfExists(path$1) {
|
|
2115
|
-
if (await fileExists(path$1)) return await readFile(path$1, "utf8");
|
|
2116
|
-
return;
|
|
2117
|
-
}
|
|
2118
2138
|
|
|
2119
2139
|
//#endregion
|
|
2120
2140
|
//#region src/commands/unpublish.ts
|
|
@@ -2146,12 +2166,12 @@ async function unpublish(options) {
|
|
|
2146
2166
|
}
|
|
2147
2167
|
log.debug(`Unpublishing preset: ${slug}.${platform}@${version$1}`);
|
|
2148
2168
|
const ctx = useAppContext();
|
|
2149
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
2150
2169
|
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2151
|
-
|
|
2170
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
2171
|
+
log.error(error$2);
|
|
2152
2172
|
return {
|
|
2153
2173
|
success: false,
|
|
2154
|
-
error:
|
|
2174
|
+
error: error$2
|
|
2155
2175
|
};
|
|
2156
2176
|
}
|
|
2157
2177
|
log.debug(`Authenticated, unpublishing from ${ctx.registry.url}`);
|
|
@@ -2194,7 +2214,7 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
|
|
|
2194
2214
|
url: actionOpts.url
|
|
2195
2215
|
});
|
|
2196
2216
|
} catch (error$2) {
|
|
2197
|
-
log.debug(`Failed to init context: ${
|
|
2217
|
+
log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
|
|
2198
2218
|
}
|
|
2199
2219
|
}).showHelpAfterError();
|
|
2200
2220
|
program.command("add <preset>").description("Download and install a preset from the registry").option("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").option("-V, --version <version>", "Install a specific version").option("-r, --registry <alias>", "Use a specific registry alias").option("-g, --global", "Install to global directory").option("--dir <path>", "Install to a custom directory").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Alias for --force").option("--dry-run", "Preview changes without writing").option("--skip-conflicts", "Skip conflicting files").action(handle(async (preset, options) => {
|
|
@@ -2250,7 +2270,7 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2250
2270
|
const useInteractive = !options.yes && process.stdin.isTTY;
|
|
2251
2271
|
if (useInteractive) {
|
|
2252
2272
|
const result$1 = await initInteractive({
|
|
2253
|
-
|
|
2273
|
+
baseDir: targetDir,
|
|
2254
2274
|
name: options.name ?? defaultName,
|
|
2255
2275
|
title: options.title,
|
|
2256
2276
|
description: options.description,
|
|
@@ -2262,23 +2282,34 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2262
2282
|
log.print(`\n${ui.header("Directory created")}`);
|
|
2263
2283
|
log.print(ui.list([ui.path(result$1.createdDir)]));
|
|
2264
2284
|
}
|
|
2265
|
-
const nextSteps$1 = [
|
|
2266
|
-
|
|
2267
|
-
|
|
2285
|
+
const nextSteps$1 = [
|
|
2286
|
+
"Add your config files to the platform directory",
|
|
2287
|
+
"Add tags (required) and features (recommended) to agentrules.json",
|
|
2288
|
+
`Run ${ui.command("agentrules publish")} to publish your preset`
|
|
2289
|
+
];
|
|
2268
2290
|
log.print(`\n${ui.header("Next steps")}`);
|
|
2269
2291
|
log.print(ui.numberedList(nextSteps$1));
|
|
2270
2292
|
return;
|
|
2271
2293
|
}
|
|
2272
|
-
const
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2294
|
+
const targetDirName = basename(targetDir);
|
|
2295
|
+
const targetIsPlatformDir = getPlatformFromDir(targetDirName);
|
|
2296
|
+
let platformDir;
|
|
2297
|
+
let platform;
|
|
2298
|
+
if (targetIsPlatformDir) {
|
|
2299
|
+
platformDir = targetDir;
|
|
2300
|
+
platform = options.platform ?? targetIsPlatformDir;
|
|
2301
|
+
} else {
|
|
2302
|
+
const detected = await detectPlatforms(targetDir);
|
|
2303
|
+
platform = options.platform ?? detected[0]?.id ?? "opencode";
|
|
2304
|
+
const detectedPath = detected.find((d) => d.id === platform)?.path;
|
|
2305
|
+
platformDir = detectedPath ? join(targetDir, detectedPath) : join(targetDir, PLATFORMS[platform].projectDir);
|
|
2306
|
+
}
|
|
2275
2307
|
const result = await initPreset({
|
|
2276
|
-
directory:
|
|
2308
|
+
directory: platformDir,
|
|
2277
2309
|
name: options.name ?? defaultName,
|
|
2278
2310
|
title: options.title,
|
|
2279
2311
|
description: options.description,
|
|
2280
2312
|
platform,
|
|
2281
|
-
detectedPath,
|
|
2282
2313
|
license: options.license,
|
|
2283
2314
|
force: options.force
|
|
2284
2315
|
});
|
|
@@ -2287,9 +2318,11 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2287
2318
|
log.print(`\n${ui.header("Directory created")}`);
|
|
2288
2319
|
log.print(ui.list([ui.path(result.createdDir)]));
|
|
2289
2320
|
}
|
|
2290
|
-
const nextSteps = [
|
|
2291
|
-
|
|
2292
|
-
|
|
2321
|
+
const nextSteps = [
|
|
2322
|
+
"Add your config files to the platform directory",
|
|
2323
|
+
"Add tags (required) and features (recommended) to agentrules.json",
|
|
2324
|
+
`Run ${ui.command("agentrules publish")} to publish your preset`
|
|
2325
|
+
];
|
|
2293
2326
|
log.print(`\n${ui.header("Next steps")}`);
|
|
2294
2327
|
log.print(ui.numberedList(nextSteps));
|
|
2295
2328
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentrules/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"author": "Brian Cheung <bcheung.dev@gmail.com> (https://github.com/bcheung)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://agentrules.directory",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"clean": "rm -rf node_modules dist .turbo"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@agentrules/core": "0.0.
|
|
51
|
+
"@agentrules/core": "0.0.9",
|
|
52
52
|
"@clack/prompts": "^0.11.0",
|
|
53
53
|
"chalk": "^5.4.1",
|
|
54
54
|
"commander": "^12.1.0",
|