@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.
- package/dist/index.js +532 -17
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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("
|
|
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.
|
|
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 <
|
|
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.
|
|
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.
|
|
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",
|