@agentrules/cli 0.0.12 → 0.0.14

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 (2) hide show
  1. package/dist/index.js +532 -17
  2. package/package.json +2 -2
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, getInstallPath, 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";
@@ -315,6 +315,49 @@ function relativeTime(date) {
315
315
  if (minutes > 0) return `${minutes}m ago`;
316
316
  return "just now";
317
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
+ }
318
361
  const ui = {
319
362
  theme,
320
363
  symbols,
@@ -354,7 +397,8 @@ const ui = {
354
397
  stripAnsi,
355
398
  truncate,
356
399
  formatBytes: formatBytes$1,
357
- relativeTime
400
+ relativeTime,
401
+ fileTree
358
402
  };
359
403
 
360
404
  //#endregion
@@ -668,6 +712,34 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
668
712
  }
669
713
  }
670
714
 
715
+ //#endregion
716
+ //#region src/lib/api/rule.ts
717
+ async function getRule(baseUrl, slug) {
718
+ const url = `${baseUrl}${API_ENDPOINTS.rule.get(slug)}`;
719
+ log.debug(`GET ${url}`);
720
+ try {
721
+ const response = await fetch(url);
722
+ log.debug(`Response status: ${response.status}`);
723
+ if (!response.ok) {
724
+ const errorData = await response.json();
725
+ return {
726
+ success: false,
727
+ error: errorData.error || `HTTP ${response.status}`
728
+ };
729
+ }
730
+ const data = await response.json();
731
+ return {
732
+ success: true,
733
+ data
734
+ };
735
+ } catch (error$2) {
736
+ return {
737
+ success: false,
738
+ error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
739
+ };
740
+ }
741
+ }
742
+
671
743
  //#endregion
672
744
  //#region src/lib/api/session.ts
