@agentrules/cli 0.0.11 → 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.
Files changed (3) hide show
  1. package/README.md +24 -3
  2. package/dist/index.js +177 -46
  3. package/package.json +1 -1
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
 
@@ -200,12 +201,32 @@ agentrules publish --dry-run
200
201
 
201
202
  **Versioning:** Presets use `MAJOR.MINOR` versioning. You set the major version, and the registry auto-increments the minor version on each publish.
202
203
 
203
- ### `agentrules unpublish <slug> <platform> <version>`
204
+ ### `agentrules unpublish <preset>`
204
205
 
205
206
  Remove a specific version of a preset from the registry. Requires authentication.
206
207
 
207
208
  ```bash
208
- agentrules unpublish my-preset opencode 1.0
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) |
218
+
219
+ **Examples:**
220
+
221
+ ```bash
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
209
230
  ```
210
231
 
211
232
  **Note:** Unpublished versions cannot be republished with the same version number.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { promisify } from "util";
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,
@@ -1209,6 +1218,7 @@ async function addPreset(options) {
1209
1218
  const writeStats = await writeBundleFiles(bundle, target, {
1210
1219
  force: Boolean(options.force),
1211
1220
  skipConflicts: Boolean(options.skipConflicts),
1221
+ noBackup: Boolean(options.noBackup),
1212
1222
  dryRun
1213
1223
  });
1214
1224
  return {
@@ -1216,6 +1226,7 @@ async function addPreset(options) {
1216
1226
  bundle,
1217
1227
  files: writeStats.files,
1218
1228
  conflicts: writeStats.conflicts,
1229
+ backups: writeStats.backups,
1219
1230
  targetRoot: target.root,
1220
1231
  targetLabel: target.label,
1221
1232
  registryAlias,
@@ -1298,6 +1309,7 @@ function resolveInstallTarget(platform, options) {
1298
1309
  async function writeBundleFiles(bundle, target, behavior) {
1299
1310
  const files = [];
1300
1311
  const conflicts = [];
1312
+ const backups = [];
1301
1313
  if (!behavior.dryRun) await mkdir(target.root, { recursive: true });
1302
1314
  for (const file of bundle.files) {
1303
1315
  const decoded = decodeBundledFile(file);
@@ -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,7 +1368,8 @@ async function writeBundleFiles(bundle, target, behavior) {
1346
1368
  }
1347
1369
  return {
1348
1370
  files,
1349
- conflicts
1371
+ conflicts,
1372
+ backups
1350
1373
  };
1351
1374
  }
1352
1375
  /**
@@ -1621,6 +1644,66 @@ async function detectPlatforms(directory) {
1621
1644
  return detected;
1622
1645
  }
1623
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
+ /**
1624
1707
  * Initialize a preset in a platform directory.
1625
1708
  *
1626
1709
  * Structure:
@@ -1704,30 +1787,36 @@ async function initInteractive(options) {
1704
1787
  let selectedPlatform;
1705
1788
  if (explicitPlatformDir) {
1706
1789
  targetPlatformDir = explicitPlatformDir;
1707
- const dirName = explicitPlatformDir.split("/").pop() ?? explicitPlatformDir;
1790
+ const dirName = basename(explicitPlatformDir);
1708
1791
  selectedPlatform = platformOption ?? getPlatformFromDir(dirName) ?? "opencode";
1709
1792
  } 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);
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;
1726
1819
  }
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
1820
  }
1732
1821
  const configPath = join(targetPlatformDir, PRESET_CONFIG_FILENAME);
1733
1822
  if (!force && await fileExists(configPath)) {
@@ -2139,10 +2228,45 @@ async function discoverPresetDirs(inputDir) {
2139
2228
  //#endregion
2140
2229
  //#region src/commands/unpublish.ts
2141
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;
2258
+ }
2259
+ return {
2260
+ slug,
2261
+ platform,
2262
+ version: version$1
2263
+ };
2264
+ }
2265
+ /**
2142
2266
  * Unpublishes a preset version from the registry
2143
2267
  */
2144
2268
  async function unpublish(options) {
2145
- const { slug, platform, version: version$1 } = options;
2269
+ const { slug, platform, version: version$1 } = parseUnpublishInput(options.preset, options.platform, options.version);
2146
2270
  if (!slug) {
2147
2271
  log.error("Preset slug is required");
2148
2272
  return {
@@ -2151,14 +2275,14 @@ async function unpublish(options) {
2151
2275
  };
2152
2276
  }
2153
2277
  if (!platform) {
2154
- log.error("Platform is required");
2278
+ log.error("Platform is required. Use --platform or specify as <slug>.<platform>@<version>");
2155
2279
  return {
2156
2280
  success: false,
2157
2281
  error: "Platform is required"
2158
2282
  };
2159
2283
  }
2160
2284
  if (!version$1) {
2161
- log.error("Version is required");
2285
+ log.error("Version is required. Use --version or specify as <slug>.<platform>@<version>");
2162
2286
  return {
2163
2287
  success: false,
2164
2288
  error: "Version is required"
@@ -2217,7 +2341,7 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
2217
2341
  log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
2218
2342
  }
2219
2343
  }).showHelpAfterError();
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) => {
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) => {
2221
2345
  const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
2222
2346
  const dryRun = Boolean(options.dryRun);
2223
2347
  const spinner$1 = await log.spinner("Fetching preset...");
@@ -2231,7 +2355,8 @@ program.command("add <preset>").description("Download and install a preset from
2231
2355
  directory: options.dir,
2232
2356
  force: Boolean(options.force || options.yes),
2233
2357
  dryRun,
2234
- skipConflicts: Boolean(options.skipConflicts)
2358
+ skipConflicts: Boolean(options.skipConflicts),
2359
+ noBackup: options.backup === false
2235
2360
  });
2236
2361
  } catch (err) {
2237
2362
  spinner$1.stop();
@@ -2241,16 +2366,23 @@ program.command("add <preset>").description("Download and install a preset from
2241
2366
  const hasBlockingConflicts = result.conflicts.length > 0 && !options.skipConflicts && !dryRun;
2242
2367
  if (hasBlockingConflicts) {
2243
2368
  const count$1 = result.conflicts.length === 1 ? "1 file has" : `${result.conflicts.length} files have`;
2244
- log.error(`${count$1} conflicts. Use ${ui.command("--force")} to overwrite.`);
2369
+ const forceHint = `Use ${ui.command("--force")} to overwrite ${ui.muted("(--no-backup to skip backups)")}`;
2370
+ log.error(`${count$1} conflicts. ${forceHint}`);
2245
2371
  log.print("");
2246
2372
  for (const conflict of result.conflicts.slice(0, 3)) {
2247
2373
  log.print(` ${ui.muted("•")} ${conflict.path}`);
2248
2374
  if (conflict.diff) log.print(conflict.diff.split("\n").map((l) => ` ${l}`).join("\n"));
2249
2375
  }
2250
2376
  if (result.conflicts.length > 3) log.print(`\n ${ui.muted(`...and ${result.conflicts.length - 3} more`)}`);
2377
+ log.print("");
2378
+ log.print(forceHint);
2251
2379
  process.exitCode = 1;
2252
2380
  return;
2253
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
+ }
2254
2386
  log.print("");
2255
2387
  for (const file of result.files) {
2256
2388
  const status = file.status === "overwritten" ? "updated" : file.status;
@@ -2291,25 +2423,23 @@ program.command("init").description("Initialize a new preset").argument("[direct
2291
2423
  log.print(ui.numberedList(nextSteps$1));
2292
2424
  return;
2293
2425
  }
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);
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
+ }
2306
2436
  }
2307
2437
  const result = await initPreset({
2308
- directory: platformDir,
2438
+ directory: resolved.platformDir,
2309
2439
  name: options.name ?? defaultName,
2310
2440
  title: options.title,
2311
2441
  description: options.description,
2312
- platform,
2442
+ platform: resolved.platform,
2313
2443
  license: options.license,
2314
2444
  force: options.force
2315
2445
  });
@@ -2464,11 +2594,12 @@ program.command("publish").description("Publish a preset to the registry").argum
2464
2594
  });
2465
2595
  if (!result.success) process.exitCode = 1;
2466
2596
  }));
2467
- program.command("unpublish").description("Remove a preset version from the registry").argument("<slug>", "Preset slug (e.g., my-preset)").argument("<platform>", "Platform (e.g., opencode, claude)").argument("<version>", "Version to unpublish (e.g., 1.1, 2.3)").action(handle(async (slug, platform, version$1) => {
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;
2468
2599
  const result = await unpublish({
2469
- slug,
2600
+ preset,
2470
2601
  platform,
2471
- version: version$1
2602
+ version: options.version
2472
2603
  });
2473
2604
  if (!result.success) process.exitCode = 1;
2474
2605
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.0.11",
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",