@agentrules/cli 0.0.11 → 0.0.13

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 +523 -49
  3. 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
 
@@ -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
@@ -1,6 +1,6 @@
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
+ 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, tagsSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
4
4
  import { Command } from "commander";
5
5
  import { basename, dirname, join, relative, resolve, sep } from "path";
6
6
  import { exec } from "child_process";
@@ -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
  */
@@ -307,6 +315,49 @@ function relativeTime(date) {
307
315
  if (minutes > 0) return `${minutes}m ago`;
308
316
  return "just now";
309
317
  }
318
+ /**
319
+ * Formats an array of files as a tree structure
320
+ */
321
+ function fileTree(files) {
322
+ const root = {
323
+ name: "",
324
+ children: new Map()
325
+ };
326
+ for (const file of files) {
327
+ const parts = file.path.split("/");
328
+ let current = root;
329
+ for (let i = 0; i < parts.length; i++) {
330
+ const part = parts[i];
331
+ const isFile = i === parts.length - 1;
332
+ let child = current.children.get(part);
333
+ if (!child) {
334
+ child = {
335
+ name: part,
336
+ size: isFile ? file.size : void 0,
337
+ children: new Map()
338
+ };
339
+ current.children.set(part, child);
340
+ }
341
+ current = child;
342
+ }
343
+ }
344
+ const lines = [];
345
+ function renderNode(node, prefix, isLast) {
346
+ const connector = isLast ? "└── " : "├── ";
347
+ const sizeStr = node.size !== void 0 ? muted(` (${formatBytes$1(node.size)})`) : "";
348
+ lines.push(`${prefix}${connector}${node.name}${sizeStr}`);
349
+ const children = Array.from(node.children.values());
350
+ const newPrefix = prefix + (isLast ? " " : "│ ");
351
+ children.forEach((child, index) => {
352
+ renderNode(child, newPrefix, index === children.length - 1);
353
+ });
354
+ }
355
+ const topLevel = Array.from(root.children.values());
356
+ topLevel.forEach((child, index) => {
357
+ renderNode(child, "", index === topLevel.length - 1);
358
+ });
359
+ return lines.join("\n");
360
+ }
310
361
  const ui = {
311
362
  theme,
312
363
  symbols,
@@ -335,6 +386,7 @@ const ui = {
335
386
  warning,
336
387
  info: info$1,
337
388
  fileStatus,
389
+ backupStatus,
338
390
  step,
339
391
  brand,
340
392
  banner,
@@ -345,7 +397,8 @@ const ui = {
345
397
  stripAnsi,
346
398
  truncate,
347
399
  formatBytes: formatBytes$1,
348
- relativeTime
400
+ relativeTime,
401
+ fileTree
349
402
  };
350
403
 
351
404
  //#endregion
@@ -1209,6 +1262,7 @@ async function addPreset(options) {
1209
1262
  const writeStats = await writeBundleFiles(bundle, target, {
1210
1263
  force: Boolean(options.force),
1211
1264
  skipConflicts: Boolean(options.skipConflicts),
1265
+ noBackup: Boolean(options.noBackup),
1212
1266
  dryRun
1213
1267
  });
1214
1268
  return {
@@ -1216,6 +1270,7 @@ async function addPreset(options) {
1216
1270
  bundle,
1217
1271
  files: writeStats.files,
1218
1272
  conflicts: writeStats.conflicts,
1273
+ backups: writeStats.backups,
1219
1274
  targetRoot: target.root,
1220
1275
  targetLabel: target.label,
1221
1276
  registryAlias,
@@ -1298,6 +1353,7 @@ function resolveInstallTarget(platform, options) {
1298
1353
  async function writeBundleFiles(bundle, target, behavior) {
1299
1354
  const files = [];
1300
1355
  const conflicts = [];
1356
+ const backups = [];
1301
1357
  if (!behavior.dryRun) await mkdir(target.root, { recursive: true });
1302
1358
  for (const file of bundle.files) {
1303
1359
  const decoded = decodeBundledFile(file);
@@ -1326,6 +1382,16 @@ async function writeBundleFiles(bundle, target, behavior) {
1326
1382
  continue;
1327
1383
  }
1328
1384
  if (behavior.force) {
1385
+ if (!behavior.noBackup) {
1386
+ const backupPath = `${destination}.bak`;
1387
+ const relativeBackupPath = `${relativePath}.bak`;
1388
+ if (!behavior.dryRun) await copyFile(destination, backupPath);
1389
+ backups.push({
1390
+ originalPath: relativePath,
1391
+ backupPath: relativeBackupPath
1392
+ });
1393
+ log.debug(`Backed up: ${relativePath} → ${relativeBackupPath}`);
1394
+ }
1329
1395
  if (!behavior.dryRun) await writeFile(destination, data);
1330
1396
  files.push({
1331
1397
  path: relativePath,
@@ -1346,7 +1412,8 @@ async function writeBundleFiles(bundle, target, behavior) {
1346
1412
  }
1347
1413
  return {
1348
1414
  files,
1349
- conflicts
1415
+ conflicts,
1416
+ backups
1350
1417
  };
1351
1418
  }
1352
1419
  /**
@@ -1621,6 +1688,66 @@ async function detectPlatforms(directory) {
1621
1688
  return detected;
1622
1689
  }
1623
1690
  /**
1691
+ * Resolve the target platform directory for initialization.
1692
+ *
1693
+ * Detection order (deterministic):
1694
+ * 1. If targetDir itself is a platform directory (e.g., ".claude"), use it directly
1695
+ * 2. Otherwise, detect platform directories inside targetDir
1696
+ *
1697
+ * @param targetDir - The target directory (cwd or user-provided path)
1698
+ * @param platformOverride - Optional platform to use instead of detecting/inferring
1699
+ */
1700
+ async function resolvePlatformDirectory(targetDir, platformOverride) {
1701
+ const targetDirName = basename(targetDir);
1702
+ const targetPlatform = getPlatformFromDir(targetDirName);
1703
+ if (targetPlatform) {
1704
+ const platform$1 = platformOverride ? normalizePlatform(platformOverride) : targetPlatform;
1705
+ return {
1706
+ platformDir: targetDir,
1707
+ platform: platform$1,
1708
+ isTargetPlatformDir: true,
1709
+ detected: []
1710
+ };
1711
+ }
1712
+ const detected = await detectPlatforms(targetDir);
1713
+ let platform;
1714
+ let platformDir;
1715
+ if (platformOverride) {
1716
+ platform = normalizePlatform(platformOverride);
1717
+ const detectedPath = detected.find((d) => d.id === platform)?.path;
1718
+ platformDir = detectedPath ? join(targetDir, detectedPath) : join(targetDir, PLATFORMS[platform].projectDir);
1719
+ } else if (detected.length > 0) {
1720
+ platform = detected[0].id;
1721
+ platformDir = join(targetDir, detected[0].path);
1722
+ } else {
1723
+ platform = "opencode";
1724
+ platformDir = join(targetDir, PLATFORMS.opencode.projectDir);
1725
+ }
1726
+ return {
1727
+ platformDir,
1728
+ platform,
1729
+ isTargetPlatformDir: false,
1730
+ detected
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Check if --platform flag is required for non-interactive mode.
1735
+ * Returns the reason if required, so CLI can show appropriate error.
1736
+ */
1737
+ function requiresPlatformFlag(resolved) {
1738
+ if (resolved.isTargetPlatformDir) return { required: false };
1739
+ if (resolved.detected.length === 0) return {
1740
+ required: true,
1741
+ reason: "no_platforms"
1742
+ };
1743
+ if (resolved.detected.length > 1) return {
1744
+ required: true,
1745
+ reason: "multiple_platforms",
1746
+ platforms: resolved.detected.map((d) => d.id)
1747
+ };
1748
+ return { required: false };
1749
+ }
1750
+ /**
1624
1751
  * Initialize a preset in a platform directory.
1625
1752
  *
1626
1753
  * Structure:
@@ -1646,6 +1773,7 @@ async function initPreset(options) {
1646
1773
  title,
1647
1774
  version: 1,
1648
1775
  description,
1776
+ tags: options.tags ?? [],
1649
1777
  license,
1650
1778
  platform
1651
1779
  };
@@ -1690,6 +1818,20 @@ function check(schema) {
1690
1818
  //#region src/commands/preset/init-interactive.ts
1691
1819
  const DEFAULT_PRESET_NAME = "my-preset";
1692
1820
  /**
1821
+ * Parse comma-separated tags string into array
1822
+ */
1823
+ function parseTags(input) {
1824
+ return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
1825
+ }
1826
+ /**
1827
+ * Validator for comma-separated tags input
1828
+ */
1829
+ function checkTags(value) {
1830
+ const tags = parseTags(value);
1831
+ const result = tagsSchema.safeParse(tags);
1832
+ if (!result.success) return result.error.issues[0]?.message;
1833
+ }
1834
+ /**
1693
1835
  * Run interactive init flow with clack prompts.
1694
1836
  *
1695
1837
  * If platformDir is provided, init directly in that directory.
@@ -1704,30 +1846,36 @@ async function initInteractive(options) {
1704
1846
  let selectedPlatform;
1705
1847
  if (explicitPlatformDir) {
1706
1848
  targetPlatformDir = explicitPlatformDir;
1707
- const dirName = explicitPlatformDir.split("/").pop() ?? explicitPlatformDir;
1849
+ const dirName = basename(explicitPlatformDir);
1708
1850
  selectedPlatform = platformOption ?? getPlatformFromDir(dirName) ?? "opencode";
1709
1851
  } 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);
1852
+ const resolved = await resolvePlatformDirectory(baseDir, platformOption);
1853
+ if (resolved.isTargetPlatformDir) {
1854
+ targetPlatformDir = resolved.platformDir;
1855
+ selectedPlatform = resolved.platform;
1856
+ p.note(`Detected platform directory: ${resolved.platform}`, "Using current directory");
1857
+ } else {
1858
+ const detectedMap = new Map(resolved.detected.map((d) => [d.id, d]));
1859
+ if (resolved.detected.length > 0) p.note(resolved.detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
1860
+ const platformChoice = await p.select({
1861
+ message: "Platform",
1862
+ options: PLATFORM_IDS.map((id) => ({
1863
+ value: id,
1864
+ label: detectedMap.has(id) ? `${id} (detected)` : id,
1865
+ hint: detectedMap.get(id)?.path
1866
+ })),
1867
+ initialValue: resolved.platform
1868
+ });
1869
+ if (p.isCancel(platformChoice)) {
1870
+ p.cancel("Cancelled");
1871
+ process.exit(0);
1872
+ }
1873
+ selectedPlatform = platformChoice;
1874
+ if (selectedPlatform !== resolved.platform) {
1875
+ const reResolved = await resolvePlatformDirectory(baseDir, selectedPlatform);
1876
+ targetPlatformDir = reResolved.platformDir;
1877
+ } else targetPlatformDir = resolved.platformDir;
1726
1878
  }
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
1879
  }
1732
1880
  const configPath = join(targetPlatformDir, PRESET_CONFIG_FILENAME);
1733
1881
  if (!force && await fileExists(configPath)) {
@@ -1766,6 +1914,11 @@ async function initInteractive(options) {
1766
1914
  validate: check(descriptionSchema)
1767
1915
  });
1768
1916
  },
1917
+ tags: () => p.text({
1918
+ message: "Tags (comma-separated, at least one)",
1919
+ placeholder: "e.g., typescript, testing, react",
1920
+ validate: checkTags
1921
+ }),
1769
1922
  license: async () => {
1770
1923
  const defaultLicense = licenseOption ?? "MIT";
1771
1924
  const choice = await p.select({
@@ -1806,6 +1959,7 @@ async function initInteractive(options) {
1806
1959
  name: result.name,
1807
1960
  title: result.title,
1808
1961
  description: result.description,
1962
+ tags: parseTags(result.tags),
1809
1963
  platform: selectedPlatform,
1810
1964
  license: result.license,
1811
1965
  force
@@ -2004,6 +2158,9 @@ async function publish(options = {}) {
2004
2158
  log.print(ui.keyValue("Files", `${fileCount} file${fileCount === 1 ? "" : "s"}`));
2005
2159
  log.print(ui.keyValue("Size", formatBytes(inputSize)));
2006
2160
  log.print("");
2161
+ log.print(ui.header("Files to publish", fileCount));
2162
+ log.print(ui.fileTree(publishInput.files));
2163
+ log.print("");
2007
2164
  log.print(ui.hint("Run without --dry-run to publish."));
2008
2165
  return {
2009
2166
  success: true,
@@ -2041,6 +2198,9 @@ async function publish(options = {}) {
2041
2198
  const { data } = result;
2042
2199
  const action = data.isNewPreset ? "Published new preset" : "Published";
2043
2200
  spinner$1.success(`${action} ${ui.code(data.slug)} ${ui.version(data.version)} (${data.platform})`);
2201
+ log.print("");
2202
+ log.print(ui.header("Published files", fileCount));
2203
+ log.print(ui.fileTree(publishInput.files));
2044
2204
  const presetName$1 = `${data.slug}.${data.platform}`;
2045
2205
  const presetRegistryUrl = `${ctx.registry.url}preset/${presetName$1}`;
2046
2206
  log.info("");
@@ -2139,10 +2299,45 @@ async function discoverPresetDirs(inputDir) {
2139
2299
  //#endregion
2140
2300
  //#region src/commands/unpublish.ts
2141
2301
  /**
2302
+ * Parses preset input to extract slug, platform, and version.
2303
+ * Supports formats:
2304
+ * - "my-preset.claude@1.0" (platform and version in string)
2305
+ * - "my-preset@1.0" (requires explicit platform)
2306
+ * - "my-preset.claude" (requires explicit version)
2307
+ *
2308
+ * Explicit --platform and --version flags take precedence.
2309
+ */
2310
+ function parseUnpublishInput(input, explicitPlatform, explicitVersion) {
2311
+ let normalized = input.toLowerCase().trim();
2312
+ let parsedVersion;
2313
+ const atIndex = normalized.lastIndexOf("@");
2314
+ if (atIndex > 0) {
2315
+ parsedVersion = normalized.slice(atIndex + 1);
2316
+ normalized = normalized.slice(0, atIndex);
2317
+ }
2318
+ const version$1 = explicitVersion ?? parsedVersion;
2319
+ const parts = normalized.split(".");
2320
+ const maybePlatform = parts.at(-1);
2321
+ let slug;
2322
+ let platform;
2323
+ if (maybePlatform && isSupportedPlatform(maybePlatform)) {
2324
+ slug = parts.slice(0, -1).join(".");
2325
+ platform = explicitPlatform ?? maybePlatform;
2326
+ } else {
2327
+ slug = normalized;
2328
+ platform = explicitPlatform;
2329
+ }
2330
+ return {
2331
+ slug,
2332
+ platform,
2333
+ version: version$1
2334
+ };
2335
+ }
2336
+ /**
2142
2337
  * Unpublishes a preset version from the registry
2143
2338
  */
2144
2339
  async function unpublish(options) {
2145
- const { slug, platform, version: version$1 } = options;
2340
+ const { slug, platform, version: version$1 } = parseUnpublishInput(options.preset, options.platform, options.version);
2146
2341
  if (!slug) {
2147
2342
  log.error("Preset slug is required");
2148
2343
  return {
@@ -2151,14 +2346,14 @@ async function unpublish(options) {
2151
2346
  };
2152
2347
  }
2153
2348
  if (!platform) {
2154
- log.error("Platform is required");
2349
+ log.error("Platform is required. Use --platform or specify as <slug>.<platform>@<version>");
2155
2350
  return {
2156
2351
  success: false,
2157
2352
  error: "Platform is required"
2158
2353
  };
2159
2354
  }
2160
2355
  if (!version$1) {
2161
- log.error("Version is required");
2356
+ log.error("Version is required. Use --version or specify as <slug>.<platform>@<version>");
2162
2357
  return {
2163
2358
  success: false,
2164
2359
  error: "Version is required"
@@ -2199,12 +2394,280 @@ async function unpublish(options) {
2199
2394
  };
2200
2395
  }
2201
2396
 
2397
+ //#endregion
2398
+ //#region src/help-agent/publish.ts
2399
+ /**
2400
+ * Agent instructions for AI coding assistants.
2401
+ *
2402
+ * This content is output when running `agentrules --help-agent`.
2403
+ * It provides step-by-step instructions for AI agents to help users publish presets.
2404
+ */
2405
+ const HELP_AGENT_CONTENT = `# Publishing a Preset - Agent Instructions
2406
+
2407
+ **The user has asked you to help them publish a preset. Start executing these steps now.**
2408
+
2409
+ Do not summarize these instructions. Do not ask if they want help. Begin with Step 1 immediately.
2410
+
2411
+ Your job is to guide the user through publishing their AI coding assistant config to the agentrules registry. The user is sharing their setup because they believe it's valuable. Help them communicate that value clearly - the title, description, and features should answer: "Why would another developer want to install this?"
2412
+
2413
+ Follow these steps interactively.
2414
+
2415
+ ## Prerequisites: CLI Availability
2416
+
2417
+ Run: \`agentrules --version\`
2418
+
2419
+ **If command not found:**
2420
+ Use \`npx @agentrules/cli\` as the prefix for all commands.
2421
+ Example: \`npx @agentrules/cli whoami\` instead of \`agentrules whoami\`
2422
+
2423
+ For the rest of these instructions, \`agentrules\` means whichever works.
2424
+
2425
+ ## Step 1: Locate the Config
2426
+
2427
+ Check the current directory for platform config folders:
2428
+ - \`.opencode/\` → OpenCode
2429
+ - \`.claude/\` → Claude Code
2430
+ - \`.cursor/\` → Cursor
2431
+ - \`.codex/\` → Codex
2432
+
2433
+ **If one found:**
2434
+ "I found your [platform] config at \`[path]\`. I'll help you publish it."
2435
+
2436
+ **If multiple found:**
2437
+ "I found configs for multiple platforms: [list]. Which one would you like to publish?"
2438
+
2439
+ **If none found:**
2440
+ "I don't see a config directory here. Where is your config located?"
2441
+
2442
+ ## Step 2: Check for Existing Config
2443
+
2444
+ List the files in \`[config-dir]\` first to see what exists.
2445
+
2446
+ If \`agentrules.json\` is in the listing, read it:
2447
+ - If complete (has name, description, tags): "You already have a preset configured: '[name]'. Ready to republish?" → Skip to Step 4 if yes
2448
+ - If missing required fields: Help them add the missing fields
2449
+
2450
+ If \`agentrules.json\` is not in the listing, continue to Step 3.
2451
+
2452
+ ### Check for ignorable files
2453
+
2454
+ While reviewing the file listing, look for files/folders that probably shouldn't be published.
2455
+
2456
+ **Already ignored by default** (don't suggest these):
2457
+ - node_modules
2458
+ - .git
2459
+ - .DS_Store
2460
+ - *.lock
2461
+ - package-lock.json
2462
+ - bun.lockb
2463
+ - pnpm-lock.yaml
2464
+
2465
+ **Commonly ignorable** (suggest adding to \`ignore\` field if present):
2466
+ - build/, dist/, out/ (build output)
2467
+ - .env, .env.* (environment files)
2468
+ - *.log (log files)
2469
+ - tmp/, temp/ (temporary files)
2470
+ - coverage/ (test coverage)
2471
+ - .cache/, .turbo/ (cache directories)
2472
+
2473
+ If you see any of these or similar files/folders, ask: "I noticed [files]. These are usually not needed in a preset. Want me to add them to the ignore list?"
2474
+
2475
+ If yes, include them in the \`ignore\` array when creating agentrules.json.
2476
+
2477
+ ## Step 3: Create agentrules.json
2478
+
2479
+ The goal is to help potential users understand the **value** of this preset - why should they install it? What problem does it solve? How will it improve their workflow?
2480
+
2481
+ ### 3a. Analyze their config
2482
+
2483
+ You already listed files in Step 2. Now read the config files you found (e.g., CLAUDE.md, AGENT_RULES.md, rules/*.md) to understand what the preset does.
2484
+
2485
+ Look for:
2486
+ - Technologies and frameworks mentioned
2487
+ - The main purpose or rules being enforced
2488
+ - Who would benefit from this setup
2489
+
2490
+ ### 3b. Generate all suggestions at once
2491
+
2492
+ Based on your analysis, generate suggestions for ALL fields:
2493
+
2494
+ - **Name**: lowercase, hyphens, based on repo/directory/theme (1-64 chars)
2495
+ - **Title**: Title-cased, compelling name
2496
+ - **Description**: Value-focused - who is this for, what problem does it solve? (max 500 chars)
2497
+ - **Tags**: For discovery - technologies, frameworks, use cases (1-10 tags)
2498
+ - **Features**: Key benefits, not just capabilities (optional, up to 5)
2499
+ - **License**: Default to MIT
2500
+
2501
+ ### 3c. Present a single summary
2502
+
2503
+ Show everything in one concise output. Put each field name on its own line, followed by the value on the next line:
2504
+
2505
+ "Based on your config, here's what I'd suggest:
2506
+
2507
+ **Name**
2508
+ typescript-strict-rules
2509
+
2510
+ **Title**
2511
+ TypeScript Strict Rules
2512
+
2513
+ **Description**
2514
+ Opinionated TypeScript rules that catch common bugs at dev time and enforce consistent patterns across your team.
2515
+
2516
+ **Tags**
2517
+ typescript, strict, type-safety
2518
+
2519
+ **Features**
2520
+ - Catches null/undefined errors before production
2521
+ - Enforces consistent code style without manual review
2522
+
2523
+ **License**
2524
+ MIT
2525
+
2526
+ Let me know if you'd like to change anything, or say 'looks good' to continue."
2527
+
2528
+ ### 3d. Handle feedback
2529
+
2530
+ If the user wants changes (e.g., "change the description" or "add a react tag"), update those fields and show the summary again.
2531
+
2532
+ When they approve, proceed to create the file.
2533
+
2534
+ ### Guidelines for good suggestions
2535
+
2536
+ **Description** should answer: What problem does this solve? Who benefits?
2537
+ - Good: "Opinionated TypeScript rules that catch common bugs at dev time and enforce consistent patterns across your team."
2538
+ - Bad: "TypeScript rules with strict settings." (too vague, no value prop)
2539
+
2540
+ **Features** should describe benefits, not capabilities:
2541
+ - Good: "Catches null/undefined errors before they hit production"
2542
+ - Bad: "Strict null checks" (feature, not benefit)
2543
+
2544
+ **Tags** should help with discovery:
2545
+ - Technologies: typescript, python, rust, go
2546
+ - Frameworks: react, nextjs, fastapi, django
2547
+ - Use cases: code-review, testing, security, onboarding
2548
+
2549
+ ### 3e. Create the file
2550
+
2551
+ Write \`[config-dir]/agentrules.json\`:
2552
+
2553
+ \`\`\`json
2554
+ {
2555
+ "$schema": "https://agentrules.directory/schema/agentrules.json",
2556
+ "name": "[name]",
2557
+ "title": "[title]",
2558
+ "version": 1,
2559
+ "description": "[description]",
2560
+ "tags": ["tag1", "tag2"],
2561
+ "license": "[license]",
2562
+ "platform": "[detected-platform]"
2563
+ }
2564
+ \`\`\`
2565
+
2566
+ Include \`"features": [...]\` only if provided.
2567
+ Include \`"ignore": ["pattern1", "pattern2"]\` if the user agreed to ignore certain files.
2568
+
2569
+ ### 3f. Show the file and get approval
2570
+
2571
+ After writing the file, show the user:
2572
+
2573
+ "I've created the config file at \`[config-dir]/agentrules.json\`:
2574
+
2575
+ \`\`\`json
2576
+ [show the actual file contents]
2577
+ \`\`\`
2578
+
2579
+ Take a look and let me know if you'd like to change anything, or say 'looks good' to continue."
2580
+
2581
+ Wait for approval before proceeding. If they want changes, edit the file and show it again.
2582
+
2583
+ Then validate: \`agentrules validate [config-dir]\`
2584
+
2585
+ If errors, fix and retry.
2586
+
2587
+ ## Step 4: Login
2588
+
2589
+ Run: \`agentrules whoami\`
2590
+
2591
+ **If output shows "loggedIn": false or "Not logged in":**
2592
+ "You need to log in to publish."
2593
+ Run: \`agentrules login\`
2594
+ This opens a browser for authentication. Wait for completion.
2595
+
2596
+ ## Step 5: Preview with Dry Run
2597
+
2598
+ Run: \`agentrules publish [config-dir] --dry-run\`
2599
+
2600
+ Show the user the preview:
2601
+ "Here's what will be published:
2602
+ - Name: [name]
2603
+ - Platform: [platform]
2604
+ - Files: [count] files ([size])
2605
+
2606
+ Ready to publish?"
2607
+
2608
+ If they want changes, help edit agentrules.json and re-run dry-run.
2609
+
2610
+ ## Step 6: Publish
2611
+
2612
+ Run: \`agentrules publish [config-dir]\`
2613
+
2614
+ **If successful:**
2615
+ Show the URL from the output:
2616
+
2617
+ "Published! Your preset is live at: [url]
2618
+
2619
+ Share with others:
2620
+ \`\`\`
2621
+ npx @agentrules/cli add [name]
2622
+ \`\`\`"
2623
+
2624
+ **If "already exists" error:**
2625
+ Ask if they want to increment the \`version\` field in agentrules.json and retry.
2626
+
2627
+ **If other errors:**
2628
+ Show the error and suggest: \`agentrules validate [config-dir]\`
2629
+
2630
+ ## Step 7: Tips
2631
+
2632
+ **If you used \`npx @agentrules/cli\`:**
2633
+ "Tip: Install globally to skip the npx download:
2634
+ \`\`\`
2635
+ npm i -g @agentrules/cli
2636
+ \`\`\`"
2637
+
2638
+ ## Notes for Agent
2639
+
2640
+ - Be conversational and helpful
2641
+ - Explain what you're doing at each step
2642
+ - Use \`agentrules validate\` to check your work after any config changes
2643
+ - Remember whether you used npx for the tip at the end
2644
+ - If the user seems confused, explain that agentrules is a registry for sharing AI coding configs
2645
+ - The config file must be inside the platform directory (e.g., \`.opencode/agentrules.json\`)
2646
+
2647
+ ## Schema Reference
2648
+
2649
+ **Required fields:**
2650
+ - \`name\`: slug format (lowercase, hyphens, 1-64 chars)
2651
+ - \`title\`: 1-80 characters
2652
+ - \`description\`: 1-500 characters
2653
+ - \`tags\`: array, 1-10 items, each lowercase/hyphens, max 35 chars
2654
+ - \`license\`: SPDX identifier (e.g., "MIT")
2655
+ - \`platform\`: one of \`opencode\`, \`claude\`, \`cursor\`, \`codex\`
2656
+
2657
+ **Optional fields:**
2658
+ - \`$schema\`: JSON schema URL for validation
2659
+ - \`version\`: major version number (default: 1)
2660
+ - \`features\`: array, max 5 items, each max 100 chars
2661
+ - \`path\`: custom path to files (advanced use)
2662
+ - \`ignore\`: patterns to exclude from bundle
2663
+ `;
2664
+
2202
2665
  //#endregion
2203
2666
  //#region src/index.ts
2204
2667
  const require = createRequire(import.meta.url);
2205
2668
  const packageJson = require("../package.json");
2206
2669
  const program = new Command();
2207
- program.name("agentrules").description("The AI Agent Directory CLI").version(packageJson.version).option("-v, --verbose", "Enable verbose/debug output").configureOutput({ outputError: (str, write) => write(ui.error(str.trim())) }).hook("preAction", async (thisCommand, actionCommand) => {
2670
+ program.name("agentrules").description("The AI Agent Directory CLI").version(packageJson.version).option("-v, --verbose", "Enable verbose/debug output").option("--help-agent", "Output instructions for AI coding assistants").configureOutput({ outputError: (str, write) => write(ui.error(str.trim())) }).hook("preAction", async (thisCommand, actionCommand) => {
2208
2671
  const opts = thisCommand.opts();
2209
2672
  if (opts.verbose) log.setVerbose(true);
2210
2673
  const actionOpts = actionCommand.opts();
@@ -2217,7 +2680,7 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
2217
2680
  log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
2218
2681
  }
2219
2682
  }).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) => {
2683
+ 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
2684
  const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
2222
2685
  const dryRun = Boolean(options.dryRun);
2223
2686
  const spinner$1 = await log.spinner("Fetching preset...");
@@ -2231,7 +2694,8 @@ program.command("add <preset>").description("Download and install a preset from
2231
2694
  directory: options.dir,
2232
2695
  force: Boolean(options.force || options.yes),
2233
2696
  dryRun,
2234
- skipConflicts: Boolean(options.skipConflicts)
2697
+ skipConflicts: Boolean(options.skipConflicts),
2698
+ noBackup: options.backup === false
2235
2699
  });
2236
2700
  } catch (err) {
2237
2701
  spinner$1.stop();
@@ -2241,16 +2705,23 @@ program.command("add <preset>").description("Download and install a preset from
2241
2705
  const hasBlockingConflicts = result.conflicts.length > 0 && !options.skipConflicts && !dryRun;
2242
2706
  if (hasBlockingConflicts) {
2243
2707
  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.`);
2708
+ const forceHint = `Use ${ui.command("--force")} to overwrite ${ui.muted("(--no-backup to skip backups)")}`;
2709
+ log.error(`${count$1} conflicts. ${forceHint}`);
2245
2710
  log.print("");
2246
2711
  for (const conflict of result.conflicts.slice(0, 3)) {
2247
2712
  log.print(` ${ui.muted("•")} ${conflict.path}`);
2248
2713
  if (conflict.diff) log.print(conflict.diff.split("\n").map((l) => ` ${l}`).join("\n"));
2249
2714
  }
2250
2715
  if (result.conflicts.length > 3) log.print(`\n ${ui.muted(`...and ${result.conflicts.length - 3} more`)}`);
2716
+ log.print("");
2717
+ log.print(forceHint);
2251
2718
  process.exitCode = 1;
2252
2719
  return;
2253
2720
  }
2721
+ if (result.backups.length > 0) {
2722
+ log.print("");
2723
+ for (const backup of result.backups) log.print(ui.backupStatus(backup.originalPath, backup.backupPath, { dryRun }));
2724
+ }
2254
2725
  log.print("");
2255
2726
  for (const file of result.files) {
2256
2727
  const status = file.status === "overwritten" ? "updated" : file.status;
@@ -2291,25 +2762,23 @@ program.command("init").description("Initialize a new preset").argument("[direct
2291
2762
  log.print(ui.numberedList(nextSteps$1));
2292
2763
  return;
2293
2764
  }
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);
2765
+ const resolved = await resolvePlatformDirectory(targetDir, options.platform);
2766
+ if (!options.platform) {
2767
+ const check$1 = requiresPlatformFlag(resolved);
2768
+ if (check$1.required) {
2769
+ if (check$1.reason === "no_platforms") {
2770
+ const targetDirName = basename(targetDir);
2771
+ log.error(`No platform directory found in "${targetDirName}". Specify --platform (${PLATFORM_IDS.join(", ")}) or run from a platform directory.`);
2772
+ } else log.error(`Multiple platform directories found (${check$1.platforms.join(", ")}). Specify --platform to choose one.`);
2773
+ process.exit(1);
2774
+ }
2306
2775
  }
2307
2776
  const result = await initPreset({
2308
- directory: platformDir,
2777
+ directory: resolved.platformDir,
2309
2778
  name: options.name ?? defaultName,
2310
2779
  title: options.title,
2311
2780
  description: options.description,
2312
- platform,
2781
+ platform: resolved.platform,
2313
2782
  license: options.license,
2314
2783
  force: options.force
2315
2784
  });
@@ -2464,14 +2933,19 @@ program.command("publish").description("Publish a preset to the registry").argum
2464
2933
  });
2465
2934
  if (!result.success) process.exitCode = 1;
2466
2935
  }));
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) => {
2936
+ 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) => {
2937
+ const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
2468
2938
  const result = await unpublish({
2469
- slug,
2939
+ preset,
2470
2940
  platform,
2471
- version: version$1
2941
+ version: options.version
2472
2942
  });
2473
2943
  if (!result.success) process.exitCode = 1;
2474
2944
  }));
2945
+ if (process.argv.includes("--help-agent")) {
2946
+ console.log(HELP_AGENT_CONTENT);
2947
+ process.exit(0);
2948
+ }
2475
2949
  program.parseAsync(process.argv).then(() => {
2476
2950
  process.exit(process.exitCode ?? 0);
2477
2951
  }).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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.9",
51
+ "@agentrules/core": "0.0.10",
52
52
  "@clack/prompts": "^0.11.0",
53
53
  "chalk": "^5.4.1",
54
54
  "commander": "^12.1.0",