673
745
  /**
@@ -1278,7 +1350,7 @@ function parsePresetInput(input, explicitPlatform, explicitVersion) {
1278
1350
  function resolveInstallTarget(platform, options) {
1279
1351
  const { projectDir, globalDir } = PLATFORMS[platform];
1280
1352
  if (options.directory) {
1281
- const customRoot = resolve(expandHome(options.directory));
1353
+ const customRoot = resolve(expandHome$1(options.directory));
1282
1354
  return {
1283
1355
  root: customRoot,
1284
1356
  mode: "custom",
@@ -1288,7 +1360,8 @@ function resolveInstallTarget(platform, options) {
1288
1360
  };
1289
1361
  }
1290
1362
  if (options.global) {
1291
- const globalRoot = resolve(expandHome(globalDir));
1363
+ if (!globalDir) throw new Error(`Platform "${platform}" does not support global installation`);
1364
+ const globalRoot = resolve(expandHome$1(globalDir));
1292
1365
  return {
1293
1366
  root: globalRoot,
1294
1367
  mode: "global",
@@ -1318,7 +1391,7 @@ async function writeBundleFiles(bundle, target, behavior) {
1318
1391
  const destResult = computeDestinationPath(file.path, target);
1319
1392
  const destination = destResult.path;
1320
1393
  if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
1321
- const existing = await readExistingFile(destination);
1394
+ const existing = await readExistingFile$1(destination);
1322
1395
  const relativePath = relativize(destination, target.root);
1323
1396
  if (!existing) {
1324
1397
  if (!behavior.dryRun) await writeFile(destination, data);
@@ -1390,7 +1463,7 @@ function computeDestinationPath(pathInput, target) {
1390
1463
  ensureWithinRoot(destination, target.root);
1391
1464
  return { path: destination };
1392
1465
  }
1393
- async function readExistingFile(pathname) {
1466
+ async function readExistingFile$1(pathname) {
1394
1467
  try {
1395
1468
  return await readFile(pathname);
1396
1469
  } catch (error$2) {
@@ -1419,7 +1492,7 @@ function ensureWithinRoot(candidate, root) {
1419
1492
  if (candidate === root) return;
1420
1493
  if (!candidate.startsWith(normalizedRoot)) throw new Error(`Refusing to write outside of ${root}. Derived path: ${candidate}`);
1421
1494
  }
1422
- function expandHome(value) {
1495
+ function expandHome$1(value) {
1423
1496
  if (value.startsWith("~")) {
1424
1497
  const remainder = value.slice(1);
1425
1498
  if (!remainder) return homedir();
@@ -1513,7 +1586,7 @@ async function loadPreset(presetDir) {
1513
1586
  const configObj = configJson;
1514
1587
  const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
1515
1588
  const config = validatePresetConfig(configJson, identifier);
1516
- const slug = config.name;
1589
+ const name = config.name;
1517
1590
  const dirName = basename(presetDir);
1518
1591
  const isConfigInPlatformDir = isPlatformDir(dirName);
1519
1592
  let filesDir;
@@ -1542,7 +1615,7 @@ async function loadPreset(presetDir) {
1542
1615
  const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
1543
1616
  if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
1544
1617
  return {
1545
- slug,
1618
+ name,
1546
1619
  config,
1547
1620
  files,
1548
1621
  installMessage,
@@ -1729,6 +1802,7 @@ async function initPreset(options) {
1729
1802
  title,
1730
1803
  version: 1,
1731
1804
  description,
1805
+ tags: options.tags ?? [],
1732
1806
  license,
1733
1807
  platform
1734
1808
  };
@@ -1773,6 +1847,20 @@ function check(schema) {
1773
1847
  //#region src/commands/preset/init-interactive.ts
1774
1848
  const DEFAULT_PRESET_NAME = "my-preset";
1775
1849
  /**
1850
+ * Parse comma-separated tags string into array
1851
+ */
1852
+ function parseTags(input) {
1853
+ return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
1854
+ }
1855
+ /**
1856
+ * Validator for comma-separated tags input
1857
+ */
1858
+ function checkTags(value) {
1859
+ const tags = parseTags(value);
1860
+ const result = tagsSchema.safeParse(tags);
1861
+ if (!result.success) return result.error.issues[0]?.message;
1862
+ }
1863
+ /**
1776
1864
  * Run interactive init flow with clack prompts.
1777
1865
  *
1778
1866
  * If platformDir is provided, init directly in that directory.
@@ -1855,6 +1943,11 @@ async function initInteractive(options) {
1855
1943
  validate: check(descriptionSchema)
1856
1944
  });
1857
1945
  },
1946
+ tags: () => p.text({
1947
+ message: "Tags (comma-separated, at least one)",
1948
+ placeholder: "e.g., typescript, testing, react",
1949
+ validate: checkTags
1950
+ }),
1858
1951
  license: async () => {
1859
1952
  const defaultLicense = licenseOption ?? "MIT";
1860
1953
  const choice = await p.select({
@@ -1895,6 +1988,7 @@ async function initInteractive(options) {
1895
1988
  name: result.name,
1896
1989
  title: result.title,
1897
1990
  description: result.description,
1991
+ tags: parseTags(result.tags),
1898
1992
  platform: selectedPlatform,
1899
1993
  license: result.license,
1900
1994
  force
@@ -1954,7 +2048,7 @@ async function validatePreset(options) {
1954
2048
  warnings
1955
2049
  };
1956
2050
  }
1957
- log.debug(`Preset slug: ${preset.name}`);
2051
+ log.debug(`Preset name: ${preset.name}`);
1958
2052
  const presetDir = dirname(configPath);
1959
2053
  const platform = preset.platform;
1960
2054
  log.debug(`Checking platform: ${platform}`);
@@ -2042,7 +2136,7 @@ async function publish(options = {}) {
2042
2136
  let presetInput;
2043
2137
  try {
2044
2138
  presetInput = await loadPreset(presetDir);
2045
- log.debug(`Loaded preset "${presetInput.slug}" for platform ${presetInput.config.platform}`);
2139
+ log.debug(`Loaded preset "${presetInput.name}" for platform ${presetInput.config.platform}`);
2046
2140
  } catch (error$2) {
2047
2141
  const message = getErrorMessage(error$2);
2048
2142
  spinner$1.fail("Failed to load preset");
@@ -2087,17 +2181,20 @@ async function publish(options = {}) {
2087
2181
  log.print("");
2088
2182
  log.print(ui.header("Publish Preview"));
2089
2183
  log.print(ui.keyValue("Preset", publishInput.title));
2090
- log.print(ui.keyValue("Slug", publishInput.slug));
2184
+ log.print(ui.keyValue("Name", publishInput.name));
2091
2185
  log.print(ui.keyValue("Platform", publishInput.platform));
2092
2186
  log.print(ui.keyValue("Version", version$1 ? `${version$1}.x (auto-assigned minor)` : "1.x (auto-assigned)"));
2093
2187
  log.print(ui.keyValue("Files", `${fileCount} file${fileCount === 1 ? "" : "s"}`));
2094
2188
  log.print(ui.keyValue("Size", formatBytes(inputSize)));
2095
2189
  log.print("");
2190
+ log.print(ui.header("Files to publish", fileCount));
2191
+ log.print(ui.fileTree(publishInput.files));
2192
+ log.print("");
2096
2193
  log.print(ui.hint("Run without --dry-run to publish."));
2097
2194
  return {
2098
2195
  success: true,
2099
2196
  preview: {
2100
- slug: publishInput.slug,
2197
+ slug: publishInput.name,
2101
2198
  platform: publishInput.platform,
2102
2199
  title: publishInput.title,
2103
2200
  totalSize: inputSize,
@@ -2130,6 +2227,9 @@ async function publish(options = {}) {
2130
2227
  const { data } = result;
2131
2228
  const action = data.isNewPreset ? "Published new preset" : "Published";
2132
2229
  spinner$1.success(`${action} ${ui.code(data.slug)} ${ui.version(data.version)} (${data.platform})`);
2230
+ log.print("");
2231
+ log.print(ui.header("Published files", fileCount));
2232
+ log.print(ui.fileTree(publishInput.files));
2133
2233
  const presetName$1 = `${data.slug}.${data.platform}`;
2134
2234
  const presetRegistryUrl = `${ctx.registry.url}preset/${presetName$1}`;
2135
2235
  log.info("");
@@ -2225,6 +2325,116 @@ async function discoverPresetDirs(inputDir) {
2225
2325
  return presetDirs.sort();
2226
2326
  }
2227
2327
 
2328
+ //#endregion
2329
+ //#region src/commands/rule/add.ts
2330
+ async function addRule(options) {
2331
+ const ctx = useAppContext();
2332
+ const dryRun = Boolean(options.dryRun);
2333
+ log.debug(`Fetching rule: ${options.slug}`);
2334
+ const result = await getRule(ctx.registry.url, options.slug);
2335
+ if (!result.success) throw new Error(result.error);
2336
+ const rule = result.data;
2337
+ const platform = rule.platform;
2338
+ const targetPath = resolveTargetPath(platform, rule.type, rule.slug, {
2339
+ global: options.global,
2340
+ directory: options.directory
2341
+ });
2342
+ log.debug(`Target path: ${targetPath}`);
2343
+ const existing = await readExistingFile(targetPath);
2344
+ if (existing !== null) {
2345
+ if (existing === rule.content) return {
2346
+ slug: rule.slug,
2347
+ platform: rule.platform,
2348
+ type: rule.type,
2349
+ title: rule.title,
2350
+ targetPath,
2351
+ status: "unchanged",
2352
+ dryRun
2353
+ };
2354
+ if (!options.force) return {
2355
+ slug: rule.slug,
2356
+ platform: rule.platform,
2357
+ type: rule.type,
2358
+ title: rule.title,
2359
+ targetPath,
2360
+ status: "conflict",
2361
+ dryRun
2362
+ };
2363
+ if (!dryRun) {
2364
+ await mkdir(dirname(targetPath), { recursive: true });
2365
+ await writeFile(targetPath, rule.content, "utf-8");
2366
+ }
2367
+ return {
2368
+ slug: rule.slug,
2369
+ platform: rule.platform,
2370
+ type: rule.type,
2371
+ title: rule.title,
2372
+ targetPath,
2373
+ status: "overwritten",
2374
+ dryRun
2375
+ };
2376
+ }
2377
+ if (!dryRun) {
2378
+ await mkdir(dirname(targetPath), { recursive: true });
2379
+ await writeFile(targetPath, rule.content, "utf-8");
2380
+ }
2381
+ return {
2382
+ slug: rule.slug,
2383
+ platform: rule.platform,
2384
+ type: rule.type,
2385
+ title: rule.title,
2386
+ targetPath,
2387
+ status: "created",
2388
+ dryRun
2389
+ };
2390
+ }
2391
+ function resolveTargetPath(platform, type, slug, options) {
2392
+ const location = options.global ? "global" : "project";
2393
+ const pathTemplate = getInstallPath(platform, type, slug, location);
2394
+ if (!pathTemplate) {
2395
+ const locationLabel = options.global ? "globally" : "to a project";
2396
+ throw new Error(`Rule type "${type}" cannot be installed ${locationLabel} for platform "${platform}"`);
2397
+ }
2398
+ if (options.directory) {
2399
+ const resolvedTemplate = pathTemplate.replace("{name}", slug);
2400
+ const filename = resolvedTemplate.split("/").pop() ?? `${slug}.md`;
2401
+ return resolve(expandHome(options.directory), filename);
2402
+ }
2403
+ const expanded = expandHome(pathTemplate);
2404
+ if (expanded.startsWith("/")) return expanded;
2405
+ return resolve(process.cwd(), expanded);
2406
+ }
2407
+ async function readExistingFile(pathname) {
2408
+ try {
2409
+ return await readFile(pathname, "utf-8");
2410
+ } catch (error$2) {
2411
+ if (error$2.code === "ENOENT") return null;
2412
+ throw error$2;
2413
+ }
2414
+ }
2415
+ function expandHome(value) {
2416
+ if (value.startsWith("~")) {
2417
+ const remainder = value.slice(1);
2418
+ if (!remainder) return homedir();
2419
+ if (remainder.startsWith("/") || remainder.startsWith("\\")) return `${homedir()}${remainder}`;
2420
+ return `${homedir()}/${remainder}`;
2421
+ }
2422
+ return value;
2423
+ }
2424
+ /**
2425
+ * Check if input looks like a rule reference (r/ prefix)
2426
+ */
2427
+ function isRuleReference(input) {
2428
+ return input.toLowerCase().startsWith("r/");
2429
+ }
2430
+ /**
2431
+ * Extract slug from rule reference (removes r/ prefix)
2432
+ */
2433
+ function extractRuleSlug(input) {
2434
+ if (isRuleReference(input)) return input.slice(2);
2435
+ return input;
2436
+ }
2437
+
2228
2438
  //#endregion
