@agentrules/cli 0.3.0 → 0.3.2

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 +2 -1
  2. package/dist/index.js +527 -421
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -193,8 +193,9 @@ agentrules publish [path] [options]
193
193
  # Publish current directory
194
194
  agentrules publish
195
195
 
196
- # Publish a specific rule
196
+ # Publish a specific directory or file (interactive if no agentrules.json)
197
197
  agentrules publish ./my-rule
198
+ agentrules publish .claude/commands/deploy.md
198
199
 
199
200
  # Publish to major version 2
200
201
  agentrules publish --version 2
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { chmod, constants as constants$1 } from "fs";
9
9
  import * as client from "openid-client";
10
10
  import { promisify } from "util";
11
11
  import { exec } from "child_process";
12
+ import * as p$2 from "@clack/prompts";
12
13
  import * as p$1 from "@clack/prompts";
13
14
  import * as p from "@clack/prompts";
14
15
 
@@ -34,6 +35,11 @@ const RULE_TYPE_TUPLE = [
34
35
 
35
36
  //#endregion
36
37
  //#region ../core/src/platform/config.ts
38
+ /**
39
+ * Placeholder for user's home directory in path templates.
40
+ * Consumers (e.g., CLI) are responsible for expanding this at runtime.
41
+ */
42
+ const USER_HOME_DIR_PLACEHOLDER = "{userHomeDir}";
37
43
  const PLATFORM_IDS = PLATFORM_ID_TUPLE;
38
44
  /**
39
45
  * Platform configuration including supported types and install paths.
@@ -42,7 +48,7 @@ const PLATFORMS = {
42
48
  opencode: {
43
49
  label: "OpenCode",
44
50
  platformDir: ".opencode",
45
- globalDir: "~/.config/opencode",
51
+ globalDir: "{userHomeDir}/.config/opencode",
46
52
  types: {
47
53
  instruction: {
48
54
  description: "Project instructions",
@@ -74,7 +80,7 @@ const PLATFORMS = {
74
80
  claude: {
75
81
  label: "Claude Code",
76
82
  platformDir: ".claude",
77
- globalDir: "~/.claude",
83
+ globalDir: "{userHomeDir}/.claude",
78
84
  types: {
79
85
  instruction: {
80
86
  description: "Project instructions",
@@ -101,7 +107,7 @@ const PLATFORMS = {
101
107
  cursor: {
102
108
  label: "Cursor",
103
109
  platformDir: ".cursor",
104
- globalDir: "~/.cursor",
110
+ globalDir: "{userHomeDir}/.cursor",
105
111
  types: {
106
112
  instruction: {
107
113
  description: "Project instructions",
@@ -128,7 +134,7 @@ const PLATFORMS = {
128
134
  codex: {
129
135
  label: "Codex",
130
136
  platformDir: ".codex",
131
- globalDir: "~/.codex",
137
+ globalDir: "{userHomeDir}/.codex",
132
138
  types: {
133
139
  instruction: {
134
140
  description: "Project instructions",
@@ -182,7 +188,7 @@ function getInstallPath({ platform, type, name, scope = "project" }) {
182
188
  * Returns the path relative to the platform's root directory.
183
189
  *
184
190
  * Example: For codex instruction with scope="global", returns "AGENTS.md"
185
- * (not "~/.codex/AGENTS.md")
191
+ * (not the full path with {userHomeDir})
186
192
  */
187
193
  function getRelativeInstallPath({ platform, type, name, scope = "project" }) {
188
194
  const typeConfig = getTypeConfig(platform, type);
@@ -1989,7 +1995,7 @@ const $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
1989
1995
  defineLazy(inst._zod, "pattern", () => {
1990
1996
  if (def.options.every((o) => o._zod.pattern)) {
1991
1997
  const patterns = def.options.map((o) => o._zod.pattern);
1992
- return new RegExp(`^(${patterns.map((p$2) => cleanRegex(p$2.source)).join("|")})$`);
1998
+ return new RegExp(`^(${patterns.map((p$3) => cleanRegex(p$3.source)).join("|")})$`);
1993
1999
  }
1994
2000
  return void 0;
1995
2001
  });
@@ -2368,9 +2374,9 @@ var $ZodRegistry = class {
2368
2374
  return this;
2369
2375
  }
2370
2376
  get(schema) {
2371
- const p$2 = schema._zod.parent;
2372
- if (p$2) {
2373
- const pm = { ...this.get(p$2) ?? {} };
2377
+ const p$3 = schema._zod.parent;
2378
+ if (p$3) {
2379
+ const pm = { ...this.get(p$3) ?? {} };
2374
2380
  delete pm.id;
2375
2381
  const f = {
2376
2382
  ...pm,
@@ -3504,6 +3510,21 @@ const COMMON_LICENSES = [
3504
3510
  ];
3505
3511
  const licenseSchema = string().trim().min(1, "License is required").max(128, "License must be 128 characters or less");
3506
3512
  const pathSchema = string().trim().min(1);
3513
+ /**
3514
+ * Validate a bundle file path for security.
3515
+ * Rejects paths that could escape the install directory.
3516
+ *
3517
+ * @returns true if path is safe, false otherwise
3518
+ */
3519
+ function isValidBundlePath(path$1) {
3520
+ if (path$1.includes("..")) return false;
3521
+ if (path$1.startsWith("/")) return false;
3522
+ if (path$1.startsWith("~")) return false;
3523
+ if (path$1.includes("/~/") || path$1.includes("\\~\\")) return false;
3524
+ if (path$1.includes("{userHomeDir}")) return false;
3525
+ return true;
3526
+ }
3527
+ const bundlePathSchema = string().min(1).refine(isValidBundlePath, { message: "Path must be relative without traversal (no .., ~, absolute paths, or {userHomeDir})" });
3507
3528
  const ignorePatternSchema = string().trim().min(1, "Ignore pattern cannot be empty");
3508
3529
  const ignoreSchema = array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
3509
3530
  /**
@@ -3548,7 +3569,7 @@ const ruleConfigSchema = object({
3548
3569
  platforms: array(platformEntrySchema).min(1, "At least one platform is required")
3549
3570
  }).strict();
3550
3571
  const bundledFileSchema = object({
3551
- path: string().min(1),
3572
+ path: bundlePathSchema,
3552
3573
  size: number().int().nonnegative(),
3553
3574
  checksum: string().length(64),
3554
3575
  content: string()
@@ -4375,8 +4396,8 @@ function command(cmd) {
4375
4396
  return theme.command(cmd);
4376
4397
  }
4377
4398
  /** Format a file path */
4378
- function path(p$2) {
4379
- return theme.path(p$2);
4399
+ function path(p$3) {
4400
+ return theme.path(p$3);
4380
4401
  }
4381
4402
  /** Format as muted/secondary text */
4382
4403
  function muted(text) {
@@ -4694,6 +4715,22 @@ function fileTree(files, options) {
4694
4715
  });
4695
4716
  return lines.join("\n");
4696
4717
  }
4718
+ function rulePreview(options) {
4719
+ const lines = [];
4720
+ lines.push(header(options.header));
4721
+ if (options.path) lines.push(keyValue(options.pathLabel ?? "Path", path(options.path)));
4722
+ lines.push(keyValue("Name", code(options.name)));
4723
+ lines.push(keyValue("Title", options.title));
4724
+ lines.push(keyValue("Description", options.description || dim("—")));
4725
+ lines.push(keyValue("Platforms", options.platforms.join(", ")));
4726
+ if (options.type) lines.push(keyValue("Type", options.type));
4727
+ if (options.tags && options.tags.length > 0) lines.push(keyValue("Tags", options.tags.join(", ")));
4728
+ else if (options.showHints) lines.push(keyValue("Tags", dim("— (add to improve discoverability)")));
4729
+ if (options.features && options.features.length > 0) lines.push(keyValue("Features", options.features.join(", ")));
4730
+ else if (options.showHints) lines.push(keyValue("Features", dim("— (highlight what this rule does)")));
4731
+ if (options.license) lines.push(keyValue("License", options.license));
4732
+ return lines.join("\n");
4733
+ }
4697
4734
  const ui = {
4698
4735
  theme,
4699
4736
  symbols,
@@ -4730,6 +4767,7 @@ const ui = {
4730
4767
  fileCounts,
4731
4768
  hint,
4732
4769
  link,
4770
+ rulePreview,
4733
4771
  stripAnsi,
4734
4772
  truncate,
4735
4773
  formatBytes: formatBytes$1,
@@ -5593,7 +5631,7 @@ function resolveInstallTarget(platform, type, name, options) {
5593
5631
  const { platformDir, globalDir } = PLATFORMS[platform];
5594
5632
  if (options.global) {
5595
5633
  if (!globalDir) throw new Error(`Platform "${platform}" does not support global installation`);
5596
- const globalRoot = resolve(expandHome(globalDir));
5634
+ const globalRoot = resolve(expandUserHomeDir(globalDir));
5597
5635
  return {
5598
5636
  root: globalRoot,
5599
5637
  mode: "global",
@@ -5605,7 +5643,7 @@ function resolveInstallTarget(platform, type, name, options) {
5605
5643
  label: `global path ${globalRoot}`
5606
5644
  };
5607
5645
  }
5608
- const projectRoot = options.directory ? resolve(expandHome(options.directory)) : process.cwd();
5646
+ const projectRoot = options.directory ? resolve(expandUserHomeDir(options.directory)) : process.cwd();
5609
5647
  const label = options.directory ? `directory ${projectRoot}` : `project root ${projectRoot}`;
5610
5648
  return {
5611
5649
  root: projectRoot,
@@ -5705,15 +5743,20 @@ function ensureWithinRoot(candidate, root) {
5705
5743
  if (candidate === root) return;
5706
5744
  if (!candidate.startsWith(normalizedRoot)) throw new Error(`Refusing to write outside of ${root}. Derived path: ${candidate}`);
5707
5745
  }
5708
- function expandHome(value) {
5709
- if (value.startsWith("~")) {
5710
- const remainder = value.slice(1);
5711
- const home = process.env.HOME || process.env.USERPROFILE || homedir();
5746
+ /**
5747
+ * Expand home directory references in a path string.
5748
+ * Handles both {userHomeDir} placeholder and ~ prefix.
5749
+ */
5750
+ function expandUserHomeDir(path$1) {
5751
+ const home = process.env.HOME || homedir();
5752
+ if (path$1.includes(USER_HOME_DIR_PLACEHOLDER)) return path$1.replace(USER_HOME_DIR_PLACEHOLDER, home);
5753
+ if (path$1.startsWith("~")) {
5754
+ const remainder = path$1.slice(1);
5712
5755
  if (!remainder) return home;
5713
5756
  if (remainder.startsWith("/") || remainder.startsWith("\\")) return `${home}${remainder}`;
5714
5757
  return `${home}/${remainder}`;
5715
5758
  }
5716
- return value;
5759
+ return path$1;
5717
5760
  }
5718
5761
 
5719
5762
  //#endregion
@@ -5882,6 +5925,20 @@ async function directoryExists(path$1) {
5882
5925
  }
5883
5926
  }
5884
5927
 
5928
+ //#endregion
5929
+ //#region src/lib/zod-validator.ts
5930
+ /**
5931
+ * Creates a validator function from a Zod schema.
5932
+ * Returns error message if invalid, undefined if valid.
5933
+ */
5934
+ function check(schema) {
5935
+ return (value) => {
5936
+ const result = schema.safeParse(value);
5937
+ if (!result.success) return result.error.issues[0]?.message;
5938
+ return;
5939
+ };
5940
+ }
5941
+
5885
5942
  //#endregion
5886
5943
  //#region src/lib/rule-utils.ts
5887
5944
  const SKILL_FILENAME = "SKILL.md";
@@ -6005,7 +6062,7 @@ async function loadConfig(inputPath, overrides) {
6005
6062
  platforms
6006
6063
  };
6007
6064
  const configDir = dirname(configPath);
6008
- const platformNames = platforms.map((p$2) => p$2.platform).join(", ");
6065
+ const platformNames = platforms.map((plat) => plat.platform).join(", ");
6009
6066
  log.debug(`Loaded config: ${config$1.name}, platforms: ${platformNames}`);
6010
6067
  return {
6011
6068
  configPath,
@@ -6191,18 +6248,270 @@ async function readFirstMatch(dir, filenames) {
6191
6248
  }
6192
6249
  return;
6193
6250
  }
6251
+ /**
6252
+ * Detect if directory contains SKILL.md and extract frontmatter defaults.
6253
+ */
6254
+ async function detectSkillDirectory$1(directory) {
6255
+ const skillPath = join(directory, SKILL_FILENAME);
6256
+ if (!await fileExists(skillPath)) return;
6257
+ const content = await readFile(skillPath, "utf8");
6258
+ const frontmatter = parseSkillFrontmatter(content);
6259
+ return {
6260
+ name: frontmatter.name,
6261
+ license: frontmatter.license
6262
+ };
6263
+ }
6264
+ function parseTags(input) {
6265
+ if (typeof input !== "string") return [];
6266
+ if (input.trim().length === 0) return [];
6267
+ return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
6268
+ }
6269
+ function checkTags(value) {
6270
+ const tags = parseTags(value);
6271
+ const result = tagsSchema.safeParse(tags);
6272
+ if (!result.success) return result.error.issues[0]?.message;
6273
+ }
6274
+ /**
6275
+ * Collect rule inputs via interactive prompts or defaults.
6276
+ *
6277
+ * Handles:
6278
+ * - Skill detection with SKILL.md frontmatter
6279
+ * - Platform multiselect with keyboard hints
6280
+ * - Per-platform path prompting (for non-skill multi-platform)
6281
+ * - Name, title, description, tags, license prompts
6282
+ */
6283
+ async function collectRuleInputs(options) {
6284
+ const { directory, defaults = {}, nonInteractive = false } = options;
6285
+ const skillInfo = await detectSkillDirectory$1(directory);
6286
+ let isSkill = false;
6287
+ if (skillInfo && !nonInteractive) {
6288
+ const confirm = await p$2.confirm({
6289
+ message: `Detected SKILL.md${skillInfo.name ? ` (${skillInfo.name})` : ""}. Initialize as skill?`,
6290
+ initialValue: true
6291
+ });
6292
+ if (p$2.isCancel(confirm)) throw new Error("Cancelled");
6293
+ isSkill = confirm;
6294
+ } else if (skillInfo && nonInteractive) isSkill = true;
6295
+ const defaultName = isSkill && skillInfo?.name ? normalizeName(skillInfo.name) : defaults.name ?? "my-rule";
6296
+ const defaultLicense = isSkill && skillInfo?.license ? skillInfo.license : defaults.license ?? "MIT";
6297
+ const validatedPlatforms = [];
6298
+ if (defaults.platforms) for (const platform of defaults.platforms) {
6299
+ if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}"`);
6300
+ validatedPlatforms.push(platform);
6301
+ }
6302
+ const selectedPlatforms = validatedPlatforms.length > 0 ? validatedPlatforms : await (async () => {
6303
+ if (nonInteractive) throw new Error("Missing --platform in non-interactive mode");
6304
+ const platformChoices = await p$2.multiselect({
6305
+ message: `Platforms ${ui.dim("(space to toggle, 'a' to select all)")}`,
6306
+ options: PLATFORM_IDS.map((id) => ({
6307
+ value: id,
6308
+ label: id
6309
+ })),
6310
+ required: true
6311
+ });
6312
+ if (p$2.isCancel(platformChoices)) throw new Error("Cancelled");
6313
+ return platformChoices;
6314
+ })();
6315
+ const platformPaths = {};
6316
+ if (selectedPlatforms.length > 1 && !isSkill && !nonInteractive) {
6317
+ const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6318
+ const value = defaults.platformPaths?.[platform];
6319
+ return typeof value === "string" && value.trim().length > 0;
6320
+ });
6321
+ if (hasCompletePathMapping && defaults.platformPaths) for (const platform of selectedPlatforms) {
6322
+ const pathVal = defaults.platformPaths[platform]?.trim();
6323
+ if (pathVal && pathVal !== ".") platformPaths[platform] = pathVal;
6324
+ }
6325
+ else for (const platform of selectedPlatforms) {
6326
+ const mappedPath = defaults.platformPaths?.[platform]?.trim();
6327
+ const suggestedPath = mappedPath ?? (await directoryExists(join(directory, platform)) ? platform : ".");
6328
+ const input = await p$2.text({
6329
+ message: `Folder for ${platform} files ('.' = same folder as agentrules.json)`,
6330
+ placeholder: suggestedPath,
6331
+ defaultValue: suggestedPath
6332
+ });
6333
+ if (p$2.isCancel(input)) throw new Error("Cancelled");
6334
+ const trimmed = input.trim();
6335
+ const resolvedPath = trimmed.length > 0 ? trimmed : suggestedPath;
6336
+ if (resolvedPath !== ".") platformPaths[platform] = resolvedPath;
6337
+ }
6338
+ } else if (defaults.platformPaths) for (const platform of selectedPlatforms) {
6339
+ const pathVal = defaults.platformPaths[platform]?.trim();
6340
+ if (pathVal && pathVal !== ".") platformPaths[platform] = pathVal;
6341
+ }
6342
+ if (nonInteractive) {
6343
+ const name = normalizeName(defaults.name ?? defaultName);
6344
+ const nameCheck = nameSchema.safeParse(name);
6345
+ if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6346
+ return {
6347
+ platforms: selectedPlatforms,
6348
+ platformPaths,
6349
+ name,
6350
+ title: defaults.title ?? toTitleCase(name),
6351
+ description: defaults.description ?? "",
6352
+ tags: defaults.tags ?? [],
6353
+ license: defaultLicense,
6354
+ isSkill,
6355
+ ruleType: isSkill ? "skill" : defaults.ruleType
6356
+ };
6357
+ }
6358
+ const result = await p$2.group({
6359
+ name: () => {
6360
+ const normalizedDefault = normalizeName(defaultName);
6361
+ return p$2.text({
6362
+ message: "Rule name",
6363
+ placeholder: normalizedDefault,
6364
+ defaultValue: normalizedDefault,
6365
+ validate: (value) => {
6366
+ if (!value || value.trim() === "") return;
6367
+ return check(nameSchema)(value);
6368
+ }
6369
+ });
6370
+ },
6371
+ title: ({ results }) => {
6372
+ const defaultTitle = defaults.title ?? toTitleCase(results.name ?? defaultName);
6373
+ return p$2.text({
6374
+ message: "Title",
6375
+ defaultValue: defaultTitle,
6376
+ placeholder: defaultTitle
6377
+ });
6378
+ },
6379
+ description: () => p$2.text({
6380
+ message: "Description",
6381
+ placeholder: "Describe what this rule does...",
6382
+ defaultValue: defaults.description,
6383
+ validate: check(descriptionSchema)
6384
+ }),
6385
+ tags: () => p$2.text({
6386
+ message: "Tags (comma-separated, optional)",
6387
+ placeholder: "e.g., typescript, testing, react",
6388
+ validate: checkTags
6389
+ }),
6390
+ license: async () => {
6391
+ const choice = await p$2.select({
6392
+ message: "License",
6393
+ options: [...COMMON_LICENSES.map((id) => ({
6394
+ value: id,
6395
+ label: id
6396
+ })), {
6397
+ value: "__other__",
6398
+ label: "Other (enter SPDX identifier)"
6399
+ }],
6400
+ initialValue: defaultLicense
6401
+ });
6402
+ if (p$2.isCancel(choice)) throw new Error("Cancelled");
6403
+ if (choice === "__other__") {
6404
+ const custom = await p$2.text({
6405
+ message: "License (SPDX identifier)",
6406
+ placeholder: "e.g., MPL-2.0, AGPL-3.0-only",
6407
+ validate: check(licenseSchema)
6408
+ });
6409
+ if (p$2.isCancel(custom)) throw new Error("Cancelled");
6410
+ return custom;
6411
+ }
6412
+ return choice;
6413
+ }
6414
+ }, { onCancel: () => {
6415
+ throw new Error("Cancelled");
6416
+ } });
6417
+ const tags = parseTags(result.tags);
6418
+ return {
6419
+ platforms: selectedPlatforms,
6420
+ platformPaths,
6421
+ name: result.name,
6422
+ title: result.title.trim() || toTitleCase(result.name),
6423
+ description: result.description ?? "",
6424
+ tags,
6425
+ license: result.license,
6426
+ isSkill,
6427
+ ruleType: isSkill ? "skill" : defaults.ruleType
6428
+ };
6429
+ }
6194
6430
 
6195
6431
  //#endregion
6196
- //#region src/lib/zod-validator.ts
6432
+ //#region src/commands/rule/init.ts
6433
+ /** Default rule name when none specified */
6434
+ const DEFAULT_RULE_NAME = "my-rule";
6197
6435
  /**
6198
- * Creates a validator function from a Zod schema.
6199
- * Returns error message if invalid, undefined if valid.
6436
+ * Detect if directory contains SKILL.md and extract frontmatter defaults.
6200
6437
  */
6201
- function check(schema) {
6202
- return (value) => {
6203
- const result = schema.safeParse(value);
6204
- if (!result.success) return result.error.issues[0]?.message;
6205
- return;
6438
+ async function detectSkillDirectory(directory) {
6439
+ const skillPath = join(directory, SKILL_FILENAME);
6440
+ if (!await fileExists(skillPath)) return;
6441
+ const content = await readFile(skillPath, "utf8");
6442
+ const frontmatter = parseSkillFrontmatter(content);
6443
+ return {
6444
+ name: frontmatter.name,
6445
+ license: frontmatter.license
6446
+ };
6447
+ }
6448
+ /**
6449
+ * Initialize a rule in a directory (rule root).
6450
+ *
6451
+ * Structure:
6452
+ * - ruleDir/agentrules.json - rule config
6453
+ * - ruleDir/* - rule files (collected by default)
6454
+ * - ruleDir/README.md, ruleDir/LICENSE.md, ruleDir/INSTALL.txt - optional metadata (not bundled)
6455
+ */
6456
+ async function initRule(options) {
6457
+ const ruleDir = options.directory ?? process.cwd();
6458
+ log.debug(`Initializing rule in: ${ruleDir}`);
6459
+ const skillInfo = await detectSkillDirectory(ruleDir);
6460
+ const isSkillDirectory = skillInfo !== void 0;
6461
+ const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6462
+ const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6463
+ if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6464
+ const platforms = platformInputs.map(normalizePlatformEntryInput);
6465
+ const name = normalizeName(options.name ?? skillInfo?.name ?? DEFAULT_RULE_NAME);
6466
+ const title = options.title ?? toTitleCase(name);
6467
+ const description = options.description ?? "";
6468
+ const license = options.license ?? skillInfo?.license ?? "MIT";
6469
+ const ruleType = isSkillDirectory ? "skill" : options.type;
6470
+ const platformLabels = platforms.map((p$3) => typeof p$3 === "string" ? p$3 : p$3.platform).join(", ");
6471
+ log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6472
+ const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
6473
+ if (!options.force && await fileExists(configPath)) throw new Error(`${RULE_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
6474
+ const config$1 = {
6475
+ $schema: RULE_SCHEMA_URL,
6476
+ name,
6477
+ ...ruleType && { type: ruleType },
6478
+ title,
6479
+ version: 1,
6480
+ description,
6481
+ tags: options.tags ?? [],
6482
+ license,
6483
+ platforms
6484
+ };
6485
+ let createdDir;
6486
+ if (await directoryExists(ruleDir)) log.debug(`Directory exists: ${ruleDir}`);
6487
+ else {
6488
+ await mkdir(ruleDir, { recursive: true });
6489
+ createdDir = ruleDir;
6490
+ log.debug(`Created directory: ${ruleDir}`);
6491
+ }
6492
+ const content = `${JSON.stringify(config$1, null, 2)}\n`;
6493
+ await writeFile(configPath, content, "utf8");
6494
+ log.debug(`Wrote config file: ${configPath}`);
6495
+ log.debug("Rule initialization complete.");
6496
+ return {
6497
+ configPath,
6498
+ rule: config$1,
6499
+ createdDir
6500
+ };
6501
+ }
6502
+ function normalizePlatform(input) {
6503
+ const normalized = input.toLowerCase();
6504
+ if (!isSupportedPlatform(normalized)) throw new Error(`Unknown platform "${input}". Supported: ${PLATFORM_IDS.join(", ")}`);
6505
+ return normalized;
6506
+ }
6507
+ function normalizePlatformEntryInput(input) {
6508
+ if (typeof input === "string") return normalizePlatform(input);
6509
+ const platform = normalizePlatform(input.platform);
6510
+ const path$1 = typeof input.path === "string" ? input.path.trim() : "";
6511
+ if (path$1.length === 0 || path$1 === ".") return platform;
6512
+ return {
6513
+ platform,
6514
+ path: path$1
6206
6515
  };
6207
6516
  }
6208
6517
 
@@ -6210,14 +6519,46 @@ function check(schema) {
6210
6519
  //#region src/commands/publish.ts
6211
6520
  /** Maximum size per variant/platform bundle in bytes (1MB) */
6212
6521
  const MAX_VARIANT_SIZE_BYTES = 1 * 1024 * 1024;
6213
- /** Schema for parsing comma-separated tags input */
6214
- const tagsInputSchema = string().transform((input) => input.split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0)).pipe(tagsSchema);
6215
6522
  function formatBytes(bytes) {
6216
6523
  if (bytes < 1024) return `${bytes} B`;
6217
6524
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6218
6525
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6219
6526
  }
6220
6527
  /**
6528
+ * Prompts to create agentrules.json after quick publish (or dry run).
6529
+ * With --yes, creates without prompting.
6530
+ */
6531
+ async function promptToCreateConfig(quickPublish, yes) {
6532
+ if (!quickPublish) return false;
6533
+ let createConfig = yes ?? false;
6534
+ if (!yes && process.stdin.isTTY) {
6535
+ log.print("");
6536
+ const answer = await p$1.confirm({
6537
+ message: "Create agentrules.json for future publishes?",
6538
+ initialValue: true
6539
+ });
6540
+ createConfig = !p$1.isCancel(answer) && answer;
6541
+ }
6542
+ if (createConfig) {
6543
+ const sourceDir = quickPublish.source.type === "directory" ? quickPublish.source.path : void 0;
6544
+ const configDir = sourceDir ?? (quickPublish.source.path.replace(/[/\\][^/\\]+$/, "") || ".");
6545
+ await initRule({
6546
+ directory: configDir,
6547
+ name: quickPublish.name,
6548
+ title: quickPublish.title,
6549
+ description: quickPublish.description || void 0,
6550
+ platforms: quickPublish.platforms,
6551
+ type: quickPublish.ruleType,
6552
+ tags: quickPublish.tags,
6553
+ license: quickPublish.license,
6554
+ force: false
6555
+ });
6556
+ log.success(`Created ${ui.path(join(configDir, "agentrules.json"))}`);
6557
+ return true;
6558
+ }
6559
+ return false;
6560
+ }
6561
+ /**
6221
6562
  * Publishes a rule to the registry.
6222
6563
  *
6223
6564
  * Supports:
@@ -6287,7 +6628,9 @@ async function publish(options = {}) {
6287
6628
  dryRun,
6288
6629
  version: version$2,
6289
6630
  spinner: fileSpinner,
6290
- ctx
6631
+ ctx,
6632
+ quickPublish: resolved,
6633
+ yes
6291
6634
  });
6292
6635
  }
6293
6636
  if (quickDir) {
@@ -6303,8 +6646,7 @@ async function publish(options = {}) {
6303
6646
  license
6304
6647
  }, {
6305
6648
  type: "directory",
6306
- path: quickDir.path,
6307
- entryFile: quickDir.entryFile
6649
+ path: quickDir
6308
6650
  }, {
6309
6651
  dryRun,
6310
6652
  yes
@@ -6338,7 +6680,9 @@ async function publish(options = {}) {
6338
6680
  dryRun,
6339
6681
  version: version$2,
6340
6682
  spinner: dirSpinner,
6341
- ctx
6683
+ ctx,
6684
+ quickPublish: resolved,
6685
+ yes
6342
6686
  });
6343
6687
  }
6344
6688
  const spinner$1 = await log.spinner("Validating rule...");
@@ -6435,14 +6779,7 @@ async function getQuickPublishDirectory(inputPath) {
6435
6779
  if (!pathStat?.isDirectory()) return;
6436
6780
  const configStat = await stat(`${inputPath}/${RULE_CONFIG_FILENAME}`).catch(() => null);
6437
6781
  if (configStat?.isFile()) return;
6438
- const skillPath = `${inputPath}/${SKILL_FILENAME}`;
6439
- const skillStat = await stat(skillPath).catch(() => null);
6440
- if (skillStat?.isFile()) return {
6441
- path: inputPath,
6442
- type: "skill",
6443
- entryFile: skillPath
6444
- };
6445
- return;
6782
+ return inputPath;
6446
6783
  }
6447
6784
  function normalizePathForInference(value) {
6448
6785
  return value.replace(/\\/g, "/");
@@ -6469,17 +6806,6 @@ function inferFileDefaults(filePath) {
6469
6806
  }
6470
6807
  return result;
6471
6808
  }
6472
- async function inferDirectoryDefaults(_dirPath, entryFile, dirType) {
6473
- const result = {};
6474
- if (dirType === "skill") {
6475
- result.ruleType = "skill";
6476
- const content = await readFile(entryFile, "utf8");
6477
- const frontmatter = parseSkillFrontmatter(content);
6478
- if (frontmatter.name) result.name = normalizeName(frontmatter.name);
6479
- if (frontmatter.license) result.license = frontmatter.license;
6480
- }
6481
- return result;
6482
- }
6483
6809
  function buildConfigPublishOverrides(options) {
6484
6810
  const overrides = {};
6485
6811
  if (options.name !== void 0) overrides.name = options.name;
@@ -6492,37 +6818,23 @@ function buildConfigPublishOverrides(options) {
6492
6818
  return Object.keys(overrides).length > 0 ? overrides : void 0;
6493
6819
  }
6494
6820
  async function resolveQuickPublishInputs(options, source, ctx) {
6495
- const inferred = source.type === "file" ? inferFileDefaults(source.path) : await inferDirectoryDefaults(source.path, source.entryFile, "skill");
6496
6821
  const isInteractive = !ctx.yes && process.stdin.isTTY;
6497
6822
  const isDirectory = source.type === "directory";
6498
- const hasRequiredArgs = isDirectory ? options.platform !== void 0 && (options.name !== void 0 || inferred.name !== void 0) : options.name !== void 0 && options.platform !== void 0 && options.type !== void 0;
6499
- if (!(isInteractive || hasRequiredArgs)) {
6500
- if (isDirectory) throw new Error("Publishing a directory in non-interactive mode requires --platform (and --name if not in frontmatter).");
6501
- throw new Error("Publishing a single file in non-interactive mode requires --name, --platform, and --type.");
6502
- }
6503
- const selectedPlatforms = options.platform ? parsePlatformSelection(options.platform) : void 0;
6504
- if (selectedPlatforms && selectedPlatforms.length > 1) throw new Error("Quick publish requires exactly one --platform value.");
6505
- let selectedPlatform = selectedPlatforms?.[0] ? normalizePlatformInput(selectedPlatforms[0]) : inferred.platform;
6506
- if (!selectedPlatform) {
6507
- if (!isInteractive) throw new Error("Missing --platform");
6508
- const selection = await p$1.select({
6509
- message: "Platform",
6510
- options: PLATFORM_IDS.map((id) => ({
6511
- value: id,
6512
- label: id
6513
- }))
6514
- });
6515
- if (p$1.isCancel(selection)) throw new Error("Cancelled");
6516
- selectedPlatform = selection;
6517
- }
6518
- let selectedType = options.type ?? inferred.ruleType;
6519
- if (!selectedType) {
6520
- if (!isInteractive) throw new Error("Missing --type");
6521
- const candidates = getValidTypes(selectedPlatform).filter((t) => supportsInstallPath({
6522
- platform: selectedPlatform,
6823
+ const isFile = source.type === "file";
6824
+ const fileInferred = isFile ? inferFileDefaults(source.path) : {};
6825
+ const parsedPlatforms = options.platform ? parsePlatformSelection(options.platform).map(normalizePlatformInput) : void 0;
6826
+ let selectedType = options.type ?? fileInferred.ruleType;
6827
+ const platformsForTypeCheck = parsedPlatforms && parsedPlatforms.length > 0 ? parsedPlatforms : fileInferred.platform ? [fileInferred.platform] : [];
6828
+ if (isFile && !selectedType) {
6829
+ if (!isInteractive) throw new Error("Publishing a single file in non-interactive mode requires --name, --platform, and --type.");
6830
+ if (platformsForTypeCheck.length === 0) throw new Error("Missing --platform");
6831
+ const candidateSets = platformsForTypeCheck.map((plat) => getValidTypes(plat).filter((t) => supportsInstallPath({
6832
+ platform: plat,
6523
6833
  type: t,
6524
6834
  scope: "project"
6525
- }));
6835
+ })));
6836
+ const candidates = candidateSets.length === 1 ? candidateSets[0] : candidateSets.reduce((acc, set) => acc.filter((t) => set.includes(t)));
6837
+ if (candidates.length === 0) throw new Error(`No common type supports all selected platforms: ${platformsForTypeCheck.join(", ")}`);
6526
6838
  const selection = await p$1.select({
6527
6839
  message: "Type",
6528
6840
  options: candidates.map((t) => ({
@@ -6533,64 +6845,48 @@ async function resolveQuickPublishInputs(options, source, ctx) {
6533
6845
  if (p$1.isCancel(selection)) throw new Error("Cancelled");
6534
6846
  selectedType = selection;
6535
6847
  }
6536
- const platform = selectedPlatform;
6537
- const ruleType = selectedType;
6538
- const nameValue = options.name ?? inferred.name ?? await (async () => {
6539
- if (!isInteractive) throw new Error("Missing --name");
6540
- const input = await p$1.text({
6541
- message: "Rule name",
6542
- placeholder: "my-rule",
6543
- validate: check(nameSchema)
6544
- });
6545
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6546
- return input;
6547
- })();
6548
- const normalizedName = normalizeName(nameValue);
6549
- const nameCheck = nameSchema.safeParse(normalizedName);
6550
- if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6551
- const defaultTitle = toTitleCase(normalizedName);
6552
- const finalTitle = options.title ?? await (async () => {
6553
- if (!isInteractive) return defaultTitle;
6554
- const input = await p$1.text({
6555
- message: "Title",
6556
- defaultValue: defaultTitle,
6557
- placeholder: defaultTitle
6558
- });
6559
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6560
- return input;
6561
- })();
6562
- const finalDescription = options.description ?? await (async () => {
6563
- if (!isInteractive) return "";
6564
- const input = await p$1.text({
6565
- message: "Description (optional)",
6566
- placeholder: "Describe what this rule does...",
6567
- validate: check(descriptionSchema)
6568
- });
6569
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6570
- return input;
6571
- })();
6572
- const finalTags = await (async () => {
6573
- if (options.tags) return tagsSchema.parse(options.tags);
6574
- if (!isInteractive) return [];
6575
- const input = await p$1.text({
6576
- message: "Tags (optional)",
6577
- placeholder: "comma-separated, e.g. typescript, react",
6578
- validate: check(tagsInputSchema)
6579
- });
6580
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6581
- return tagsInputSchema.parse(input);
6582
- })();
6583
- const finalLicense = options.license ?? inferred.license ?? "MIT";
6848
+ if (options.type && platformsForTypeCheck.length > 0) {
6849
+ const ruleType = selectedType;
6850
+ for (const platform of platformsForTypeCheck) if (!supportsInstallPath({
6851
+ platform,
6852
+ type: ruleType,
6853
+ scope: "project"
6854
+ })) throw new Error(`Type "${ruleType}" is not supported for project installs on platform "${platform}".`);
6855
+ }
6856
+ const collected = await collectRuleInputs({
6857
+ directory: isDirectory ? source.path : dirname(source.path),
6858
+ defaults: {
6859
+ name: options.name ?? fileInferred.name,
6860
+ title: options.title,
6861
+ description: options.description,
6862
+ platforms: parsedPlatforms,
6863
+ license: options.license ?? fileInferred.license,
6864
+ tags: options.tags,
6865
+ ruleType: selectedType
6866
+ },
6867
+ nonInteractive: !isInteractive,
6868
+ detectType: isDirectory
6869
+ });
6870
+ const finalRuleType = isFile ? selectedType : collected.ruleType ?? (collected.isSkill ? "skill" : "instruction");
6871
+ for (const platform of collected.platforms) if (!supportsInstallPath({
6872
+ platform,
6873
+ type: finalRuleType,
6874
+ scope: "project"
6875
+ })) throw new Error(`Type "${finalRuleType}" is not supported for project installs on platform "${platform}".`);
6584
6876
  if (isInteractive && !ctx.dryRun) {
6585
6877
  log.print("");
6586
- log.print(ui.header("Quick publish"));
6587
- log.print(ui.keyValue(isDirectory ? "Directory" : "File", ui.path(source.path)));
6588
- log.print(ui.keyValue("Name", ui.code(normalizedName)));
6589
- log.print(ui.keyValue("Title", finalTitle));
6590
- log.print(ui.keyValue("Description", finalDescription));
6591
- log.print(ui.keyValue("Platform", platform));
6592
- log.print(ui.keyValue("Type", ruleType));
6593
- if (finalTags.length > 0) log.print(ui.keyValue("Tags", finalTags.join(", ")));
6878
+ log.print(ui.rulePreview({
6879
+ header: "Quick publish",
6880
+ path: source.path,
6881
+ pathLabel: isDirectory ? "Directory" : "File",
6882
+ name: collected.name,
6883
+ title: collected.title,
6884
+ description: collected.description,
6885
+ platforms: collected.platforms,
6886
+ type: finalRuleType,
6887
+ tags: collected.tags,
6888
+ showHints: true
6889
+ }));
6594
6890
  log.print("");
6595
6891
  const confirm = await p$1.confirm({
6596
6892
  message: isDirectory ? "Publish this directory?" : "Publish this file?",
@@ -6600,16 +6896,24 @@ async function resolveQuickPublishInputs(options, source, ctx) {
6600
6896
  }
6601
6897
  return {
6602
6898
  source,
6603
- name: normalizedName,
6604
- platform,
6605
- ruleType,
6606
- title: finalTitle,
6607
- description: finalDescription,
6608
- tags: finalTags,
6609
- license: finalLicense
6899
+ name: collected.name,
6900
+ platforms: collected.platforms,
6901
+ platformPaths: isFile ? {} : collected.platformPaths,
6902
+ ruleType: finalRuleType,
6903
+ title: collected.title,
6904
+ description: collected.description,
6905
+ tags: collected.tags,
6906
+ license: collected.license
6610
6907
  };
6611
6908
  }
6612
6909
  async function buildRuleInputFromQuickPublish(inputs) {
6910
+ const platformEntries = inputs.platforms.map((platform) => {
6911
+ const path$1 = inputs.platformPaths[platform];
6912
+ return path$1 ? {
6913
+ platform,
6914
+ path: path$1
6915
+ } : { platform };
6916
+ });
6613
6917
  const config$1 = {
6614
6918
  $schema: RULE_SCHEMA_URL,
6615
6919
  name: inputs.name,
@@ -6618,34 +6922,38 @@ async function buildRuleInputFromQuickPublish(inputs) {
6618
6922
  description: inputs.description,
6619
6923
  license: inputs.license,
6620
6924
  tags: inputs.tags,
6621
- platforms: [{ platform: inputs.platform }]
6925
+ platforms: platformEntries
6622
6926
  };
6623
6927
  if (inputs.source.type === "file") {
6624
6928
  const content = await readFile(inputs.source.path);
6625
- const bundlePath = getInstallPath({
6626
- platform: inputs.platform,
6627
- type: inputs.ruleType,
6628
- name: inputs.name,
6629
- scope: "project"
6630
- });
6631
- if (!bundlePath) throw new Error(`Type "${inputs.ruleType}" is not supported for project installs on platform "${inputs.platform}".`);
6632
- return {
6633
- name: inputs.name,
6634
- config: config$1,
6635
- platformFiles: [{
6636
- platform: inputs.platform,
6929
+ const platformFiles$1 = [];
6930
+ for (const platform of inputs.platforms) {
6931
+ const bundlePath = getInstallPath({
6932
+ platform,
6933
+ type: inputs.ruleType,
6934
+ name: inputs.name,
6935
+ scope: "project"
6936
+ });
6937
+ if (!bundlePath) throw new Error(`Type "${inputs.ruleType}" is not supported for project installs on platform "${platform}".`);
6938
+ platformFiles$1.push({
6939
+ platform,
6637
6940
  files: [{
6638
6941
  path: bundlePath,
6639
6942
  content
6640
6943
  }]
6641
- }]
6944
+ });
6945
+ }
6946
+ return {
6947
+ name: inputs.name,
6948
+ config: config$1,
6949
+ platformFiles: platformFiles$1
6642
6950
  };
6643
6951
  }
6644
6952
  const loadedConfig = {
6645
6953
  configPath: `${inputs.source.path}/agentrules.json`,
6646
6954
  config: {
6647
6955
  ...config$1,
6648
- platforms: [{ platform: inputs.platform }]
6956
+ platforms: platformEntries
6649
6957
  },
6650
6958
  configDir: inputs.source.path
6651
6959
  };
@@ -6657,7 +6965,7 @@ async function buildRuleInputFromQuickPublish(inputs) {
6657
6965
  };
6658
6966
  }
6659
6967
  async function finalizePublish(options) {
6660
- const { publishInput, dryRun, version: version$2, spinner: spinner$1, ctx } = options;
6968
+ const { publishInput, dryRun, version: version$2, spinner: spinner$1, ctx, quickPublish, yes } = options;
6661
6969
  const totalFileCount = publishInput.variants.reduce((sum, v) => sum + v.files.length, 0);
6662
6970
  const platformList = publishInput.variants.map((v) => v.platform).join(", ");
6663
6971
  let totalSize = 0;
@@ -6696,6 +7004,7 @@ async function finalizePublish(options) {
6696
7004
  log.print("");
6697
7005
  }
6698
7006
  log.print(ui.hint("Run without --dry-run to publish."));
7007
+ await promptToCreateConfig(quickPublish, yes);
6699
7008
  return {
6700
7009
  success: true,
6701
7010
  preview: {
@@ -6743,6 +7052,7 @@ async function finalizePublish(options) {
6743
7052
  }
6744
7053
  log.info("");
6745
7054
  log.info(ui.keyValue("Now live at", ui.link(data.url)));
7055
+ await promptToCreateConfig(quickPublish, yes);
6746
7056
  return {
6747
7057
  success: true,
6748
7058
  rule: {
@@ -6833,196 +7143,15 @@ async function discoverRuleDirs(inputDir) {
6833
7143
  return ruleDirs.sort();
6834
7144
  }
6835
7145
 
6836
- //#endregion
6837
- //#region src/commands/rule/init.ts
6838
- /** Default rule name when none specified */
6839
- const DEFAULT_RULE_NAME$1 = "my-rule";
6840
- /**
6841
- * Detect if directory contains SKILL.md and extract frontmatter defaults.
6842
- */
6843
- async function detectSkillDirectory(directory) {
6844
- const skillPath = join(directory, SKILL_FILENAME);
6845
- if (!await fileExists(skillPath)) return;
6846
- const content = await readFile(skillPath, "utf8");
6847
- const frontmatter = parseSkillFrontmatter(content);
6848
- return {
6849
- name: frontmatter.name,
6850
- license: frontmatter.license
6851
- };
6852
- }
6853
- /**
6854
- * Initialize a rule in a directory (rule root).
6855
- *
6856
- * Structure:
6857
- * - ruleDir/agentrules.json - rule config
6858
- * - ruleDir/* - rule files (collected by default)
6859
- * - ruleDir/README.md, ruleDir/LICENSE.md, ruleDir/INSTALL.txt - optional metadata (not bundled)
6860
- */
6861
- async function initRule(options) {
6862
- const ruleDir = options.directory ?? process.cwd();
6863
- log.debug(`Initializing rule in: ${ruleDir}`);
6864
- const skillInfo = await detectSkillDirectory(ruleDir);
6865
- const isSkillDirectory = skillInfo !== void 0;
6866
- const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6867
- const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6868
- if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6869
- const platforms = platformInputs.map(normalizePlatformEntryInput);
6870
- const name = normalizeName(options.name ?? skillInfo?.name ?? DEFAULT_RULE_NAME$1);
6871
- const title = options.title ?? toTitleCase(name);
6872
- const description = options.description ?? "";
6873
- const license = options.license ?? skillInfo?.license ?? "MIT";
6874
- const ruleType = isSkillDirectory ? "skill" : options.type;
6875
- const platformLabels = platforms.map((p$2) => typeof p$2 === "string" ? p$2 : p$2.platform).join(", ");
6876
- log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6877
- const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
6878
- if (!options.force && await fileExists(configPath)) throw new Error(`${RULE_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
6879
- const config$1 = {
6880
- $schema: RULE_SCHEMA_URL,
6881
- name,
6882
- ...ruleType && { type: ruleType },
6883
- title,
6884
- version: 1,
6885
- description,
6886
- tags: options.tags ?? [],
6887
- license,
6888
- platforms
6889
- };
6890
- let createdDir;
6891
- if (await directoryExists(ruleDir)) log.debug(`Directory exists: ${ruleDir}`);
6892
- else {
6893
- await mkdir(ruleDir, { recursive: true });
6894
- createdDir = ruleDir;
6895
- log.debug(`Created directory: ${ruleDir}`);
6896
- }
6897
- const content = `${JSON.stringify(config$1, null, 2)}\n`;
6898
- await writeFile(configPath, content, "utf8");
6899
- log.debug(`Wrote config file: ${configPath}`);
6900
- log.debug("Rule initialization complete.");
6901
- return {
6902
- configPath,
6903
- rule: config$1,
6904
- createdDir
6905
- };
6906
- }
6907
- function normalizePlatform(input) {
6908
- const normalized = input.toLowerCase();
6909
- if (!isSupportedPlatform(normalized)) throw new Error(`Unknown platform "${input}". Supported: ${PLATFORM_IDS.join(", ")}`);
6910
- return normalized;
6911
- }
6912
- function normalizePlatformEntryInput(input) {
6913
- if (typeof input === "string") return normalizePlatform(input);
6914
- const platform = normalizePlatform(input.platform);
6915
- const path$1 = typeof input.path === "string" ? input.path.trim() : "";
6916
- if (path$1.length === 0 || path$1 === ".") return platform;
6917
- return {
6918
- platform,
6919
- path: path$1
6920
- };
6921
- }
6922
-
6923
7146
  //#endregion
6924
7147
  //#region src/commands/rule/init-interactive.ts
6925
- const DEFAULT_RULE_NAME = "my-rule";
6926
- /**
6927
- * Parse comma-separated tags string into array.
6928
- */
6929
- function parseTags(input) {
6930
- if (typeof input !== "string") return [];
6931
- if (input.trim().length === 0) return [];
6932
- return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
6933
- }
6934
- /**
6935
- * Validator for comma-separated tags input.
6936
- */
6937
- function checkTags(value) {
6938
- const tags = parseTags(value);
6939
- const result = tagsSchema.safeParse(tags);
6940
- if (!result.success) return result.error.issues[0]?.message;
6941
- }
6942
7148
  /**
6943
7149
  * Run interactive init flow with clack prompts.
6944
7150
  */
6945
7151
  async function initInteractive(options) {
6946
- const { directory, name: nameOption, title: titleOption, description: descriptionOption, platforms: platformsOption, platformPaths, license: licenseOption } = options;
7152
+ const { directory, platformPaths } = options;
6947
7153
  let { force } = options;
6948
7154
  p.intro("Create a new rule");
6949
- const skillInfo = await detectSkillDirectory(directory);
6950
- let useSkillDefaults = false;
6951
- if (skillInfo) {
6952
- const confirm = await p.confirm({
6953
- message: `Detected SKILL.md${skillInfo.name ? ` (${skillInfo.name})` : ""}. Initialize as skill?`,
6954
- initialValue: true
6955
- });
6956
- if (p.isCancel(confirm)) {
6957
- p.cancel("Cancelled");
6958
- process.exit(0);
6959
- }
6960
- useSkillDefaults = confirm;
6961
- }
6962
- const defaultName = useSkillDefaults && skillInfo?.name ? skillInfo.name : nameOption ?? DEFAULT_RULE_NAME;
6963
- const defaultLicense = useSkillDefaults && skillInfo?.license ? skillInfo.license : licenseOption ?? "MIT";
6964
- const validatedPlatforms = [];
6965
- if (platformsOption) for (const platform of platformsOption) {
6966
- if (!isSupportedPlatform(platform)) {
6967
- p.cancel(`Unknown platform "${platform}"`);
6968
- process.exit(1);
6969
- }
6970
- validatedPlatforms.push(platform);
6971
- }
6972
- const selectedPlatforms = validatedPlatforms.length > 0 ? validatedPlatforms : await (async () => {
6973
- const platformChoices = await p.multiselect({
6974
- message: "Platforms (select one or more)",
6975
- options: PLATFORM_IDS.map((id) => ({
6976
- value: id,
6977
- label: id
6978
- })),
6979
- required: true
6980
- });
6981
- if (p.isCancel(platformChoices)) {
6982
- p.cancel("Cancelled");
6983
- process.exit(0);
6984
- }
6985
- return platformChoices;
6986
- })();
6987
- const platformEntries = await (async () => {
6988
- if (selectedPlatforms.length === 0) return [];
6989
- if (useSkillDefaults) return selectedPlatforms;
6990
- const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6991
- const value = platformPaths?.[platform];
6992
- return typeof value === "string" && value.trim().length > 0;
6993
- });
6994
- if (hasCompletePathMapping) return selectedPlatforms.map((platform) => {
6995
- const path$1 = platformPaths?.[platform]?.trim();
6996
- if (!path$1 || path$1 === ".") return platform;
6997
- return {
6998
- platform,
6999
- path: path$1
7000
- };
7001
- });
7002
- if (selectedPlatforms.length === 1) return selectedPlatforms;
7003
- const entries = [];
7004
- for (const platform of selectedPlatforms) {
7005
- const mappedPath = platformPaths?.[platform]?.trim();
7006
- const suggestedPath = mappedPath ?? (await directoryExists(join(directory, platform)) ? platform : ".");
7007
- const input = await p.text({
7008
- message: `Folder for ${platform} files ('.' = same folder as agentrules.json)`,
7009
- placeholder: suggestedPath,
7010
- defaultValue: suggestedPath
7011
- });
7012
- if (p.isCancel(input)) {
7013
- p.cancel("Cancelled");
7014
- process.exit(0);
7015
- }
7016
- const trimmed = input.trim();
7017
- const resolvedPath = trimmed.length > 0 ? trimmed : suggestedPath;
7018
- if (resolvedPath === ".") entries.push(platform);
7019
- else entries.push({
7020
- platform,
7021
- path: resolvedPath
7022
- });
7023
- }
7024
- return entries;
7025
- })();
7026
7155
  const configPath = join(directory, RULE_CONFIG_FILENAME);
7027
7156
  if (!force && await fileExists(configPath)) {
7028
7157
  const overwrite = await p.confirm({
@@ -7035,81 +7164,58 @@ async function initInteractive(options) {
7035
7164
  }
7036
7165
  force = true;
7037
7166
  }
7038
- const result = await p.group({
7039
- name: () => {
7040
- const normalizedDefault = normalizeName(defaultName);
7041
- return p.text({
7042
- message: "Rule name",
7043
- placeholder: normalizedDefault,
7044
- defaultValue: normalizedDefault,
7045
- validate: (value) => {
7046
- if (!value || value.trim() === "") return;
7047
- return check(nameSchema)(value);
7048
- }
7049
- });
7050
- },
7051
- title: ({ results }) => {
7052
- const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
7053
- return p.text({
7054
- message: "Title",
7055
- defaultValue: defaultTitle,
7056
- placeholder: defaultTitle
7057
- });
7058
- },
7059
- description: () => p.text({
7060
- message: "Description",
7061
- placeholder: "Describe what this rule does...",
7062
- defaultValue: descriptionOption,
7063
- validate: check(descriptionSchema)
7064
- }),
7065
- tags: () => p.text({
7066
- message: "Tags (comma-separated, optional)",
7067
- placeholder: "e.g., typescript, testing, react",
7068
- validate: checkTags
7069
- }),
7070
- license: async () => {
7071
- const choice = await p.select({
7072
- message: "License",
7073
- options: [...COMMON_LICENSES.map((id) => ({
7074
- value: id,
7075
- label: id
7076
- })), {
7077
- value: "__other__",
7078
- label: "Other (enter SPDX identifier)"
7079
- }],
7080
- initialValue: defaultLicense
7081
- });
7082
- if (p.isCancel(choice)) {
7083
- p.cancel("Cancelled");
7084
- process.exit(0);
7085
- }
7086
- if (choice === "__other__") {
7087
- const custom = await p.text({
7088
- message: "License (SPDX identifier)",
7089
- placeholder: "e.g., MPL-2.0, AGPL-3.0-only",
7090
- validate: check(licenseSchema)
7091
- });
7092
- if (p.isCancel(custom)) {
7093
- p.cancel("Cancelled");
7094
- process.exit(0);
7095
- }
7096
- return custom;
7097
- }
7098
- return choice;
7167
+ let collected;
7168
+ try {
7169
+ collected = await collectRuleInputs({
7170
+ directory,
7171
+ defaults: {
7172
+ name: options.name,
7173
+ title: options.title,
7174
+ description: options.description,
7175
+ platforms: options.platforms,
7176
+ platformPaths,
7177
+ license: options.license
7178
+ },
7179
+ nonInteractive: false
7180
+ });
7181
+ } catch (error$2) {
7182
+ if (error$2 instanceof Error && error$2.message === "Cancelled") {
7183
+ p.cancel("Cancelled");
7184
+ process.exit(0);
7099
7185
  }
7100
- }, { onCancel: () => {
7101
- p.cancel("Cancelled");
7102
- return process.exit(0);
7103
- } });
7186
+ throw error$2;
7187
+ }
7188
+ const platformEntries = collected.platforms.map((platform) => {
7189
+ const path$1 = collected.platformPaths[platform];
7190
+ return path$1 ? {
7191
+ platform,
7192
+ path: path$1
7193
+ } : platform;
7194
+ });
7195
+ log.print("");
7196
+ log.print(ui.rulePreview({
7197
+ header: "Rule configuration",
7198
+ path: directory,
7199
+ pathLabel: "Directory",
7200
+ name: collected.name,
7201
+ title: collected.title || toTitleCase(collected.name),
7202
+ description: collected.description,
7203
+ platforms: collected.platforms,
7204
+ type: collected.isSkill ? "skill" : void 0,
7205
+ tags: collected.tags,
7206
+ license: collected.license,
7207
+ showHints: true
7208
+ }));
7209
+ log.print("");
7104
7210
  const initOptions = {
7105
7211
  directory,
7106
- name: result.name,
7107
- type: useSkillDefaults ? "skill" : void 0,
7108
- title: result.title.trim() || void 0,
7109
- description: result.description,
7110
- tags: parseTags(result.tags),
7212
+ name: collected.name,
7213
+ type: collected.isSkill ? "skill" : void 0,
7214
+ title: collected.title || void 0,
7215
+ description: collected.description,
7216
+ tags: collected.tags,
7111
7217
  platforms: platformEntries,
7112
- license: result.license,
7218
+ license: collected.license,
7113
7219
  force
7114
7220
  };
7115
7221
  const initResult = await initRule(initOptions);
@@ -7582,7 +7688,7 @@ program.command("add <item>").description("Download and install a rule from the
7582
7688
  program.command("init").description("Initialize a new rule").argument("[directory]", "Directory to initialize (created if it doesn't exist)").option("-y, --yes", "Accept defaults without prompting").option("-n, --name <name>", "Rule name").option("-t, --title <title>", "Display title").option("--description <text>", "Rule description").option("-p, --platform <platform>", "Target platform(s). Repeatable, accepts comma-separated. Supports <platform>=<path> mappings.", (value, previous) => previous ? [...previous, value] : [value]).option("-l, --license <license>", "License (e.g., MIT)").option("-f, --force", "Overwrite existing agentrules.json").action(handle(async (directory, options) => {
7583
7689
  const targetDir = directory ?? process.cwd();
7584
7690
  const defaultName = directory ? basename(directory) : void 0;
7585
- const platformInputs = options.platform?.flatMap((p$2) => p$2.split(",").map((s) => s.trim())).filter((p$2) => p$2.length > 0);
7691
+ const platformInputs = options.platform?.flatMap((p$3) => p$3.split(",").map((s) => s.trim())).filter((p$3) => p$3.length > 0);
7586
7692
  const platformIds = [];
7587
7693
  const platformPaths = {};
7588
7694
  if (platformInputs) for (const input of platformInputs) {
@@ -7623,7 +7729,7 @@ program.command("init").description("Initialize a new rule").argument("[director
7623
7729
  }
7624
7730
  const nextSteps$1 = [
7625
7731
  "Add your rule files in this directory",
7626
- "Add tags (recommended) and features (recommended) to agentrules.json",
7732
+ "Add tags and features to agentrules.json",
7627
7733
  `Run ${ui.command("agentrules publish")} to publish your rule`
7628
7734
  ];
7629
7735
  log.print(`\n${ui.header("Next steps")}`);
@@ -7647,7 +7753,7 @@ program.command("init").description("Initialize a new rule").argument("[director
7647
7753
  }
7648
7754
  const nextSteps = [
7649
7755
  "Add your rule files in this directory",
7650
- "Add tags (recommended) and features (recommended) to agentrules.json",
7756
+ "Add tags and features to agentrules.json",
7651
7757
  `Run ${ui.command("agentrules publish")} to publish your rule`
7652
7758
  ];
7653
7759
  log.print(`\n${ui.header("Next steps")}`);
@@ -7656,13 +7762,13 @@ program.command("init").description("Initialize a new rule").argument("[director
7656
7762
  program.command("validate").description("Validate an agentrules.json configuration").argument("[path]", "Path to agentrules.json or directory").action(handle(async (path$1) => {
7657
7763
  const result = await validateRule({ path: path$1 });
7658
7764
  if (result.valid && result.rule) {
7659
- const p$2 = result.rule;
7660
- const platforms = p$2.platforms.map((entry) => entry.platform).join(", ");
7661
- log.success(p$2.title);
7662
- if (p$2.description) log.print(ui.keyValue("Description", p$2.description));
7663
- log.print(ui.keyValue("License", p$2.license));
7765
+ const p$3 = result.rule;
7766
+ const platforms = p$3.platforms.map((entry) => entry.platform).join(", ");
7767
+ log.success(p$3.title);
7768
+ if (p$3.description) log.print(ui.keyValue("Description", p$3.description));
7769
+ log.print(ui.keyValue("License", p$3.license));
7664
7770
  log.print(ui.keyValue("Platforms", platforms));
7665
- if (p$2.tags?.length) log.print(ui.keyValue("Tags", p$2.tags.join(", ")));
7771
+ if (p$3.tags?.length) log.print(ui.keyValue("Tags", p$3.tags.join(", ")));
7666
7772
  } else if (!result.valid) log.error(`Invalid: ${ui.path(result.configPath)}`);
7667
7773
  if (result.errors.length > 0) {
7668
7774
  log.print("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "author": "Brian Cheung <bcheung.dev@gmail.com> (https://github.com/bcheung)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://agentrules.directory",
@@ -53,7 +53,7 @@
53
53
  "clean": "rm -rf node_modules dist .turbo"
54
54
  },
55
55
  "dependencies": {
56
- "@agentrules/core": "0.3.0",
56
+ "@agentrules/core": "0.3.1",
57
57
  "@clack/prompts": "^0.11.0",
58
58
  "chalk": "^5.4.1",
59
59
  "commander": "^12.1.0",