@agentrules/cli 0.0.13 → 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 +187 -15
  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, tagsSchema, 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";
@@ -712,6 +712,34 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
712
712
  }
713
713
  }
714
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
+
715
743
  //#endregion
716
744
  //#region src/lib/api/session.ts
717
745
  /**
@@ -1322,7 +1350,7 @@ function parsePresetInput(input, explicitPlatform, explicitVersion) {
1322
1350
  function resolveInstallTarget(platform, options) {
1323
1351
  const { projectDir, globalDir } = PLATFORMS[platform];
1324
1352
  if (options.directory) {
1325
- const customRoot = resolve(expandHome(options.directory));
1353
+ const customRoot = resolve(expandHome$1(options.directory));
1326
1354
  return {
1327
1355
  root: customRoot,
1328
1356
  mode: "custom",
@@ -1332,7 +1360,8 @@ function resolveInstallTarget(platform, options) {
1332
1360
  };
1333
1361
  }
1334
1362
  if (options.global) {
1335
- 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));
1336
1365
  return {
1337
1366
  root: globalRoot,
1338
1367
  mode: "global",
@@ -1362,7 +1391,7 @@ async function writeBundleFiles(bundle, target, behavior) {
1362
1391
  const destResult = computeDestinationPath(file.path, target);
1363
1392
  const destination = destResult.path;
1364
1393
  if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
1365
- const existing = await readExistingFile(destination);
1394
+ const existing = await readExistingFile$1(destination);
1366
1395
  const relativePath = relativize(destination, target.root);
1367
1396
  if (!existing) {
1368
1397
  if (!behavior.dryRun) await writeFile(destination, data);
@@ -1434,7 +1463,7 @@ function computeDestinationPath(pathInput, target) {
1434
1463
  ensureWithinRoot(destination, target.root);
1435
1464
  return { path: destination };
1436
1465
  }
1437
- async function readExistingFile(pathname) {
1466
+ async function readExistingFile$1(pathname) {
1438
1467
  try {
1439
1468
  return await readFile(pathname);
1440
1469
  } catch (error$2) {
@@ -1463,7 +1492,7 @@ function ensureWithinRoot(candidate, root) {
1463
1492
  if (candidate === root) return;
1464
1493
  if (!candidate.startsWith(normalizedRoot)) throw new Error(`Refusing to write outside of ${root}. Derived path: ${candidate}`);
1465
1494
  }
1466
- function expandHome(value) {
1495
+ function expandHome$1(value) {
1467
1496
  if (value.startsWith("~")) {
1468
1497
  const remainder = value.slice(1);
1469
1498
  if (!remainder) return homedir();
@@ -1557,7 +1586,7 @@ async function loadPreset(presetDir) {
1557
1586
  const configObj = configJson;
1558
1587
  const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
1559
1588
  const config = validatePresetConfig(configJson, identifier);
1560
- const slug = config.name;
1589
+ const name = config.name;
1561
1590
  const dirName = basename(presetDir);
1562
1591
  const isConfigInPlatformDir = isPlatformDir(dirName);
1563
1592
  let filesDir;
@@ -1586,7 +1615,7 @@ async function loadPreset(presetDir) {
1586
1615
  const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
1587
1616
  if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
1588
1617
  return {
1589
- slug,
1618
+ name,
1590
1619
  config,
1591
1620
  files,
1592
1621
  installMessage,
@@ -2019,7 +2048,7 @@ async function validatePreset(options) {
2019
2048
  warnings
2020
2049
  };
2021
2050
  }
2022
- log.debug(`Preset slug: ${preset.name}`);
2051
+ log.debug(`Preset name: ${preset.name}`);
2023
2052
  const presetDir = dirname(configPath);
2024
2053
  const platform = preset.platform;
2025
2054
  log.debug(`Checking platform: ${platform}`);
@@ -2107,7 +2136,7 @@ async function publish(options = {}) {
2107
2136
  let presetInput;
2108
2137
  try {
2109
2138
  presetInput = await loadPreset(presetDir);
2110
- log.debug(`Loaded preset "${presetInput.slug}" for platform ${presetInput.config.platform}`);
2139
+ log.debug(`Loaded preset "${presetInput.name}" for platform ${presetInput.config.platform}`);
2111
2140
  } catch (error$2) {
2112
2141
  const message = getErrorMessage(error$2);
2113
2142
  spinner$1.fail("Failed to load preset");
@@ -2152,7 +2181,7 @@ async function publish(options = {}) {
2152
2181
  log.print("");
2153
2182
  log.print(ui.header("Publish Preview"));
2154
2183
  log.print(ui.keyValue("Preset", publishInput.title));
2155
- log.print(ui.keyValue("Slug", publishInput.slug));
2184
+ log.print(ui.keyValue("Name", publishInput.name));
2156
2185
  log.print(ui.keyValue("Platform", publishInput.platform));
2157
2186
  log.print(ui.keyValue("Version", version$1 ? `${version$1}.x (auto-assigned minor)` : "1.x (auto-assigned)"));
2158
2187
  log.print(ui.keyValue("Files", `${fileCount} file${fileCount === 1 ? "" : "s"}`));
@@ -2165,7 +2194,7 @@ async function publish(options = {}) {
2165
2194
  return {
2166
2195
  success: true,
2167
2196
  preview: {
2168
- slug: publishInput.slug,
2197
+ slug: publishInput.name,
2169
2198
  platform: publishInput.platform,
2170
2199
  title: publishInput.title,
2171
2200
  totalSize: inputSize,
@@ -2296,6 +2325,116 @@ async function discoverPresetDirs(inputDir) {
2296
2325
  return presetDirs.sort();
2297
2326
  }
2298
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
+
2299
2438
  //#endregion
2300
2439
  //#region src/commands/unpublish.ts
2301
2440
  /**
@@ -2680,14 +2819,47 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
2680
2819
  log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
2681
2820
  }
2682
2821
  }).showHelpAfterError();
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) => {
2684
- 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) => {
2685
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;
2686
2858
  const spinner$1 = await log.spinner("Fetching preset...");
2687
2859
  let result;
2688
2860
  try {
2689
2861
  result = await addPreset({
2690
- preset,
2862
+ preset: item,
2691
2863
  platform,
2692
2864
  version: options.version,
2693
2865
  global: Boolean(options.global),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.0.13",
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.10",
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",