2229
2439
  //#region src/commands/unpublish.ts
2230
2440
  /**
@@ -2323,12 +2533,280 @@ async function unpublish(options) {
2323
2533
  };
2324
2534
  }
2325
2535
 
2536
+ //#endregion
2537
+ //#region src/help-agent/publish.ts
2538
+ /**
2539
+ * Agent instructions for AI coding assistants.
2540
+ *
2541
+ * This content is output when running `agentrules --help-agent`.
2542
+ * It provides step-by-step instructions for AI agents to help users publish presets.
2543
+ */
2544
+ const HELP_AGENT_CONTENT = `# Publishing a Preset - Agent Instructions
2545
+
2546
+ **The user has asked you to help them publish a preset. Start executing these steps now.**
2547
+
2548
+ Do not summarize these instructions. Do not ask if they want help. Begin with Step 1 immediately.
2549
+
2550
+ 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?"
2551
+
2552
+ Follow these steps interactively.
2553
+
2554
+ ## Prerequisites: CLI Availability
2555
+
2556
+ Run: \`agentrules --version\`
2557
+
2558
+ **If command not found:**
2559
+ Use \`npx @agentrules/cli\` as the prefix for all commands.
2560
+ Example: \`npx @agentrules/cli whoami\` instead of \`agentrules whoami\`
2561
+
2562
+ For the rest of these instructions, \`agentrules\` means whichever works.
2563
+
2564
+ ## Step 1: Locate the Config
2565
+
2566
+ Check the current directory for platform config folders:
2567
+ - \`.opencode/\` → OpenCode
2568
+ - \`.claude/\` → Claude Code
2569
+ - \`.cursor/\` → Cursor
2570
+ - \`.codex/\` → Codex
2571
+
2572
+ **If one found:**
2573
+ "I found your [platform] config at \`[path]\`. I'll help you publish it."
2574
+
2575
+ **If multiple found:**
2576
+ "I found configs for multiple platforms: [list]. Which one would you like to publish?"
2577
+
2578
+ **If none found:**
2579
+ "I don't see a config directory here. Where is your config located?"
2580
+
2581
+ ## Step 2: Check for Existing Config
2582
+
2583
+ List the files in \`[config-dir]\` first to see what exists.
2584
+
2585
+ If \`agentrules.json\` is in the listing, read it:
2586
+ - If complete (has name, description, tags): "You already have a preset configured: '[name]'. Ready to republish?" → Skip to Step 4 if yes
2587
+ - If missing required fields: Help them add the missing fields
2588
+
2589
+ If \`agentrules.json\` is not in the listing, continue to Step 3.
2590
+
2591
+ ### Check for ignorable files
2592
+
2593
+ While reviewing the file listing, look for files/folders that probably shouldn't be published.
2594
+
2595
+ **Already ignored by default** (don't suggest these):
2596
+ - node_modules
2597
+ - .git
2598
+ - .DS_Store
2599
+ - *.lock
2600
+ - package-lock.json
2601
+ - bun.lockb
2602
+ - pnpm-lock.yaml
2603
+
2604
+ **Commonly ignorable** (suggest adding to \`ignore\` field if present):
2605
+ - build/, dist/, out/ (build output)
2606
+ - .env, .env.* (environment files)
2607
+ - *.log (log files)
2608
+ - tmp/, temp/ (temporary files)
2609
+ - coverage/ (test coverage)
2610
+ - .cache/, .turbo/ (cache directories)
2611
+
2612
+ 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?"
2613
+
2614
+ If yes, include them in the \`ignore\` array when creating agentrules.json.
2615
+
2616
+ ## Step 3: Create agentrules.json
2617
+
2618
+ 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?
2619
+
2620
+ ### 3a. Analyze their config
2621
+
2622
+ 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.
2623
+
2624
+ Look for:
2625
+ - Technologies and frameworks mentioned
2626
+ - The main purpose or rules being enforced
2627
+ - Who would benefit from this setup
2628
+
2629
+ ### 3b. Generate all suggestions at once
2630
+
2631
+ Based on your analysis, generate suggestions for ALL fields:
2632
+
2633
+ - **Name**: lowercase, hyphens, based on repo/directory/theme (1-64 chars)
2634
+ - **Title**: Title-cased, compelling name
2635
+ - **Description**: Value-focused - who is this for, what problem does it solve? (max 500 chars)
2636
+ - **Tags**: For discovery - technologies, frameworks, use cases (1-10 tags)
2637
+ - **Features**: Key benefits, not just capabilities (optional, up to 5)
2638
+ - **License**: Default to MIT
2639
+
2640
+ ### 3c. Present a single summary
2641
+
2642
+ Show everything in one concise output. Put each field name on its own line, followed by the value on the next line:
2643
+
2644
+ "Based on your config, here's what I'd suggest:
2645
+
2646
+ **Name**
2647
+ typescript-strict-rules
2648
+
2649
+ **Title**
2650
+ TypeScript Strict Rules
2651
+
2652
+ **Description**
2653
+ Opinionated TypeScript rules that catch common bugs at dev time and enforce consistent patterns across your team.
2654
+
2655
+ **Tags**
2656
+ typescript, strict, type-safety
2657
+
2658
+ **Features**
2659
+ - Catches null/undefined errors before production
2660
+ - Enforces consistent code style without manual review
2661
+
2662
+ **License**
2663
+ MIT
2664
+
2665
+ Let me know if you'd like to change anything, or say 'looks good' to continue."
2666
+
2667
+ ### 3d. Handle feedback
2668
+
2669
+ If the user wants changes (e.g., "change the description" or "add a react tag"), update those fields and show the summary again.
2670
+
2671
+ When they approve, proceed to create the file.
2672
+
2673
+ ### Guidelines for good suggestions
2674
+
2675
+ **Description** should answer: What problem does this solve? Who benefits?
2676
+ - Good: "Opinionated TypeScript rules that catch common bugs at dev time and enforce consistent patterns across your team."
2677
+ - Bad: "TypeScript rules with strict settings." (too vague, no value prop)
2678
+
2679
+ **Features** should describe benefits, not capabilities:
2680
+ - Good: "Catches null/undefined errors before they hit production"
2681
+ - Bad: "Strict null checks" (feature, not benefit)
2682
+
2683
+ **Tags** should help with discovery:
2684
+ - Technologies: typescript, python, rust, go
2685
+ - Frameworks: react, nextjs, fastapi, django
2686
+ - Use cases: code-review, testing, security, onboarding
2687
+
2688
+ ### 3e. Create the file
2689
+
2690
+ Write \`[config-dir]/agentrules.json\`:
2691
+
2692
+ \`\`\`json
2693
+ {
2694
+ "$schema": "https://agentrules.directory/schema/agentrules.json",
2695
+ "name": "[name]",
2696
+ "title": "[title]",
2697
+ "version": 1,
2698
+ "description": "[description]",
2699
+ "tags": ["tag1", "tag2"],
2700
+ "license": "[license]",
2701
+ "platform": "[detected-platform]"
2702
+ }
2703
+ \`\`\`
2704
+
2705
+ Include \`"features": [...]\` only if provided.
2706
+ Include \`"ignore": ["pattern1", "pattern2"]\` if the user agreed to ignore certain files.
2707
+
2708
+ ### 3f. Show the file and get approval
2709
+
2710
+ After writing the file, show the user:
2711
+
2712
+ "I've created the config file at \`[config-dir]/agentrules.json\`:
2713
+
2714
+ \`\`\`json
2715
+ [show the actual file contents]
2716
+ \`\`\`
2717
+
2718
+ Take a look and let me know if you'd like to change anything, or say 'looks good' to continue."
2719
+
2720
+ Wait for approval before proceeding. If they want changes, edit the file and show it again.
2721
+
2722
+ Then validate: \`agentrules validate [config-dir]\`
2723
+
2724
+ If errors, fix and retry.
2725
+
2726
+ ## Step 4: Login
2727
+
2728
+ Run: \`agentrules whoami\`
2729
+
2730
+ **If output shows "loggedIn": false or "Not logged in":**
2731
+ "You need to log in to publish."
2732
+ Run: \`agentrules login\`
2733
+ This opens a browser for authentication. Wait for completion.
2734
+
2735
+ ## Step 5: Preview with Dry Run
2736
+
2737
+ Run: \`agentrules publish [config-dir] --dry-run\`
2738
+
2739
+ Show the user the preview:
2740
+ "Here's what will be published:
2741
+ - Name: [name]
2742
+ - Platform: [platform]
2743
+ - Files: [count] files ([size])
2744
+
2745
+ Ready to publish?"
2746
+
2747
+ If they want changes, help edit agentrules.json and re-run dry-run.
2748
+
2749
+ ## Step 6: Publish
2750
+
2751
+ Run: \`agentrules publish [config-dir]\`
2752
+
2753
+ **If successful:**
2754
+ Show the URL from the output:
2755
+
2756
+ "Published! Your preset is live at: [url]
2757
+
2758
+ Share with others:
2759
+ \`\`\`
2760
+ npx @agentrules/cli add [name]
2761
+ \`\`\`"
2762
+
2763
+ **If "already exists" error:**
2764
+ Ask if they want to increment the \`version\` field in agentrules.json and retry.
2765
+
2766
+ **If other errors:**
2767
+ Show the error and suggest: \`agentrules validate [config-dir]\`
2768
+
2769
+ ## Step 7: Tips
2770
+
2771
+ **If you used \`npx @agentrules/cli\`:**
2772
+ "Tip: Install globally to skip the npx download:
2773
+ \`\`\`
2774
+ npm i -g @agentrules/cli
2775
+ \`\`\`"
2776
+
2777
+ ## Notes for Agent
2778
+
2779
+ - Be conversational and helpful
2780
+ - Explain what you're doing at each step
2781
+ - Use \`agentrules validate\` to check your work after any config changes
2782
+ - Remember whether you used npx for the tip at the end
2783
+ - If the user seems confused, explain that agentrules is a registry for sharing AI coding configs
2784
+ - The config file must be inside the platform directory (e.g., \`.opencode/agentrules.json\`)
2785
+
2786
+ ## Schema Reference
2787
+
2788
+ **Required fields:**
2789
+ - \`name\`: slug format (lowercase, hyphens, 1-64 chars)
2790
+ - \`title\`: 1-80 characters
2791
+ - \`description\`: 1-500 characters
2792
+ - \`tags\`: array, 1-10 items, each lowercase/hyphens, max 35 chars
2793
+ - \`license\`: SPDX identifier (e.g., "MIT")
2794
+ - \`platform\`: one of \`opencode\`, \`claude\`, \`cursor\`, \`codex\`
2795
+
2796
+ **Optional fields:**
2797
+ - \`$schema\`: JSON schema URL for validation
2798
+ - \`version\`: major version number (default: 1)
2799
+ - \`features\`: array, max 5 items, each max 100 chars
2800
+ - \`path\`: custom path to files (advanced use)
2801
+ - \`ignore\`: patterns to exclude from bundle
2802
+ `;
2803
+
2326
2804
  //#endregion
