@agentrules/cli 0.0.10 → 0.0.12
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 +78 -16
- package/dist/index.js +422 -258
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ agentrules add <preset> --platform <platform> [options]
|
|
|
34
34
|
| `-g, --global` | Install to global config directory |
|
|
35
35
|
| `--dir <path>` | Install to a custom directory |
|
|
36
36
|
| `-r, --registry <alias>` | Use a specific registry |
|
|
37
|
-
| `-f, --force` | Overwrite existing files |
|
|
37
|
+
| `-f, --force` | Overwrite existing files (backs up originals to `.bak`) |
|
|
38
|
+
| `--no-backup` | Don't backup files before overwriting (use with `--force`) |
|
|
38
39
|
| `--dry-run` | Preview changes without writing |
|
|
39
40
|
| `--skip-conflicts` | Skip files that already exist |
|
|
40
41
|
|
|
@@ -63,7 +64,7 @@ agentrules add agentic-dev-starter --platform opencode --dry-run
|
|
|
63
64
|
|
|
64
65
|
### `agentrules init [directory]`
|
|
65
66
|
|
|
66
|
-
Initialize a
|
|
67
|
+
Initialize a preset config in a platform directory. The command guides you through the required fields for publishing.
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
agentrules init [directory] [options]
|
|
@@ -73,7 +74,7 @@ agentrules init [directory] [options]
|
|
|
73
74
|
|
|
74
75
|
| Option | Description |
|
|
75
76
|
|--------|-------------|
|
|
76
|
-
| `-n, --name <name>` | Preset name (default:
|
|
77
|
+
| `-n, --name <name>` | Preset name (default: `my-preset`) |
|
|
77
78
|
| `-t, --title <title>` | Display title |
|
|
78
79
|
| `--description <text>` | Preset description |
|
|
79
80
|
| `-p, --platform <platform>` | Target platform |
|
|
@@ -84,30 +85,69 @@ agentrules init [directory] [options]
|
|
|
84
85
|
**Examples:**
|
|
85
86
|
|
|
86
87
|
```bash
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
cd my-preset
|
|
90
|
-
|
|
91
|
-
# Initialize in current directory
|
|
88
|
+
# Initialize in your existing platform directory
|
|
89
|
+
cd .opencode
|
|
92
90
|
agentrules init
|
|
93
91
|
|
|
94
|
-
#
|
|
95
|
-
agentrules init
|
|
92
|
+
# Initialize in a specific platform directory
|
|
93
|
+
agentrules init .claude
|
|
96
94
|
|
|
97
95
|
# Accept all defaults, skip prompts
|
|
98
|
-
agentrules init
|
|
96
|
+
agentrules init .opencode --yes
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
After running `init`, your preset structure is:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
.opencode/
|
|
103
|
+
├── agentrules.json # Preset config (created by init)
|
|
104
|
+
├── AGENTS.md # Your config files (included in bundle)
|
|
105
|
+
├── commands/
|
|
106
|
+
│ └── review.md
|
|
107
|
+
└── .agentrules/ # Optional metadata folder
|
|
108
|
+
├── README.md # Shown on registry page
|
|
109
|
+
├── LICENSE.md # Full license text
|
|
110
|
+
└── INSTALL.txt # Shown after install
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Preset Config Fields
|
|
114
|
+
|
|
115
|
+
| Field | Required | Description |
|
|
116
|
+
|-------|----------|-------------|
|
|
117
|
+
| `name` | Yes | URL-safe identifier (lowercase, hyphens) |
|
|
118
|
+
| `title` | Yes | Display name |
|
|
119
|
+
| `description` | Yes | Short description (max 500 chars) |
|
|
120
|
+
| `license` | Yes | SPDX license identifier (e.g., `MIT`) |
|
|
121
|
+
| `platform` | Yes | Target platform: `opencode`, `claude`, `cursor`, `codex` |
|
|
122
|
+
| `version` | No | Major version (default: 1) |
|
|
123
|
+
| `tags` | No | Up to 10 tags for discoverability |
|
|
124
|
+
| `features` | No | Up to 5 key features to highlight |
|
|
125
|
+
| `ignore` | No | Additional patterns to exclude from bundle |
|
|
126
|
+
|
|
127
|
+
### Auto-Excluded Files
|
|
128
|
+
|
|
129
|
+
These files are automatically excluded from bundles:
|
|
130
|
+
- `node_modules/`, `.git/`, `.DS_Store`
|
|
131
|
+
- Lock files: `package-lock.json`, `bun.lockb`, `pnpm-lock.yaml`, `*.lock`
|
|
132
|
+
|
|
133
|
+
Use the `ignore` field for additional exclusions:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"ignore": ["*.log", "test-fixtures", "*.tmp"]
|
|
138
|
+
}
|
|
99
139
|
```
|
|
100
140
|
|
|
101
141
|
### `agentrules validate [path]`
|
|
102
142
|
|
|
103
|
-
Validate a preset configuration.
|
|
143
|
+
Validate a preset configuration before publishing.
|
|
104
144
|
|
|
105
145
|
```bash
|
|
106
146
|
# Validate current directory
|
|
107
147
|
agentrules validate
|
|
108
148
|
|
|
109
149
|
# Validate a specific path
|
|
110
|
-
agentrules validate
|
|
150
|
+
agentrules validate .opencode
|
|
111
151
|
```
|
|
112
152
|
|
|
113
153
|
---
|
|
@@ -161,14 +201,36 @@ agentrules publish --dry-run
|
|
|
161
201
|
|
|
162
202
|
**Versioning:** Presets use `MAJOR.MINOR` versioning. You set the major version, and the registry auto-increments the minor version on each publish.
|
|
163
203
|
|
|
164
|
-
### `agentrules unpublish <
|
|
204
|
+
### `agentrules unpublish <preset>`
|
|
205
|
+
|
|
206
|
+
Remove a specific version of a preset from the registry. Requires authentication.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
agentrules unpublish <preset> [options]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Options:**
|
|
213
|
+
|
|
214
|
+
| Option | Description |
|
|
215
|
+
|--------|-------------|
|
|
216
|
+
| `-p, --platform <platform>` | Target platform (if not in preset string) |
|
|
217
|
+
| `-V, --version <version>` | Version to unpublish (if not in preset string) |
|
|
165
218
|
|
|
166
|
-
|
|
219
|
+
**Examples:**
|
|
167
220
|
|
|
168
221
|
```bash
|
|
169
|
-
|
|
222
|
+
# Full format: slug.platform@version
|
|
223
|
+
agentrules unpublish my-preset.opencode@1.0
|
|
224
|
+
|
|
225
|
+
# With flags
|
|
226
|
+
agentrules unpublish my-preset --platform opencode --version 1.0
|
|
227
|
+
|
|
228
|
+
# Mixed: version in string, platform as flag
|
|
229
|
+
agentrules unpublish my-preset@1.0 --platform opencode
|
|
170
230
|
```
|
|
171
231
|
|
|
232
|
+
**Note:** Unpublished versions cannot be republished with the same version number.
|
|
233
|
+
|
|
172
234
|
---
|
|
173
235
|
|
|
174
236
|
## Registry Management
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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";
|
|
11
|
-
import { access, constants as constants$1, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
11
|
+
import { access, constants as constants$1, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import * as p from "@clack/prompts";
|
|
14
14
|
|
|
@@ -221,6 +221,14 @@ function fileStatus(status, filePath, options = {}) {
|
|
|
221
221
|
return `${config.style(config.symbol)} ${config.style(pad(label, 14))} ${filePath}`;
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
|
+
* Format a backup status line
|
|
225
|
+
* e.g., "↪ backed up .opencode/AGENT_RULES.md → .opencode/AGENT_RULES.md.bak"
|
|
226
|
+
*/
|
|
227
|
+
function backupStatus(originalPath, backupPath, options = {}) {
|
|
228
|
+
const label = options.dryRun ? "would backup" : "backed up";
|
|
229
|
+
return `${theme.info("↪")} ${theme.info(pad(label, 14))} ${originalPath} ${symbols.arrow} ${backupPath}`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
224
232
|
* Step indicator for multi-step operations
|
|
225
233
|
* e.g., "[1/3] Fetching registry..."
|
|
226
234
|
*/
|
|
@@ -335,6 +343,7 @@ const ui = {
|
|
|
335
343
|
warning,
|
|
336
344
|
info: info$1,
|
|
337
345
|
fileStatus,
|
|
346
|
+
backupStatus,
|
|
338
347
|
step,
|
|
339
348
|
brand,
|
|
340
349
|
banner,
|
|
@@ -575,6 +584,19 @@ function formatPollingError(err) {
|
|
|
575
584
|
return "Lost connection to server. Please try again.";
|
|
576
585
|
}
|
|
577
586
|
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/lib/errors.ts
|
|
589
|
+
/**
|
|
590
|
+
* Error utilities
|
|
591
|
+
*/
|
|
592
|
+
/**
|
|
593
|
+
* Safely extract an error message from any error type.
|
|
594
|
+
*/
|
|
595
|
+
function getErrorMessage(error$2) {
|
|
596
|
+
if (error$2 instanceof Error) return error$2.message;
|
|
597
|
+
return String(error$2);
|
|
598
|
+
}
|
|
599
|
+
|
|
578
600
|
//#endregion
|
|
579
601
|
//#region src/lib/api/presets.ts
|
|
580
602
|
/**
|
|
@@ -608,10 +630,9 @@ async function publishPreset(baseUrl, token, input) {
|
|
|
608
630
|
data
|
|
609
631
|
};
|
|
610
632
|
} catch (error$2) {
|
|
611
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
612
633
|
return {
|
|
613
634
|
success: false,
|
|
614
|
-
error: `Failed to connect to registry: ${
|
|
635
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
615
636
|
};
|
|
616
637
|
}
|
|
617
638
|
}
|
|
@@ -640,10 +661,9 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
|
|
|
640
661
|
data
|
|
641
662
|
};
|
|
642
663
|
} catch (error$2) {
|
|
643
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
644
664
|
return {
|
|
645
665
|
success: false,
|
|
646
|
-
error: `Failed to connect to registry: ${
|
|
666
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
647
667
|
};
|
|
648
668
|
}
|
|
649
669
|
}
|
|
@@ -673,6 +693,7 @@ async function fetchSession(baseUrl, token) {
|
|
|
673
693
|
|
|
674
694
|
//#endregion
|
|
675
695
|
//#region src/lib/config.ts
|
|
696
|
+
/** Directory for CLI configuration and credentials (e.g., ~/.agentrules/) */
|
|
676
697
|
const CONFIG_DIRNAME = ".agentrules";
|
|
677
698
|
const CONFIG_FILENAME = "config.json";
|
|
678
699
|
const CONFIG_HOME_ENV = "AGENT_RULES_HOME";
|
|
@@ -867,7 +888,7 @@ async function loadStore() {
|
|
|
867
888
|
const store = JSON.parse(raw);
|
|
868
889
|
return store;
|
|
869
890
|
} catch (error$2) {
|
|
870
|
-
log.debug(`Failed to load credentials: ${
|
|
891
|
+
log.debug(`Failed to load credentials: ${getErrorMessage(error$2)}`);
|
|
871
892
|
return {};
|
|
872
893
|
}
|
|
873
894
|
}
|
|
@@ -981,7 +1002,7 @@ async function createAppContext(options = {}) {
|
|
|
981
1002
|
log.debug("Saved fetched user info to credentials");
|
|
982
1003
|
}
|
|
983
1004
|
} catch (error$2) {
|
|
984
|
-
log.debug(`Failed to fetch user info: ${
|
|
1005
|
+
log.debug(`Failed to fetch user info: ${getErrorMessage(error$2)}`);
|
|
985
1006
|
}
|
|
986
1007
|
}
|
|
987
1008
|
log.debug(`App context loaded: isLoggedIn=${isLoggedIn}, user=${user?.name ?? "none"}`);
|
|
@@ -1018,10 +1039,11 @@ function resolveRegistry(config, alias) {
|
|
|
1018
1039
|
}
|
|
1019
1040
|
let globalContext = null;
|
|
1020
1041
|
/**
|
|
1021
|
-
* Gets the global app context
|
|
1022
|
-
*
|
|
1042
|
+
* Gets the global app context.
|
|
1043
|
+
* Throws if context has not been initialized via initAppContext().
|
|
1023
1044
|
*/
|
|
1024
1045
|
function useAppContext() {
|
|
1046
|
+
if (!globalContext) throw new Error("App context not initialized");
|
|
1025
1047
|
return globalContext;
|
|
1026
1048
|
}
|
|
1027
1049
|
/**
|
|
@@ -1042,7 +1064,6 @@ const CLIENT_ID = "agentrules-cli";
|
|
|
1042
1064
|
async function login(options = {}) {
|
|
1043
1065
|
const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
|
|
1044
1066
|
const ctx = useAppContext();
|
|
1045
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1046
1067
|
const { url: registryUrl } = ctx.registry;
|
|
1047
1068
|
log.debug(`Authenticating with ${registryUrl}`);
|
|
1048
1069
|
try {
|
|
@@ -1106,10 +1127,9 @@ async function login(options = {}) {
|
|
|
1106
1127
|
} : void 0
|
|
1107
1128
|
};
|
|
1108
1129
|
} catch (error$2) {
|
|
1109
|
-
const message = error$2 instanceof Error ? error$2.message : String(error$2);
|
|
1110
1130
|
return {
|
|
1111
1131
|
success: false,
|
|
1112
|
-
error:
|
|
1132
|
+
error: getErrorMessage(error$2)
|
|
1113
1133
|
};
|
|
1114
1134
|
}
|
|
1115
1135
|
}
|
|
@@ -1152,7 +1172,6 @@ async function logout(options = {}) {
|
|
|
1152
1172
|
};
|
|
1153
1173
|
}
|
|
1154
1174
|
const ctx = useAppContext();
|
|
1155
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1156
1175
|
const { url: registryUrl } = ctx.registry;
|
|
1157
1176
|
const hadCredentials = ctx.credentials !== null;
|
|
1158
1177
|
if (hadCredentials) {
|
|
@@ -1172,7 +1191,6 @@ async function logout(options = {}) {
|
|
|
1172
1191
|
*/
|
|
1173
1192
|
async function whoami() {
|
|
1174
1193
|
const ctx = useAppContext();
|
|
1175
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1176
1194
|
const { url: registryUrl } = ctx.registry;
|
|
1177
1195
|
return {
|
|
1178
1196
|
success: true,
|
|
@@ -1187,7 +1205,6 @@ async function whoami() {
|
|
|
1187
1205
|
//#region src/commands/preset/add.ts
|
|
1188
1206
|
async function addPreset(options) {
|
|
1189
1207
|
const ctx = useAppContext();
|
|
1190
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1191
1208
|
const { alias: registryAlias, url: registryUrl } = ctx.registry;
|
|
1192
1209
|
const dryRun = Boolean(options.dryRun);
|
|
1193
1210
|
const { slug, platform, version: version$1 } = parsePresetInput(options.preset, options.platform, options.version);
|
|
@@ -1201,6 +1218,7 @@ async function addPreset(options) {
|
|
|
1201
1218
|
const writeStats = await writeBundleFiles(bundle, target, {
|
|
1202
1219
|
force: Boolean(options.force),
|
|
1203
1220
|
skipConflicts: Boolean(options.skipConflicts),
|
|
1221
|
+
noBackup: Boolean(options.noBackup),
|
|
1204
1222
|
dryRun
|
|
1205
1223
|
});
|
|
1206
1224
|
return {
|
|
@@ -1208,6 +1226,7 @@ async function addPreset(options) {
|
|
|
1208
1226
|
bundle,
|
|
1209
1227
|
files: writeStats.files,
|
|
1210
1228
|
conflicts: writeStats.conflicts,
|
|
1229
|
+
backups: writeStats.backups,
|
|
1211
1230
|
targetRoot: target.root,
|
|
1212
1231
|
targetLabel: target.label,
|
|
1213
1232
|
registryAlias,
|
|
@@ -1290,20 +1309,13 @@ function resolveInstallTarget(platform, options) {
|
|
|
1290
1309
|
async function writeBundleFiles(bundle, target, behavior) {
|
|
1291
1310
|
const files = [];
|
|
1292
1311
|
const conflicts = [];
|
|
1312
|
+
const backups = [];
|
|
1293
1313
|
if (!behavior.dryRun) await mkdir(target.root, { recursive: true });
|
|
1294
1314
|
for (const file of bundle.files) {
|
|
1295
1315
|
const decoded = decodeBundledFile(file);
|
|
1296
1316
|
const data = Buffer.from(decoded);
|
|
1297
1317
|
await verifyBundledFileChecksum(file, data);
|
|
1298
1318
|
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
1319
|
const destination = destResult.path;
|
|
1308
1320
|
if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
|
|
1309
1321
|
const existing = await readExistingFile(destination);
|
|
@@ -1326,6 +1338,16 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1326
1338
|
continue;
|
|
1327
1339
|
}
|
|
1328
1340
|
if (behavior.force) {
|
|
1341
|
+
if (!behavior.noBackup) {
|
|
1342
|
+
const backupPath = `${destination}.bak`;
|
|
1343
|
+
const relativeBackupPath = `${relativePath}.bak`;
|
|
1344
|
+
if (!behavior.dryRun) await copyFile(destination, backupPath);
|
|
1345
|
+
backups.push({
|
|
1346
|
+
originalPath: relativePath,
|
|
1347
|
+
backupPath: relativeBackupPath
|
|
1348
|
+
});
|
|
1349
|
+
log.debug(`Backed up: ${relativePath} → ${relativeBackupPath}`);
|
|
1350
|
+
}
|
|
1329
1351
|
if (!behavior.dryRun) await writeFile(destination, data);
|
|
1330
1352
|
files.push({
|
|
1331
1353
|
path: relativePath,
|
|
@@ -1346,30 +1368,27 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1346
1368
|
}
|
|
1347
1369
|
return {
|
|
1348
1370
|
files,
|
|
1349
|
-
conflicts
|
|
1371
|
+
conflicts,
|
|
1372
|
+
backups
|
|
1350
1373
|
};
|
|
1351
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Compute destination path for a bundled file.
|
|
1377
|
+
*
|
|
1378
|
+
* Bundle files are stored with paths relative to the platform directory
|
|
1379
|
+
* (e.g., "AGENTS.md", "commands/test.md") and installed to:
|
|
1380
|
+
* - Project/custom: <root>/<projectDir>/<path> (e.g., .opencode/AGENTS.md)
|
|
1381
|
+
* - Global: <root>/<path> (e.g., ~/.config/opencode/AGENTS.md)
|
|
1382
|
+
*/
|
|
1352
1383
|
function computeDestinationPath(pathInput, target) {
|
|
1353
1384
|
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
|
-
};
|
|
1385
|
+
if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
|
|
1360
1386
|
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.`);
|
|
1387
|
+
if (target.mode === "global") relativePath = normalized;
|
|
1388
|
+
else relativePath = `${target.projectDir}/${normalized}`;
|
|
1367
1389
|
const destination = resolve(target.root, relativePath);
|
|
1368
1390
|
ensureWithinRoot(destination, target.root);
|
|
1369
|
-
return {
|
|
1370
|
-
skipped: false,
|
|
1371
|
-
path: destination
|
|
1372
|
-
};
|
|
1391
|
+
return { path: destination };
|
|
1373
1392
|
}
|
|
1374
1393
|
async function readExistingFile(pathname) {
|
|
1375
1394
|
try {
|
|
@@ -1431,6 +1450,22 @@ async function directoryExists(path$1) {
|
|
|
1431
1450
|
|
|
1432
1451
|
//#endregion
|
|
1433
1452
|
//#region src/lib/preset-utils.ts
|
|
1453
|
+
const INSTALL_FILENAME = "INSTALL.txt";
|
|
1454
|
+
const README_FILENAME = "README.md";
|
|
1455
|
+
const LICENSE_FILENAME = "LICENSE.md";
|
|
1456
|
+
/**
|
|
1457
|
+
* Files/directories that are always excluded from presets.
|
|
1458
|
+
* These are never useful in a preset bundle.
|
|
1459
|
+
*/
|
|
1460
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
1461
|
+
"node_modules",
|
|
1462
|
+
".git",
|
|
1463
|
+
".DS_Store",
|
|
1464
|
+
"*.lock",
|
|
1465
|
+
"package-lock.json",
|
|
1466
|
+
"bun.lockb",
|
|
1467
|
+
"pnpm-lock.yaml"
|
|
1468
|
+
];
|
|
1434
1469
|
/**
|
|
1435
1470
|
* Normalize a string to a valid preset slug (lowercase kebab-case)
|
|
1436
1471
|
*/
|
|
@@ -1454,6 +1489,128 @@ async function resolveConfigPath(inputPath) {
|
|
|
1454
1489
|
if (stats?.isDirectory()) return join(inputPath, PRESET_CONFIG_FILENAME);
|
|
1455
1490
|
return inputPath;
|
|
1456
1491
|
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Load a preset from a directory containing agentrules.json.
|
|
1494
|
+
*
|
|
1495
|
+
* Config in platform dir (e.g., .claude/agentrules.json):
|
|
1496
|
+
* - Preset files: siblings of config
|
|
1497
|
+
* - Metadata: .agentrules/ subfolder
|
|
1498
|
+
*
|
|
1499
|
+
* Config at repo root:
|
|
1500
|
+
* - Preset files: in .claude/ (or `path` from config)
|
|
1501
|
+
* - Metadata: .agentrules/ subfolder
|
|
1502
|
+
*/
|
|
1503
|
+
async function loadPreset(presetDir) {
|
|
1504
|
+
const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
|
|
1505
|
+
if (!await fileExists(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
1506
|
+
const configRaw = await readFile(configPath, "utf8");
|
|
1507
|
+
let configJson;
|
|
1508
|
+
try {
|
|
1509
|
+
configJson = JSON.parse(configRaw);
|
|
1510
|
+
} catch {
|
|
1511
|
+
throw new Error(`Invalid JSON in ${configPath}`);
|
|
1512
|
+
}
|
|
1513
|
+
const configObj = configJson;
|
|
1514
|
+
const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
|
|
1515
|
+
const config = validatePresetConfig(configJson, identifier);
|
|
1516
|
+
const slug = config.name;
|
|
1517
|
+
const dirName = basename(presetDir);
|
|
1518
|
+
const isConfigInPlatformDir = isPlatformDir(dirName);
|
|
1519
|
+
let filesDir;
|
|
1520
|
+
let metadataDir;
|
|
1521
|
+
if (isConfigInPlatformDir) {
|
|
1522
|
+
filesDir = presetDir;
|
|
1523
|
+
metadataDir = join(presetDir, AGENT_RULES_DIR);
|
|
1524
|
+
log.debug(`Config in platform dir: files in ${filesDir}, metadata in ${metadataDir}`);
|
|
1525
|
+
} else {
|
|
1526
|
+
const platformDir = config.path ?? PLATFORMS[config.platform].projectDir;
|
|
1527
|
+
filesDir = join(presetDir, platformDir);
|
|
1528
|
+
metadataDir = join(presetDir, AGENT_RULES_DIR);
|
|
1529
|
+
log.debug(`Config at repo root: files in ${filesDir}, metadata in ${metadataDir}`);
|
|
1530
|
+
if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in ${PRESET_CONFIG_FILENAME}.`);
|
|
1531
|
+
}
|
|
1532
|
+
let installMessage;
|
|
1533
|
+
let readmeContent;
|
|
1534
|
+
let licenseContent;
|
|
1535
|
+
if (await directoryExists(metadataDir)) {
|
|
1536
|
+
installMessage = await readFileIfExists(join(metadataDir, INSTALL_FILENAME));
|
|
1537
|
+
readmeContent = await readFileIfExists(join(metadataDir, README_FILENAME));
|
|
1538
|
+
licenseContent = await readFileIfExists(join(metadataDir, LICENSE_FILENAME));
|
|
1539
|
+
}
|
|
1540
|
+
const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...config.ignore ?? []];
|
|
1541
|
+
const rootExclude = [PRESET_CONFIG_FILENAME, AGENT_RULES_DIR];
|
|
1542
|
+
const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
|
|
1543
|
+
if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
|
|
1544
|
+
return {
|
|
1545
|
+
slug,
|
|
1546
|
+
config,
|
|
1547
|
+
files,
|
|
1548
|
+
installMessage,
|
|
1549
|
+
readmeContent,
|
|
1550
|
+
licenseContent
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Check if a filename matches an ignore pattern.
|
|
1555
|
+
* Supports:
|
|
1556
|
+
* - Exact match: "node_modules"
|
|
1557
|
+
* - Extension match: "*.lock"
|
|
1558
|
+
* - Prefix match: ".git*" (not implemented yet, keeping simple)
|
|
1559
|
+
*/
|
|
1560
|
+
function matchesPattern(name, pattern) {
|
|
1561
|
+
if (pattern.startsWith("*.")) {
|
|
1562
|
+
const ext = pattern.slice(1);
|
|
1563
|
+
return name.endsWith(ext);
|
|
1564
|
+
}
|
|
1565
|
+
return name === pattern;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Check if a filename should be ignored based on patterns.
|
|
1569
|
+
*/
|
|
1570
|
+
function shouldIgnore(name, patterns) {
|
|
1571
|
+
return patterns.some((pattern) => matchesPattern(name, pattern));
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Recursively collect all files from a directory.
|
|
1575
|
+
*
|
|
1576
|
+
* @param dir - Current directory being scanned
|
|
1577
|
+
* @param rootExclude - Entries to exclude at root level only (config, metadata dir)
|
|
1578
|
+
* @param ignorePatterns - Patterns to ignore at all levels
|
|
1579
|
+
* @param root - The root directory (for computing relative paths)
|
|
1580
|
+
*/
|
|
1581
|
+
async function collectFiles(dir, rootExclude, ignorePatterns, root) {
|
|
1582
|
+
const configRoot = root ?? dir;
|
|
1583
|
+
const isRoot = configRoot === dir;
|
|
1584
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1585
|
+
const files = [];
|
|
1586
|
+
for (const entry of entries) {
|
|
1587
|
+
if (isRoot && rootExclude.includes(entry.name)) continue;
|
|
1588
|
+
if (shouldIgnore(entry.name, ignorePatterns)) {
|
|
1589
|
+
log.debug(`Ignoring: ${entry.name}`);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const fullPath = join(dir, entry.name);
|
|
1593
|
+
if (entry.isDirectory()) {
|
|
1594
|
+
const nested = await collectFiles(fullPath, rootExclude, ignorePatterns, configRoot);
|
|
1595
|
+
files.push(...nested);
|
|
1596
|
+
} else if (entry.isFile()) {
|
|
1597
|
+
const contents = await readFile(fullPath, "utf8");
|
|
1598
|
+
const relativePath = relative(configRoot, fullPath);
|
|
1599
|
+
files.push({
|
|
1600
|
+
path: relativePath,
|
|
1601
|
+
contents
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return files;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Read a file if it exists, otherwise return undefined.
|
|
1609
|
+
*/
|
|
1610
|
+
async function readFileIfExists(path$1) {
|
|
1611
|
+
if (await fileExists(path$1)) return await readFile(path$1, "utf8");
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1457
1614
|
|
|
1458
1615
|
//#endregion
|
|
1459
1616
|
//#region src/commands/preset/init.ts
|
|
@@ -1461,11 +1618,9 @@ async function resolveConfigPath(inputPath) {
|
|
|
1461
1618
|
const PLATFORM_DETECTION_PATHS = {
|
|
1462
1619
|
opencode: [".opencode"],
|
|
1463
1620
|
claude: [".claude"],
|
|
1464
|
-
cursor: [".cursor"
|
|
1621
|
+
cursor: [".cursor"],
|
|
1465
1622
|
codex: [".codex"]
|
|
1466
1623
|
};
|
|
1467
|
-
/** Default path for new preset authoring */
|
|
1468
|
-
const DEFAULT_FILES_PATH = "files";
|
|
1469
1624
|
/** Default preset name when none specified */
|
|
1470
1625
|
const DEFAULT_PRESET_NAME$1 = "my-preset";
|
|
1471
1626
|
/**
|
|
@@ -1488,49 +1643,105 @@ async function detectPlatforms(directory) {
|
|
|
1488
1643
|
}
|
|
1489
1644
|
return detected;
|
|
1490
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Resolve the target platform directory for initialization.
|
|
1648
|
+
*
|
|
1649
|
+
* Detection order (deterministic):
|
|
1650
|
+
* 1. If targetDir itself is a platform directory (e.g., ".claude"), use it directly
|
|
1651
|
+
* 2. Otherwise, detect platform directories inside targetDir
|
|
1652
|
+
*
|
|
1653
|
+
* @param targetDir - The target directory (cwd or user-provided path)
|
|
1654
|
+
* @param platformOverride - Optional platform to use instead of detecting/inferring
|
|
1655
|
+
*/
|
|
1656
|
+
async function resolvePlatformDirectory(targetDir, platformOverride) {
|
|
1657
|
+
const targetDirName = basename(targetDir);
|
|
1658
|
+
const targetPlatform = getPlatformFromDir(targetDirName);
|
|
1659
|
+
if (targetPlatform) {
|
|
1660
|
+
const platform$1 = platformOverride ? normalizePlatform(platformOverride) : targetPlatform;
|
|
1661
|
+
return {
|
|
1662
|
+
platformDir: targetDir,
|
|
1663
|
+
platform: platform$1,
|
|
1664
|
+
isTargetPlatformDir: true,
|
|
1665
|
+
detected: []
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const detected = await detectPlatforms(targetDir);
|
|
1669
|
+
let platform;
|
|
1670
|
+
let platformDir;
|
|
1671
|
+
if (platformOverride) {
|
|
1672
|
+
platform = normalizePlatform(platformOverride);
|
|
1673
|
+
const detectedPath = detected.find((d) => d.id === platform)?.path;
|
|
1674
|
+
platformDir = detectedPath ? join(targetDir, detectedPath) : join(targetDir, PLATFORMS[platform].projectDir);
|
|
1675
|
+
} else if (detected.length > 0) {
|
|
1676
|
+
platform = detected[0].id;
|
|
1677
|
+
platformDir = join(targetDir, detected[0].path);
|
|
1678
|
+
} else {
|
|
1679
|
+
platform = "opencode";
|
|
1680
|
+
platformDir = join(targetDir, PLATFORMS.opencode.projectDir);
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
platformDir,
|
|
1684
|
+
platform,
|
|
1685
|
+
isTargetPlatformDir: false,
|
|
1686
|
+
detected
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Check if --platform flag is required for non-interactive mode.
|
|
1691
|
+
* Returns the reason if required, so CLI can show appropriate error.
|
|
1692
|
+
*/
|
|
1693
|
+
function requiresPlatformFlag(resolved) {
|
|
1694
|
+
if (resolved.isTargetPlatformDir) return { required: false };
|
|
1695
|
+
if (resolved.detected.length === 0) return {
|
|
1696
|
+
required: true,
|
|
1697
|
+
reason: "no_platforms"
|
|
1698
|
+
};
|
|
1699
|
+
if (resolved.detected.length > 1) return {
|
|
1700
|
+
required: true,
|
|
1701
|
+
reason: "multiple_platforms",
|
|
1702
|
+
platforms: resolved.detected.map((d) => d.id)
|
|
1703
|
+
};
|
|
1704
|
+
return { required: false };
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Initialize a preset in a platform directory.
|
|
1708
|
+
*
|
|
1709
|
+
* Structure:
|
|
1710
|
+
* - platformDir/agentrules.json - preset config
|
|
1711
|
+
* - platformDir/* - platform files (added by user)
|
|
1712
|
+
* - platformDir/.agentrules/ - optional metadata folder (README, LICENSE, etc.)
|
|
1713
|
+
*/
|
|
1491
1714
|
async function initPreset(options) {
|
|
1492
|
-
const
|
|
1493
|
-
log.debug(`Initializing preset in: ${
|
|
1715
|
+
const platformDir = options.directory ?? process.cwd();
|
|
1716
|
+
log.debug(`Initializing preset in: ${platformDir}`);
|
|
1717
|
+
const inferredPlatform = getPlatformFromDir(basename(platformDir));
|
|
1718
|
+
const platform = normalizePlatform(options.platform ?? inferredPlatform ?? "opencode");
|
|
1494
1719
|
const name = normalizeName(options.name ?? DEFAULT_PRESET_NAME$1);
|
|
1495
1720
|
const title = options.title ?? toTitleCase(name);
|
|
1496
1721
|
const description = options.description ?? `${title} preset`;
|
|
1497
|
-
const platform = normalizePlatform(options.platform ?? "opencode");
|
|
1498
|
-
const detectedPath = options.detectedPath;
|
|
1499
1722
|
const license = options.license ?? "MIT";
|
|
1500
1723
|
log.debug(`Preset name: ${name}, platform: ${platform}`);
|
|
1501
|
-
const configPath = join(
|
|
1724
|
+
const configPath = join(platformDir, PRESET_CONFIG_FILENAME);
|
|
1502
1725
|
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
1726
|
const preset = {
|
|
1506
1727
|
$schema: PRESET_SCHEMA_URL,
|
|
1507
1728
|
name,
|
|
1508
1729
|
title,
|
|
1509
1730
|
version: 1,
|
|
1510
1731
|
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
1732
|
license,
|
|
1514
1733
|
platform
|
|
1515
1734
|
};
|
|
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
1735
|
let createdDir;
|
|
1523
|
-
if (
|
|
1736
|
+
if (await directoryExists(platformDir)) log.debug(`Platform directory exists: ${platformDir}`);
|
|
1524
1737
|
else {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
else {
|
|
1529
|
-
await mkdir(fullPath, { recursive: true });
|
|
1530
|
-
createdDir = filesPath;
|
|
1531
|
-
log.debug(`Created files directory: ${filesPath}`);
|
|
1532
|
-
}
|
|
1738
|
+
await mkdir(platformDir, { recursive: true });
|
|
1739
|
+
createdDir = platformDir;
|
|
1740
|
+
log.debug(`Created platform directory: ${platformDir}`);
|
|
1533
1741
|
}
|
|
1742
|
+
const content = `${JSON.stringify(preset, null, 2)}\n`;
|
|
1743
|
+
await writeFile(configPath, content, "utf8");
|
|
1744
|
+
log.debug(`Wrote config file: ${configPath}`);
|
|
1534
1745
|
log.debug("Preset initialization complete.");
|
|
1535
1746
|
return {
|
|
1536
1747
|
configPath,
|
|
@@ -1562,17 +1773,55 @@ function check(schema) {
|
|
|
1562
1773
|
//#region src/commands/preset/init-interactive.ts
|
|
1563
1774
|
const DEFAULT_PRESET_NAME = "my-preset";
|
|
1564
1775
|
/**
|
|
1565
|
-
* Run interactive init flow with clack prompts
|
|
1776
|
+
* Run interactive init flow with clack prompts.
|
|
1777
|
+
*
|
|
1778
|
+
* If platformDir is provided, init directly in that directory.
|
|
1779
|
+
* Otherwise, detect platform directories and prompt user to select one.
|
|
1566
1780
|
*/
|
|
1567
1781
|
async function initInteractive(options) {
|
|
1568
|
-
const {
|
|
1782
|
+
const { baseDir, platformDir: explicitPlatformDir, name: nameOption, title: titleOption, description: descriptionOption, platform: platformOption, license: licenseOption } = options;
|
|
1569
1783
|
let { force } = options;
|
|
1570
1784
|
const defaultName = nameOption ?? DEFAULT_PRESET_NAME;
|
|
1571
1785
|
p.intro("Create a new preset");
|
|
1572
|
-
|
|
1786
|
+
let targetPlatformDir;
|
|
1787
|
+
let selectedPlatform;
|
|
1788
|
+
if (explicitPlatformDir) {
|
|
1789
|
+
targetPlatformDir = explicitPlatformDir;
|
|
1790
|
+
const dirName = basename(explicitPlatformDir);
|
|
1791
|
+
selectedPlatform = platformOption ?? getPlatformFromDir(dirName) ?? "opencode";
|
|
1792
|
+
} else {
|
|
1793
|
+
const resolved = await resolvePlatformDirectory(baseDir, platformOption);
|
|
1794
|
+
if (resolved.isTargetPlatformDir) {
|
|
1795
|
+
targetPlatformDir = resolved.platformDir;
|
|
1796
|
+
selectedPlatform = resolved.platform;
|
|
1797
|
+
p.note(`Detected platform directory: ${resolved.platform}`, "Using current directory");
|
|
1798
|
+
} else {
|
|
1799
|
+
const detectedMap = new Map(resolved.detected.map((d) => [d.id, d]));
|
|
1800
|
+
if (resolved.detected.length > 0) p.note(resolved.detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
|
|
1801
|
+
const platformChoice = await p.select({
|
|
1802
|
+
message: "Platform",
|
|
1803
|
+
options: PLATFORM_IDS.map((id) => ({
|
|
1804
|
+
value: id,
|
|
1805
|
+
label: detectedMap.has(id) ? `${id} (detected)` : id,
|
|
1806
|
+
hint: detectedMap.get(id)?.path
|
|
1807
|
+
})),
|
|
1808
|
+
initialValue: resolved.platform
|
|
1809
|
+
});
|
|
1810
|
+
if (p.isCancel(platformChoice)) {
|
|
1811
|
+
p.cancel("Cancelled");
|
|
1812
|
+
process.exit(0);
|
|
1813
|
+
}
|
|
1814
|
+
selectedPlatform = platformChoice;
|
|
1815
|
+
if (selectedPlatform !== resolved.platform) {
|
|
1816
|
+
const reResolved = await resolvePlatformDirectory(baseDir, selectedPlatform);
|
|
1817
|
+
targetPlatformDir = reResolved.platformDir;
|
|
1818
|
+
} else targetPlatformDir = resolved.platformDir;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const configPath = join(targetPlatformDir, PRESET_CONFIG_FILENAME);
|
|
1573
1822
|
if (!force && await fileExists(configPath)) {
|
|
1574
1823
|
const overwrite = await p.confirm({
|
|
1575
|
-
message: `${PRESET_CONFIG_FILENAME} already exists. Overwrite?`,
|
|
1824
|
+
message: `${PRESET_CONFIG_FILENAME} already exists in ${targetPlatformDir}. Overwrite?`,
|
|
1576
1825
|
initialValue: false
|
|
1577
1826
|
});
|
|
1578
1827
|
if (p.isCancel(overwrite) || !overwrite) {
|
|
@@ -1581,9 +1830,6 @@ async function initInteractive(options) {
|
|
|
1581
1830
|
}
|
|
1582
1831
|
force = true;
|
|
1583
1832
|
}
|
|
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
1833
|
const result = await p.group({
|
|
1588
1834
|
name: () => p.text({
|
|
1589
1835
|
message: "Preset name (slug)",
|
|
@@ -1609,18 +1855,6 @@ async function initInteractive(options) {
|
|
|
1609
1855
|
validate: check(descriptionSchema)
|
|
1610
1856
|
});
|
|
1611
1857
|
},
|
|
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
1858
|
license: async () => {
|
|
1625
1859
|
const defaultLicense = licenseOption ?? "MIT";
|
|
1626
1860
|
const choice = await p.select({
|
|
@@ -1656,14 +1890,12 @@ async function initInteractive(options) {
|
|
|
1656
1890
|
p.cancel("Cancelled");
|
|
1657
1891
|
return process.exit(0);
|
|
1658
1892
|
} });
|
|
1659
|
-
const detectedPath = detectedMap.get(result.platform)?.path;
|
|
1660
1893
|
const initOptions = {
|
|
1661
|
-
directory,
|
|
1894
|
+
directory: targetPlatformDir,
|
|
1662
1895
|
name: result.name,
|
|
1663
1896
|
title: result.title,
|
|
1664
1897
|
description: result.description,
|
|
1665
|
-
platform:
|
|
1666
|
-
detectedPath,
|
|
1898
|
+
platform: selectedPlatform,
|
|
1667
1899
|
license: result.license,
|
|
1668
1900
|
force
|
|
1669
1901
|
};
|
|
@@ -1727,11 +1959,16 @@ async function validatePreset(options) {
|
|
|
1727
1959
|
const platform = preset.platform;
|
|
1728
1960
|
log.debug(`Checking platform: ${platform}`);
|
|
1729
1961
|
if (isSupportedPlatform(platform)) {
|
|
1730
|
-
const
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1962
|
+
const dirName = basename(presetDir);
|
|
1963
|
+
const isInProjectMode = isPlatformDir(dirName);
|
|
1964
|
+
if (isInProjectMode) log.debug(`In-project mode: files expected in ${presetDir}`);
|
|
1965
|
+
else {
|
|
1966
|
+
const filesPath = preset.path ?? PLATFORMS[platform].projectDir;
|
|
1967
|
+
const filesDir = join(presetDir, filesPath);
|
|
1968
|
+
const filesExists = await directoryExists(filesDir);
|
|
1969
|
+
log.debug(`Standalone mode: files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
|
|
1970
|
+
if (!filesExists) errors.push(`Files directory not found: ${filesPath}`);
|
|
1971
|
+
}
|
|
1735
1972
|
} else {
|
|
1736
1973
|
errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
1737
1974
|
log.debug(`Platform "${platform}" is not supported`);
|
|
@@ -1762,9 +1999,6 @@ async function validatePreset(options) {
|
|
|
1762
1999
|
|
|
1763
2000
|
//#endregion
|
|
1764
2001
|
//#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
2002
|
/** Maximum size per bundle in bytes (1MB) */
|
|
1769
2003
|
const MAX_BUNDLE_SIZE_BYTES = 1 * 1024 * 1024;
|
|
1770
2004
|
/**
|
|
@@ -1782,12 +2016,12 @@ async function publish(options = {}) {
|
|
|
1782
2016
|
const { path: path$1, version: version$1, dryRun = false } = options;
|
|
1783
2017
|
log.debug(`Publishing preset from path: ${path$1 ?? process.cwd()}${dryRun ? " (dry run)" : ""}`);
|
|
1784
2018
|
const ctx = useAppContext();
|
|
1785
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
1786
2019
|
if (!(dryRun || ctx.isLoggedIn && ctx.credentials)) {
|
|
1787
|
-
|
|
2020
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
2021
|
+
log.error(error$2);
|
|
1788
2022
|
return {
|
|
1789
2023
|
success: false,
|
|
1790
|
-
error:
|
|
2024
|
+
error: error$2
|
|
1791
2025
|
};
|
|
1792
2026
|
}
|
|
1793
2027
|
if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
|
|
@@ -1807,15 +2041,15 @@ async function publish(options = {}) {
|
|
|
1807
2041
|
spinner$1.update("Loading preset...");
|
|
1808
2042
|
let presetInput;
|
|
1809
2043
|
try {
|
|
1810
|
-
presetInput = await loadPreset
|
|
2044
|
+
presetInput = await loadPreset(presetDir);
|
|
1811
2045
|
log.debug(`Loaded preset "${presetInput.slug}" for platform ${presetInput.config.platform}`);
|
|
1812
2046
|
} catch (error$2) {
|
|
1813
|
-
const
|
|
2047
|
+
const message = getErrorMessage(error$2);
|
|
1814
2048
|
spinner$1.fail("Failed to load preset");
|
|
1815
|
-
log.error(
|
|
2049
|
+
log.error(message);
|
|
1816
2050
|
return {
|
|
1817
2051
|
success: false,
|
|
1818
|
-
error:
|
|
2052
|
+
error: message
|
|
1819
2053
|
};
|
|
1820
2054
|
}
|
|
1821
2055
|
spinner$1.update("Building bundle...");
|
|
@@ -1827,12 +2061,12 @@ async function publish(options = {}) {
|
|
|
1827
2061
|
});
|
|
1828
2062
|
log.debug(`Built publish input for ${publishInput.platform}`);
|
|
1829
2063
|
} catch (error$2) {
|
|
1830
|
-
const
|
|
2064
|
+
const message = getErrorMessage(error$2);
|
|
1831
2065
|
spinner$1.fail("Failed to build bundle");
|
|
1832
|
-
log.error(
|
|
2066
|
+
log.error(message);
|
|
1833
2067
|
return {
|
|
1834
2068
|
success: false,
|
|
1835
|
-
error:
|
|
2069
|
+
error: message
|
|
1836
2070
|
};
|
|
1837
2071
|
}
|
|
1838
2072
|
const inputJson = JSON.stringify(publishInput);
|
|
@@ -1912,79 +2146,9 @@ async function publish(options = {}) {
|
|
|
1912
2146
|
}
|
|
1913
2147
|
};
|
|
1914
2148
|
}
|
|
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
2149
|
|
|
1983
2150
|
//#endregion
|
|
1984
2151
|
//#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
2152
|
async function buildRegistry(options) {
|
|
1989
2153
|
const inputDir = options.input;
|
|
1990
2154
|
const outputDir = options.out ?? null;
|
|
@@ -2060,69 +2224,49 @@ async function discoverPresetDirs(inputDir) {
|
|
|
2060
2224
|
await searchDir(inputDir, 0);
|
|
2061
2225
|
return presetDirs.sort();
|
|
2062
2226
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2227
|
+
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/commands/unpublish.ts
|
|
2230
|
+
/**
|
|
2231
|
+
* Parses preset input to extract slug, platform, and version.
|
|
2232
|
+
* Supports formats:
|
|
2233
|
+
* - "my-preset.claude@1.0" (platform and version in string)
|
|
2234
|
+
* - "my-preset@1.0" (requires explicit platform)
|
|
2235
|
+
* - "my-preset.claude" (requires explicit version)
|
|
2236
|
+
*
|
|
2237
|
+
* Explicit --platform and --version flags take precedence.
|
|
2238
|
+
*/
|
|
2239
|
+
function parseUnpublishInput(input, explicitPlatform, explicitVersion) {
|
|
2240
|
+
let normalized = input.toLowerCase().trim();
|
|
2241
|
+
let parsedVersion;
|
|
2242
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
2243
|
+
if (atIndex > 0) {
|
|
2244
|
+
parsedVersion = normalized.slice(atIndex + 1);
|
|
2245
|
+
normalized = normalized.slice(0, atIndex);
|
|
2246
|
+
}
|
|
2247
|
+
const version$1 = explicitVersion ?? parsedVersion;
|
|
2248
|
+
const parts = normalized.split(".");
|
|
2249
|
+
const maybePlatform = parts.at(-1);
|
|
2250
|
+
let slug;
|
|
2251
|
+
let platform;
|
|
2252
|
+
if (maybePlatform && isSupportedPlatform(maybePlatform)) {
|
|
2253
|
+
slug = parts.slice(0, -1).join(".");
|
|
2254
|
+
platform = explicitPlatform ?? maybePlatform;
|
|
2255
|
+
} else {
|
|
2256
|
+
slug = normalized;
|
|
2257
|
+
platform = explicitPlatform;
|
|
2071
2258
|
}
|
|
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
2259
|
return {
|
|
2086
2260
|
slug,
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
installMessage,
|
|
2090
|
-
readmeContent,
|
|
2091
|
-
licenseContent
|
|
2261
|
+
platform,
|
|
2262
|
+
version: version$1
|
|
2092
2263
|
};
|
|
2093
2264
|
}
|
|
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
|
-
|
|
2119
|
-
//#endregion
|
|
2120
|
-
//#region src/commands/unpublish.ts
|
|
2121
2265
|
/**
|
|
2122
2266
|
* Unpublishes a preset version from the registry
|
|
2123
2267
|
*/
|
|
2124
2268
|
async function unpublish(options) {
|
|
2125
|
-
const { slug, platform, version: version$1 } = options;
|
|
2269
|
+
const { slug, platform, version: version$1 } = parseUnpublishInput(options.preset, options.platform, options.version);
|
|
2126
2270
|
if (!slug) {
|
|
2127
2271
|
log.error("Preset slug is required");
|
|
2128
2272
|
return {
|
|
@@ -2131,14 +2275,14 @@ async function unpublish(options) {
|
|
|
2131
2275
|
};
|
|
2132
2276
|
}
|
|
2133
2277
|
if (!platform) {
|
|
2134
|
-
log.error("Platform is required");
|
|
2278
|
+
log.error("Platform is required. Use --platform or specify as <slug>.<platform>@<version>");
|
|
2135
2279
|
return {
|
|
2136
2280
|
success: false,
|
|
2137
2281
|
error: "Platform is required"
|
|
2138
2282
|
};
|
|
2139
2283
|
}
|
|
2140
2284
|
if (!version$1) {
|
|
2141
|
-
log.error("Version is required");
|
|
2285
|
+
log.error("Version is required. Use --version or specify as <slug>.<platform>@<version>");
|
|
2142
2286
|
return {
|
|
2143
2287
|
success: false,
|
|
2144
2288
|
error: "Version is required"
|
|
@@ -2146,12 +2290,12 @@ async function unpublish(options) {
|
|
|
2146
2290
|
}
|
|
2147
2291
|
log.debug(`Unpublishing preset: ${slug}.${platform}@${version$1}`);
|
|
2148
2292
|
const ctx = useAppContext();
|
|
2149
|
-
if (!ctx) throw new Error("App context not initialized");
|
|
2150
2293
|
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2151
|
-
|
|
2294
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
2295
|
+
log.error(error$2);
|
|
2152
2296
|
return {
|
|
2153
2297
|
success: false,
|
|
2154
|
-
error:
|
|
2298
|
+
error: error$2
|
|
2155
2299
|
};
|
|
2156
2300
|
}
|
|
2157
2301
|
log.debug(`Authenticated, unpublishing from ${ctx.registry.url}`);
|
|
@@ -2194,10 +2338,10 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
|
|
|
2194
2338
|
url: actionOpts.url
|
|
2195
2339
|
});
|
|
2196
2340
|
} catch (error$2) {
|
|
2197
|
-
log.debug(`Failed to init context: ${
|
|
2341
|
+
log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
|
|
2198
2342
|
}
|
|
2199
2343
|
}).showHelpAfterError();
|
|
2200
|
-
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) => {
|
|
2344
|
+
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 (backs up originals)").option("-y, --yes", "Alias for --force").option("--dry-run", "Preview changes without writing").option("--skip-conflicts", "Skip conflicting files").option("--no-backup", "Don't backup files before overwriting (use with --force)").action(handle(async (preset, options) => {
|
|
2201
2345
|
const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
|
|
2202
2346
|
const dryRun = Boolean(options.dryRun);
|
|
2203
2347
|
const spinner$1 = await log.spinner("Fetching preset...");
|
|
@@ -2211,7 +2355,8 @@ program.command("add <preset>").description("Download and install a preset from
|
|
|
2211
2355
|
directory: options.dir,
|
|
2212
2356
|
force: Boolean(options.force || options.yes),
|
|
2213
2357
|
dryRun,
|
|
2214
|
-
skipConflicts: Boolean(options.skipConflicts)
|
|
2358
|
+
skipConflicts: Boolean(options.skipConflicts),
|
|
2359
|
+
noBackup: options.backup === false
|
|
2215
2360
|
});
|
|
2216
2361
|
} catch (err) {
|
|
2217
2362
|
spinner$1.stop();
|
|
@@ -2221,16 +2366,23 @@ program.command("add <preset>").description("Download and install a preset from
|
|
|
2221
2366
|
const hasBlockingConflicts = result.conflicts.length > 0 && !options.skipConflicts && !dryRun;
|
|
2222
2367
|
if (hasBlockingConflicts) {
|
|
2223
2368
|
const count$1 = result.conflicts.length === 1 ? "1 file has" : `${result.conflicts.length} files have`;
|
|
2224
|
-
|
|
2369
|
+
const forceHint = `Use ${ui.command("--force")} to overwrite ${ui.muted("(--no-backup to skip backups)")}`;
|
|
2370
|
+
log.error(`${count$1} conflicts. ${forceHint}`);
|
|
2225
2371
|
log.print("");
|
|
2226
2372
|
for (const conflict of result.conflicts.slice(0, 3)) {
|
|
2227
2373
|
log.print(` ${ui.muted("•")} ${conflict.path}`);
|
|
2228
2374
|
if (conflict.diff) log.print(conflict.diff.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
2229
2375
|
}
|
|
2230
2376
|
if (result.conflicts.length > 3) log.print(`\n ${ui.muted(`...and ${result.conflicts.length - 3} more`)}`);
|
|
2377
|
+
log.print("");
|
|
2378
|
+
log.print(forceHint);
|
|
2231
2379
|
process.exitCode = 1;
|
|
2232
2380
|
return;
|
|
2233
2381
|
}
|
|
2382
|
+
if (result.backups.length > 0) {
|
|
2383
|
+
log.print("");
|
|
2384
|
+
for (const backup of result.backups) log.print(ui.backupStatus(backup.originalPath, backup.backupPath, { dryRun }));
|
|
2385
|
+
}
|
|
2234
2386
|
log.print("");
|
|
2235
2387
|
for (const file of result.files) {
|
|
2236
2388
|
const status = file.status === "overwritten" ? "updated" : file.status;
|
|
@@ -2250,7 +2402,7 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2250
2402
|
const useInteractive = !options.yes && process.stdin.isTTY;
|
|
2251
2403
|
if (useInteractive) {
|
|
2252
2404
|
const result$1 = await initInteractive({
|
|
2253
|
-
|
|
2405
|
+
baseDir: targetDir,
|
|
2254
2406
|
name: options.name ?? defaultName,
|
|
2255
2407
|
title: options.title,
|
|
2256
2408
|
description: options.description,
|
|
@@ -2262,23 +2414,32 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2262
2414
|
log.print(`\n${ui.header("Directory created")}`);
|
|
2263
2415
|
log.print(ui.list([ui.path(result$1.createdDir)]));
|
|
2264
2416
|
}
|
|
2265
|
-
const nextSteps$1 = [
|
|
2266
|
-
|
|
2267
|
-
|
|
2417
|
+
const nextSteps$1 = [
|
|
2418
|
+
"Add your config files to the platform directory",
|
|
2419
|
+
"Add tags (required) and features (recommended) to agentrules.json",
|
|
2420
|
+
`Run ${ui.command("agentrules publish")} to publish your preset`
|
|
2421
|
+
];
|
|
2268
2422
|
log.print(`\n${ui.header("Next steps")}`);
|
|
2269
2423
|
log.print(ui.numberedList(nextSteps$1));
|
|
2270
2424
|
return;
|
|
2271
2425
|
}
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
|
|
2426
|
+
const resolved = await resolvePlatformDirectory(targetDir, options.platform);
|
|
2427
|
+
if (!options.platform) {
|
|
2428
|
+
const check$1 = requiresPlatformFlag(resolved);
|
|
2429
|
+
if (check$1.required) {
|
|
2430
|
+
if (check$1.reason === "no_platforms") {
|
|
2431
|
+
const targetDirName = basename(targetDir);
|
|
2432
|
+
log.error(`No platform directory found in "${targetDirName}". Specify --platform (${PLATFORM_IDS.join(", ")}) or run from a platform directory.`);
|
|
2433
|
+
} else log.error(`Multiple platform directories found (${check$1.platforms.join(", ")}). Specify --platform to choose one.`);
|
|
2434
|
+
process.exit(1);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2275
2437
|
const result = await initPreset({
|
|
2276
|
-
directory:
|
|
2438
|
+
directory: resolved.platformDir,
|
|
2277
2439
|
name: options.name ?? defaultName,
|
|
2278
2440
|
title: options.title,
|
|
2279
2441
|
description: options.description,
|
|
2280
|
-
platform,
|
|
2281
|
-
detectedPath,
|
|
2442
|
+
platform: resolved.platform,
|
|
2282
2443
|
license: options.license,
|
|
2283
2444
|
force: options.force
|
|
2284
2445
|
});
|
|
@@ -2287,9 +2448,11 @@ program.command("init").description("Initialize a new preset").argument("[direct
|
|
|
2287
2448
|
log.print(`\n${ui.header("Directory created")}`);
|
|
2288
2449
|
log.print(ui.list([ui.path(result.createdDir)]));
|
|
2289
2450
|
}
|
|
2290
|
-
const nextSteps = [
|
|
2291
|
-
|
|
2292
|
-
|
|
2451
|
+
const nextSteps = [
|
|
2452
|
+
"Add your config files to the platform directory",
|
|
2453
|
+
"Add tags (required) and features (recommended) to agentrules.json",
|
|
2454
|
+
`Run ${ui.command("agentrules publish")} to publish your preset`
|
|
2455
|
+
];
|
|
2293
2456
|
log.print(`\n${ui.header("Next steps")}`);
|
|
2294
2457
|
log.print(ui.numberedList(nextSteps));
|
|
2295
2458
|
}));
|
|
@@ -2431,11 +2594,12 @@ program.command("publish").description("Publish a preset to the registry").argum
|
|
|
2431
2594
|
});
|
|
2432
2595
|
if (!result.success) process.exitCode = 1;
|
|
2433
2596
|
}));
|
|
2434
|
-
program.command("unpublish").description("Remove a preset version from the registry").argument("<
|
|
2597
|
+
program.command("unpublish").description("Remove a preset version from the registry").argument("<preset>", "Preset to unpublish (e.g., my-preset.claude@1.0 or my-preset@1.0)").option("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").option("-V, --version <version>", "Version to unpublish").action(handle(async (preset, options) => {
|
|
2598
|
+
const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
|
|
2435
2599
|
const result = await unpublish({
|
|
2436
|
-
|
|
2600
|
+
preset,
|
|
2437
2601
|
platform,
|
|
2438
|
-
version: version
|
|
2602
|
+
version: options.version
|
|
2439
2603
|
});
|
|
2440
2604
|
if (!result.success) process.exitCode = 1;
|
|
2441
2605
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentrules/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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",
|