2327
2805
  //#region src/index.ts
2328
2806
  const require = createRequire(import.meta.url);
2329
2807
  const packageJson = require("../package.json");
2330
2808
  const program = new Command();
2331
- 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) => {
2809
+ 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) => {
2332
2810
  const opts = thisCommand.opts();
2333
2811
  if (opts.verbose) log.setVerbose(true);
2334
2812
  const actionOpts = actionCommand.opts();
@@ -2341,14 +2819,47 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
2341
2819
  log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
2342
2820
  }
2343
2821
  }).showHelpAfterError();
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) => {
2345
- const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
2822
+ program.command("add <item>").description("Download and install a preset or rule from the registry (use r/<slug> for rules)").option("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").option("-V, --version <version>", "Install a specific version (presets only)").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 (presets only)").option("--no-backup", "Don't backup files before overwriting (use with --force)").action(handle(async (item, options) => {
2346
2823
  const dryRun = Boolean(options.dryRun);
2824
+ if (isRuleReference(item)) {
2825
+ const slug = extractRuleSlug(item);
2826
+ const spinner$2 = await log.spinner(`Fetching rule "${slug}"...`);
2827
+ try {
2828
+ const result$1 = await addRule({
2829
+ slug,
2830
+ global: Boolean(options.global),
2831
+ directory: options.dir,
2832
+ force: Boolean(options.force || options.yes),
2833
+ dryRun
2834
+ });
2835
+ spinner$2.stop();
2836
+ if (result$1.status === "conflict") {
2837
+ log.error(`File already exists: ${result$1.targetPath}. Use ${ui.command("--force")} to overwrite.`);
2838
+ process.exitCode = 1;
2839
+ return;
2840
+ }
2841
+ if (result$1.status === "unchanged") {
2842
+ log.info(`Already up to date: ${ui.path(result$1.targetPath)}`);
2843
+ return;
2844
+ }
2845
+ const verb$1 = dryRun ? "Would install" : "Installed";
2846
+ const action = result$1.status === "overwritten" ? "updated" : "created";
2847
+ log.print(ui.fileStatus(action, result$1.targetPath, { dryRun }));
2848
+ log.print("");
2849
+ log.success(`${verb$1} ${ui.bold(result$1.title)} ${ui.muted(`(${result$1.platform}/${result$1.type})`)}`);
2850
+ if (dryRun) log.print(ui.hint("\nDry run complete. No files were written."));
2851
+ } catch (err) {
2852
+ spinner$2.stop();
2853
+ throw err;
2854
+ }
2855
+ return;
2856
+ }
2857
+ const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
2347
2858
  const spinner$1 = await log.spinner("Fetching preset...");
2348
2859
  let result;
2349
2860
  try {
2350
2861
  result = await addPreset({
2351
- preset,
2862
+ preset: item,
2352
2863
  platform,
2353
2864
  version: options.version,
2354
2865
  global: Boolean(options.global),
@@ -2603,6 +3114,10 @@ program.command("unpublish").description("Remove a preset version from the regis
2603
3114
  });
2604
3115
  if (!result.success) process.exitCode = 1;
2605
3116
  }));
3117
+ if (process.argv.includes("--help-agent")) {
3118
+ console.log(HELP_AGENT_CONTENT);
3119
+ process.exit(0);
3120
+ }
2606
3121
  program.parseAsync(process.argv).then(() => {
2607
3122
  process.exit(process.exitCode ?? 0);
2608
3123
  }).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
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.11",
52
52
  "@clack/prompts": "^0.11.0",
53
53
  "chalk": "^5.4.1",
54
54
  "commander": "^12.1.0",