@agentrules/cli 0.2.1 → 0.3.1

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 +3 -2
  2. package/dist/index.js +688 -372
  3. package/package.json +2 -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
 
@@ -63,6 +64,11 @@ const PLATFORMS = {
63
64
  description: "Custom tool",
64
65
  project: "{platformDir}/tool/{name}.ts",
65
66
  global: "{platformDir}/tool/{name}.ts"
67
+ },
68
+ skill: {
69
+ description: "Agent skill",
70
+ project: "{platformDir}/skill/{name}/SKILL.md",
71
+ global: "{platformDir}/skill/{name}/SKILL.md"
66
72
  }
67
73
  }
68
74
  },
@@ -112,6 +118,11 @@ const PLATFORMS = {
112
118
  description: "Custom slash command",
113
119
  project: "{platformDir}/commands/{name}.md",
114
120
  global: "{platformDir}/commands/{name}.md"
121
+ },
122
+ skill: {
123
+ description: "Agent skill",
124
+ project: "{platformDir}/skills/{name}/SKILL.md",
125
+ global: "{platformDir}/skills/{name}/SKILL.md"
115
126
  }
116
127
  }
117
128
  },
@@ -129,6 +140,11 @@ const PLATFORMS = {
129
140
  description: "Custom prompt",
130
141
  project: null,
131
142
  global: "{platformDir}/prompts/{name}.md"
143
+ },
144
+ skill: {
145
+ description: "Agent skill",
146
+ project: "{platformDir}/skills/{name}/SKILL.md",
147
+ global: "{platformDir}/skills/{name}/SKILL.md"
132
148
  }
133
149
  }
134
150
  }
@@ -265,6 +281,41 @@ function inferTypeFromPath(platform, filePath) {
265
281
  if (!nextDir) return;
266
282
  return getProjectTypeDirMap(platform).get(nextDir);
267
283
  }
284
+ /**
285
+ * Get the install directory for a type (parent directory of the install path).
286
+ * For skills, this is the directory containing SKILL.md.
287
+ */
288
+ function getInstallDir({ platform, type, name }) {
289
+ const installPath = getInstallPath({
290
+ platform,
291
+ type,
292
+ name,
293
+ scope: "project"
294
+ });
295
+ if (!installPath) return null;
296
+ const lastSlash = installPath.lastIndexOf("/");
297
+ if (lastSlash === -1) return null;
298
+ return installPath.slice(0, lastSlash);
299
+ }
300
+ /**
301
+ * Normalize skill files by finding SKILL.md anchor and adjusting all paths.
302
+ * Strips any existing path prefix to prevent duplication.
303
+ */
304
+ function normalizeSkillFiles({ files, installDir }) {
305
+ const marker = files.find((f) => f.path === "SKILL.md" || f.path.endsWith("/SKILL.md"));
306
+ if (!marker) throw new Error("SKILL.md not found in files");
307
+ const skillRoot = marker.path === "SKILL.md" ? "." : marker.path.slice(0, marker.path.lastIndexOf("/"));
308
+ return files.map((f) => {
309
+ let relative$1;
310
+ if (skillRoot === ".") relative$1 = f.path;
311
+ else if (f.path.startsWith(`${skillRoot}/`)) relative$1 = f.path.slice(skillRoot.length + 1);
312
+ else relative$1 = f.path;
313
+ return {
314
+ ...f,
315
+ path: `${installDir}/${relative$1}`
316
+ };
317
+ });
318
+ }
268
319
 
269
320
  //#endregion
270
321
  //#region ../core/src/utils/encoding.ts
@@ -1939,7 +1990,7 @@ const $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
1939
1990
  defineLazy(inst._zod, "pattern", () => {
1940
1991
  if (def.options.every((o) => o._zod.pattern)) {
1941
1992
  const patterns = def.options.map((o) => o._zod.pattern);
1942
- return new RegExp(`^(${patterns.map((p$2) => cleanRegex(p$2.source)).join("|")})$`);
1993
+ return new RegExp(`^(${patterns.map((p$3) => cleanRegex(p$3.source)).join("|")})$`);
1943
1994
  }
1944
1995
  return void 0;
1945
1996
  });
@@ -2318,9 +2369,9 @@ var $ZodRegistry = class {
2318
2369
  return this;
2319
2370
  }
2320
2371
  get(schema) {
2321
- const p$2 = schema._zod.parent;
2322
- if (p$2) {
2323
- const pm = { ...this.get(p$2) ?? {} };
2372
+ const p$3 = schema._zod.parent;
2373
+ if (p$3) {
2374
+ const pm = { ...this.get(p$3) ?? {} };
2324
2375
  delete pm.id;
2325
2376
  const f = {
2326
2377
  ...pm,
@@ -4325,8 +4376,8 @@ function command(cmd) {
4325
4376
  return theme.command(cmd);
4326
4377
  }
4327
4378
  /** Format a file path */
4328
- function path(p$2) {
4329
- return theme.path(p$2);
4379
+ function path(p$3) {
4380
+ return theme.path(p$3);
4330
4381
  }
4331
4382
  /** Format as muted/secondary text */
4332
4383
  function muted(text) {
@@ -4644,6 +4695,22 @@ function fileTree(files, options) {
4644
4695
  });
4645
4696
  return lines.join("\n");
4646
4697
  }
4698
+ function rulePreview(options) {
4699
+ const lines = [];
4700
+ lines.push(header(options.header));
4701
+ if (options.path) lines.push(keyValue(options.pathLabel ?? "Path", path(options.path)));
4702
+ lines.push(keyValue("Name", code(options.name)));
4703
+ lines.push(keyValue("Title", options.title));
4704
+ lines.push(keyValue("Description", options.description || dim("—")));
4705
+ lines.push(keyValue("Platforms", options.platforms.join(", ")));
4706
+ if (options.type) lines.push(keyValue("Type", options.type));
4707
+ if (options.tags && options.tags.length > 0) lines.push(keyValue("Tags", options.tags.join(", ")));
4708
+ else if (options.showHints) lines.push(keyValue("Tags", dim("— (add to improve discoverability)")));
4709
+ if (options.features && options.features.length > 0) lines.push(keyValue("Features", options.features.join(", ")));
4710
+ else if (options.showHints) lines.push(keyValue("Features", dim("— (highlight what this rule does)")));
4711
+ if (options.license) lines.push(keyValue("License", options.license));
4712
+ return lines.join("\n");
4713
+ }
4647
4714
  const ui = {
4648
4715
  theme,
4649
4716
  symbols,
@@ -4680,6 +4747,7 @@ const ui = {
4680
4747
  fileCounts,
4681
4748
  hint,
4682
4749
  link,
4750
+ rulePreview,
4683
4751
  stripAnsi,
4684
4752
  truncate,
4685
4753
  formatBytes: formatBytes$1,
@@ -5832,11 +5900,43 @@ async function directoryExists(path$1) {
5832
5900
  }
5833
5901
  }
5834
5902
 
5903
+ //#endregion
5904
+ //#region src/lib/zod-validator.ts
5905
+ /**
5906
+ * Creates a validator function from a Zod schema.
5907
+ * Returns error message if invalid, undefined if valid.
5908
+ */
5909
+ function check(schema) {
5910
+ return (value) => {
5911
+ const result = schema.safeParse(value);
5912
+ if (!result.success) return result.error.issues[0]?.message;
5913
+ return;
5914
+ };
5915
+ }
5916
+
5835
5917
  //#endregion
5836
5918
  //#region src/lib/rule-utils.ts
5837
- const INSTALL_FILENAME = "INSTALL.txt";
5838
- const README_FILENAME = "README.md";
5839
- const LICENSE_FILENAME = "LICENSE.md";
5919
+ const SKILL_FILENAME = "SKILL.md";
5920
+ /**
5921
+ * Parse SKILL.md frontmatter for name and license.
5922
+ * Only extracts simple key: value pairs we need for quick publish defaults.
5923
+ */
5924
+ function parseSkillFrontmatter(content) {
5925
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
5926
+ if (!match?.[1]) return {};
5927
+ const frontmatter = match[1];
5928
+ const result = {};
5929
+ const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
5930
+ if (nameMatch?.[1]) result.name = nameMatch[1].trim();
5931
+ const licenseMatch = frontmatter.match(/^license:\s*["']?([^"'\n]+)["']?\s*$/m);
5932
+ if (licenseMatch?.[1]) result.license = licenseMatch[1].trim();
5933
+ return result;
5934
+ }
5935
+ const METADATA_FILES = {
5936
+ install: ["INSTALL.txt"],
5937
+ readme: ["README.md"],
5938
+ license: ["LICENSE.md", "LICENSE.txt"]
5939
+ };
5840
5940
  /**
5841
5941
  * Files/directories that are always excluded from rules.
5842
5942
  * These are never useful in a rule bundle.
@@ -5937,7 +6037,7 @@ async function loadConfig(inputPath, overrides) {
5937
6037
  platforms
5938
6038
  };
5939
6039
  const configDir = dirname(configPath);
5940
- const platformNames = platforms.map((p$2) => p$2.platform).join(", ");
6040
+ const platformNames = platforms.map((plat) => plat.platform).join(", ");
5941
6041
  log.debug(`Loaded config: ${config$1.name}, platforms: ${platformNames}`);
5942
6042
  return {
5943
6043
  configPath,
@@ -5970,9 +6070,9 @@ async function loadRule(ruleDir, overrides) {
5970
6070
  }
5971
6071
  async function collectMetadata(loaded) {
5972
6072
  const { configDir } = loaded;
5973
- const installMessage = await readFileIfExists(join(configDir, INSTALL_FILENAME));
5974
- const readmeContent = await readFileIfExists(join(configDir, README_FILENAME));
5975
- const licenseContent = await readFileIfExists(join(configDir, LICENSE_FILENAME));
6073
+ const installMessage = await readFirstMatch(configDir, METADATA_FILES.install);
6074
+ const readmeContent = await readFirstMatch(configDir, METADATA_FILES.readme);
6075
+ const licenseContent = await readFirstMatch(configDir, METADATA_FILES.license);
5976
6076
  return {
5977
6077
  installMessage,
5978
6078
  readmeContent,
@@ -5990,6 +6090,34 @@ async function collectPlatformFiles(loaded) {
5990
6090
  const resolvedSourcePath = sourcePath ?? ".";
5991
6091
  const filesDir = join(configDir, resolvedSourcePath);
5992
6092
  log.debug(`Files for ${platform}: source=${resolvedSourcePath}, dir=${filesDir}`);
6093
+ const filesDirExists = await directoryExists(filesDir);
6094
+ const rootExclude = [RULE_CONFIG_FILENAME];
6095
+ if (filesDir === configDir) rootExclude.push(...METADATA_FILES.readme, ...METADATA_FILES.license, ...METADATA_FILES.install);
6096
+ const collectedFiles = filesDirExists ? await collectFiles(filesDir, rootExclude, ignorePatterns) : [];
6097
+ if (collectedFiles.length === 0) {
6098
+ if (!filesDirExists) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in the platform entry.`);
6099
+ throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
6100
+ }
6101
+ if (config$1.type === "skill") {
6102
+ const installDir = getInstallDir({
6103
+ platform,
6104
+ type: "skill",
6105
+ name: config$1.name
6106
+ });
6107
+ if (!installDir) throw new Error(`Platform "${platform}" does not support skill type.`);
6108
+ const normalizedFiles = normalizeSkillFiles({
6109
+ files: collectedFiles,
6110
+ installDir
6111
+ });
6112
+ platformFiles.push({
6113
+ platform,
6114
+ files: normalizedFiles.map((f) => ({
6115
+ path: f.path,
6116
+ content: typeof f.content === "string" ? f.content : new TextDecoder().decode(f.content)
6117
+ }))
6118
+ });
6119
+ continue;
6120
+ }
5993
6121
  const treatInstructionAsRoot = config$1.type === void 0 || config$1.type === "instruction";
5994
6122
  const instructionProjectPath = treatInstructionAsRoot ? getInstallPath({
5995
6123
  platform,
@@ -5997,10 +6125,6 @@ async function collectPlatformFiles(loaded) {
5997
6125
  scope: "project"
5998
6126
  }) : null;
5999
6127
  const instructionContent = instructionProjectPath ? await readFileIfExists(join(configDir, instructionProjectPath)) : void 0;
6000
- const filesDirExists = await directoryExists(filesDir);
6001
- const rootExclude = [RULE_CONFIG_FILENAME];
6002
- if (filesDir === configDir) rootExclude.push(README_FILENAME, LICENSE_FILENAME, INSTALL_FILENAME);
6003
- const collectedFiles = filesDirExists ? await collectFiles(filesDir, rootExclude, ignorePatterns) : [];
6004
6128
  const publishFiles = [];
6005
6129
  const seenPublishPaths = new Set();
6006
6130
  for (const file of collectedFiles) {
@@ -6020,10 +6144,7 @@ async function collectPlatformFiles(loaded) {
6020
6144
  content: instructionContent
6021
6145
  });
6022
6146
  }
6023
- if (publishFiles.length === 0) {
6024
- if (!filesDirExists) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in the platform entry.`);
6025
- throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
6026
- }
6147
+ if (publishFiles.length === 0) throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
6027
6148
  platformFiles.push({
6028
6149
  platform,
6029
6150
  files: publishFiles
@@ -6092,18 +6213,280 @@ async function readFileIfExists(path$1) {
6092
6213
  if (await fileExists(path$1)) return await readFile(path$1, "utf8");
6093
6214
  return;
6094
6215
  }
6216
+ /**
6217
+ * Try reading files from a list of candidates, return first match.
6218
+ */
6219
+ async function readFirstMatch(dir, filenames) {
6220
+ for (const filename of filenames) {
6221
+ const content = await readFileIfExists(join(dir, filename));
6222
+ if (content !== void 0) return content;
6223
+ }
6224
+ return;
6225
+ }
6226
+ /**
6227
+ * Detect if directory contains SKILL.md and extract frontmatter defaults.
6228
+ */
6229
+ async function detectSkillDirectory$1(directory) {
6230
+ const skillPath = join(directory, SKILL_FILENAME);
6231
+ if (!await fileExists(skillPath)) return;
6232
+ const content = await readFile(skillPath, "utf8");
6233
+ const frontmatter = parseSkillFrontmatter(content);
6234
+ return {
6235
+ name: frontmatter.name,
6236
+ license: frontmatter.license
6237
+ };
6238
+ }
6239
+ function parseTags(input) {
6240
+ if (typeof input !== "string") return [];
6241
+ if (input.trim().length === 0) return [];
6242
+ return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
6243
+ }
6244
+ function checkTags(value) {
6245
+ const tags = parseTags(value);
6246
+ const result = tagsSchema.safeParse(tags);
6247
+ if (!result.success) return result.error.issues[0]?.message;
6248
+ }
6249
+ /**
6250
+ * Collect rule inputs via interactive prompts or defaults.
6251
+ *
6252
+ * Handles:
6253
+ * - Skill detection with SKILL.md frontmatter
6254
+ * - Platform multiselect with keyboard hints
6255
+ * - Per-platform path prompting (for non-skill multi-platform)
6256
+ * - Name, title, description, tags, license prompts
6257
+ */
6258
+ async function collectRuleInputs(options) {
6259
+ const { directory, defaults = {}, nonInteractive = false } = options;
6260
+ const skillInfo = await detectSkillDirectory$1(directory);
6261
+ let isSkill = false;
6262
+ if (skillInfo && !nonInteractive) {
6263
+ const confirm = await p$2.confirm({
6264
+ message: `Detected SKILL.md${skillInfo.name ? ` (${skillInfo.name})` : ""}. Initialize as skill?`,
6265
+ initialValue: true
6266
+ });
6267
+ if (p$2.isCancel(confirm)) throw new Error("Cancelled");
6268
+ isSkill = confirm;
6269
+ } else if (skillInfo && nonInteractive) isSkill = true;
6270
+ const defaultName = isSkill && skillInfo?.name ? normalizeName(skillInfo.name) : defaults.name ?? "my-rule";
6271
+ const defaultLicense = isSkill && skillInfo?.license ? skillInfo.license : defaults.license ?? "MIT";
6272
+ const validatedPlatforms = [];
6273
+ if (defaults.platforms) for (const platform of defaults.platforms) {
6274
+ if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}"`);
6275
+ validatedPlatforms.push(platform);
6276
+ }
6277
+ const selectedPlatforms = validatedPlatforms.length > 0 ? validatedPlatforms : await (async () => {
6278
+ if (nonInteractive) throw new Error("Missing --platform in non-interactive mode");
6279
+ const platformChoices = await p$2.multiselect({
6280
+ message: `Platforms ${ui.dim("(space to toggle, 'a' to select all)")}`,
6281
+ options: PLATFORM_IDS.map((id) => ({
6282
+ value: id,
6283
+ label: id
6284
+ })),
6285
+ required: true
6286
+ });
6287
+ if (p$2.isCancel(platformChoices)) throw new Error("Cancelled");
6288
+ return platformChoices;
6289
+ })();
6290
+ const platformPaths = {};
6291
+ if (selectedPlatforms.length > 1 && !isSkill && !nonInteractive) {
6292
+ const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6293
+ const value = defaults.platformPaths?.[platform];
6294
+ return typeof value === "string" && value.trim().length > 0;
6295
+ });
6296
+ if (hasCompletePathMapping && defaults.platformPaths) for (const platform of selectedPlatforms) {
6297
+ const pathVal = defaults.platformPaths[platform]?.trim();
6298
+ if (pathVal && pathVal !== ".") platformPaths[platform] = pathVal;
6299
+ }
6300
+ else for (const platform of selectedPlatforms) {
6301
+ const mappedPath = defaults.platformPaths?.[platform]?.trim();
6302
+ const suggestedPath = mappedPath ?? (await directoryExists(join(directory, platform)) ? platform : ".");
6303
+ const input = await p$2.text({
6304
+ message: `Folder for ${platform} files ('.' = same folder as agentrules.json)`,
6305
+ placeholder: suggestedPath,
6306
+ defaultValue: suggestedPath
6307
+ });
6308
+ if (p$2.isCancel(input)) throw new Error("Cancelled");
6309
+ const trimmed = input.trim();
6310
+ const resolvedPath = trimmed.length > 0 ? trimmed : suggestedPath;
6311
+ if (resolvedPath !== ".") platformPaths[platform] = resolvedPath;
6312
+ }
6313
+ } else if (defaults.platformPaths) for (const platform of selectedPlatforms) {
6314
+ const pathVal = defaults.platformPaths[platform]?.trim();
6315
+ if (pathVal && pathVal !== ".") platformPaths[platform] = pathVal;
6316
+ }
6317
+ if (nonInteractive) {
6318
+ const name = normalizeName(defaults.name ?? defaultName);
6319
+ const nameCheck = nameSchema.safeParse(name);
6320
+ if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6321
+ return {
6322
+ platforms: selectedPlatforms,
6323
+ platformPaths,
6324
+ name,
6325
+ title: defaults.title ?? toTitleCase(name),
6326
+ description: defaults.description ?? "",
6327
+ tags: defaults.tags ?? [],
6328
+ license: defaultLicense,
6329
+ isSkill,
6330
+ ruleType: isSkill ? "skill" : defaults.ruleType
6331
+ };
6332
+ }
6333
+ const result = await p$2.group({
6334
+ name: () => {
6335
+ const normalizedDefault = normalizeName(defaultName);
6336
+ return p$2.text({
6337
+ message: "Rule name",
6338
+ placeholder: normalizedDefault,
6339
+ defaultValue: normalizedDefault,
6340
+ validate: (value) => {
6341
+ if (!value || value.trim() === "") return;
6342
+ return check(nameSchema)(value);
6343
+ }
6344
+ });
6345
+ },
6346
+ title: ({ results }) => {
6347
+ const defaultTitle = defaults.title ?? toTitleCase(results.name ?? defaultName);
6348
+ return p$2.text({
6349
+ message: "Title",
6350
+ defaultValue: defaultTitle,
6351
+ placeholder: defaultTitle
6352
+ });
6353
+ },
6354
+ description: () => p$2.text({
6355
+ message: "Description",
6356
+ placeholder: "Describe what this rule does...",
6357
+ defaultValue: defaults.description,
6358
+ validate: check(descriptionSchema)
6359
+ }),
6360
+ tags: () => p$2.text({
6361
+ message: "Tags (comma-separated, optional)",
6362
+ placeholder: "e.g., typescript, testing, react",
6363
+ validate: checkTags
6364
+ }),
6365
+ license: async () => {
6366
+ const choice = await p$2.select({
6367
+ message: "License",
6368
+ options: [...COMMON_LICENSES.map((id) => ({
6369
+ value: id,
6370
+ label: id
6371
+ })), {
6372
+ value: "__other__",
6373
+ label: "Other (enter SPDX identifier)"
6374
+ }],
6375
+ initialValue: defaultLicense
6376
+ });
6377
+ if (p$2.isCancel(choice)) throw new Error("Cancelled");
6378
+ if (choice === "__other__") {
6379
+ const custom = await p$2.text({
6380
+ message: "License (SPDX identifier)",
6381
+ placeholder: "e.g., MPL-2.0, AGPL-3.0-only",
6382
+ validate: check(licenseSchema)
6383
+ });
6384
+ if (p$2.isCancel(custom)) throw new Error("Cancelled");
6385
+ return custom;
6386
+ }
6387
+ return choice;
6388
+ }
6389
+ }, { onCancel: () => {
6390
+ throw new Error("Cancelled");
6391
+ } });
6392
+ const tags = parseTags(result.tags);
6393
+ return {
6394
+ platforms: selectedPlatforms,
6395
+ platformPaths,
6396
+ name: result.name,
6397
+ title: result.title.trim() || toTitleCase(result.name),
6398
+ description: result.description ?? "",
6399
+ tags,
6400
+ license: result.license,
6401
+ isSkill,
6402
+ ruleType: isSkill ? "skill" : defaults.ruleType
6403
+ };
6404
+ }
6095
6405
 
6096
6406
  //#endregion
6097
- //#region src/lib/zod-validator.ts
6407
+ //#region src/commands/rule/init.ts
6408
+ /** Default rule name when none specified */
6409
+ const DEFAULT_RULE_NAME = "my-rule";
6098
6410
  /**
6099
- * Creates a validator function from a Zod schema.
6100
- * Returns error message if invalid, undefined if valid.
6411
+ * Detect if directory contains SKILL.md and extract frontmatter defaults.
6101
6412
  */
6102
- function check(schema) {
6103
- return (value) => {
6104
- const result = schema.safeParse(value);
6105
- if (!result.success) return result.error.issues[0]?.message;
6106
- return;
6413
+ async function detectSkillDirectory(directory) {
6414
+ const skillPath = join(directory, SKILL_FILENAME);
6415
+ if (!await fileExists(skillPath)) return;
6416
+ const content = await readFile(skillPath, "utf8");
6417
+ const frontmatter = parseSkillFrontmatter(content);
6418
+ return {
6419
+ name: frontmatter.name,
6420
+ license: frontmatter.license
6421
+ };
6422
+ }
6423
+ /**
6424
+ * Initialize a rule in a directory (rule root).
6425
+ *
6426
+ * Structure:
6427
+ * - ruleDir/agentrules.json - rule config
6428
+ * - ruleDir/* - rule files (collected by default)
6429
+ * - ruleDir/README.md, ruleDir/LICENSE.md, ruleDir/INSTALL.txt - optional metadata (not bundled)
6430
+ */
6431
+ async function initRule(options) {
6432
+ const ruleDir = options.directory ?? process.cwd();
6433
+ log.debug(`Initializing rule in: ${ruleDir}`);
6434
+ const skillInfo = await detectSkillDirectory(ruleDir);
6435
+ const isSkillDirectory = skillInfo !== void 0;
6436
+ const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6437
+ const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6438
+ if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6439
+ const platforms = platformInputs.map(normalizePlatformEntryInput);
6440
+ const name = normalizeName(options.name ?? skillInfo?.name ?? DEFAULT_RULE_NAME);
6441
+ const title = options.title ?? toTitleCase(name);
6442
+ const description = options.description ?? "";
6443
+ const license = options.license ?? skillInfo?.license ?? "MIT";
6444
+ const ruleType = isSkillDirectory ? "skill" : options.type;
6445
+ const platformLabels = platforms.map((p$3) => typeof p$3 === "string" ? p$3 : p$3.platform).join(", ");
6446
+ log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6447
+ const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
6448
+ if (!options.force && await fileExists(configPath)) throw new Error(`${RULE_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
6449
+ const config$1 = {
6450
+ $schema: RULE_SCHEMA_URL,
6451
+ name,
6452
+ ...ruleType && { type: ruleType },
6453
+ title,
6454
+ version: 1,
6455
+ description,
6456
+ tags: options.tags ?? [],
6457
+ license,
6458
+ platforms
6459
+ };
6460
+ let createdDir;
6461
+ if (await directoryExists(ruleDir)) log.debug(`Directory exists: ${ruleDir}`);
6462
+ else {
6463
+ await mkdir(ruleDir, { recursive: true });
6464
+ createdDir = ruleDir;
6465
+ log.debug(`Created directory: ${ruleDir}`);
6466
+ }
6467
+ const content = `${JSON.stringify(config$1, null, 2)}\n`;
6468
+ await writeFile(configPath, content, "utf8");
6469
+ log.debug(`Wrote config file: ${configPath}`);
6470
+ log.debug("Rule initialization complete.");
6471
+ return {
6472
+ configPath,
6473
+ rule: config$1,
6474
+ createdDir
6475
+ };
6476
+ }
6477
+ function normalizePlatform(input) {
6478
+ const normalized = input.toLowerCase();
6479
+ if (!isSupportedPlatform(normalized)) throw new Error(`Unknown platform "${input}". Supported: ${PLATFORM_IDS.join(", ")}`);
6480
+ return normalized;
6481
+ }
6482
+ function normalizePlatformEntryInput(input) {
6483
+ if (typeof input === "string") return normalizePlatform(input);
6484
+ const platform = normalizePlatform(input.platform);
6485
+ const path$1 = typeof input.path === "string" ? input.path.trim() : "";
6486
+ if (path$1.length === 0 || path$1 === ".") return platform;
6487
+ return {
6488
+ platform,
6489
+ path: path$1
6107
6490
  };
6108
6491
  }
6109
6492
 
@@ -6111,14 +6494,46 @@ function check(schema) {
6111
6494
  //#region src/commands/publish.ts
6112
6495
  /** Maximum size per variant/platform bundle in bytes (1MB) */
6113
6496
  const MAX_VARIANT_SIZE_BYTES = 1 * 1024 * 1024;
6114
- /** Schema for parsing comma-separated tags input */
6115
- const tagsInputSchema = string().transform((input) => input.split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0)).pipe(tagsSchema);
6116
6497
  function formatBytes(bytes) {
6117
6498
  if (bytes < 1024) return `${bytes} B`;
6118
6499
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6119
6500
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6120
6501
  }
6121
6502
  /**
6503
+ * Prompts to create agentrules.json after quick publish (or dry run).
6504
+ * With --yes, creates without prompting.
6505
+ */
6506
+ async function promptToCreateConfig(quickPublish, yes) {
6507
+ if (!quickPublish) return false;
6508
+ let createConfig = yes ?? false;
6509
+ if (!yes && process.stdin.isTTY) {
6510
+ log.print("");
6511
+ const answer = await p$1.confirm({
6512
+ message: "Create agentrules.json for future publishes?",
6513
+ initialValue: true
6514
+ });
6515
+ createConfig = !p$1.isCancel(answer) && answer;
6516
+ }
6517
+ if (createConfig) {
6518
+ const sourceDir = quickPublish.source.type === "directory" ? quickPublish.source.path : void 0;
6519
+ const configDir = sourceDir ?? (quickPublish.source.path.replace(/[/\\][^/\\]+$/, "") || ".");
6520
+ await initRule({
6521
+ directory: configDir,
6522
+ name: quickPublish.name,
6523
+ title: quickPublish.title,
6524
+ description: quickPublish.description || void 0,
6525
+ platforms: quickPublish.platforms,
6526
+ type: quickPublish.ruleType,
6527
+ tags: quickPublish.tags,
6528
+ license: quickPublish.license,
6529
+ force: false
6530
+ });
6531
+ log.success(`Created ${ui.path(join(configDir, "agentrules.json"))}`);
6532
+ return true;
6533
+ }
6534
+ return false;
6535
+ }
6536
+ /**
6122
6537
  * Publishes a rule to the registry.
6123
6538
  *
6124
6539
  * Supports:
@@ -6139,11 +6554,12 @@ async function publish(options = {}) {
6139
6554
  }
6140
6555
  if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
6141
6556
  const filePath = await getSingleFilePath(path$1);
6557
+ const quickDir = await getQuickPublishDirectory(path$1);
6142
6558
  let publishInput;
6143
6559
  if (filePath) {
6144
6560
  let resolved;
6145
6561
  try {
6146
- resolved = await resolveSingleFileInputs({
6562
+ resolved = await resolveQuickPublishInputs({
6147
6563
  name,
6148
6564
  platform,
6149
6565
  type,
@@ -6151,7 +6567,10 @@ async function publish(options = {}) {
6151
6567
  description,
6152
6568
  tags,
6153
6569
  license
6154
- }, filePath, {
6570
+ }, {
6571
+ type: "file",
6572
+ path: filePath
6573
+ }, {
6155
6574
  dryRun,
6156
6575
  yes
6157
6576
  });
@@ -6166,7 +6585,7 @@ async function publish(options = {}) {
6166
6585
  const fileSpinner = await log.spinner("Building bundle...");
6167
6586
  try {
6168
6587
  publishInput = await buildPublishInput({
6169
- rule: await buildRuleInputFromSingleFile(resolved),
6588
+ rule: await buildRuleInputFromQuickPublish(resolved),
6170
6589
  version: version$2
6171
6590
  });
6172
6591
  log.debug(`Built publish input for platforms: ${publishInput.variants.map((v) => v.platform).join(", ")}`);
@@ -6184,7 +6603,61 @@ async function publish(options = {}) {
6184
6603
  dryRun,
6185
6604
  version: version$2,
6186
6605
  spinner: fileSpinner,
6187
- ctx
6606
+ ctx,
6607
+ quickPublish: resolved,
6608
+ yes
6609
+ });
6610
+ }
6611
+ if (quickDir) {
6612
+ let resolved;
6613
+ try {
6614
+ resolved = await resolveQuickPublishInputs({
6615
+ name,
6616
+ platform,
6617
+ type,
6618
+ title,
6619
+ description,
6620
+ tags,
6621
+ license
6622
+ }, {
6623
+ type: "directory",
6624
+ path: quickDir
6625
+ }, {
6626
+ dryRun,
6627
+ yes
6628
+ });
6629
+ } catch (error$2) {
6630
+ const message = getErrorMessage(error$2);
6631
+ log.error(message);
6632
+ return {
6633
+ success: false,
6634
+ error: message
6635
+ };
6636
+ }
6637
+ const dirSpinner = await log.spinner("Building bundle...");
6638
+ try {
6639
+ publishInput = await buildPublishInput({
6640
+ rule: await buildRuleInputFromQuickPublish(resolved),
6641
+ version: version$2
6642
+ });
6643
+ log.debug(`Built publish input for platforms: ${publishInput.variants.map((v) => v.platform).join(", ")}`);
6644
+ } catch (error$2) {
6645
+ const message = getErrorMessage(error$2);
6646
+ dirSpinner.fail("Failed to build bundle");
6647
+ log.error(message);
6648
+ return {
6649
+ success: false,
6650
+ error: message
6651
+ };
6652
+ }
6653
+ return await finalizePublish({
6654
+ publishInput,
6655
+ dryRun,
6656
+ version: version$2,
6657
+ spinner: dirSpinner,
6658
+ ctx,
6659
+ quickPublish: resolved,
6660
+ yes
6188
6661
  });
6189
6662
  }
6190
6663
  const spinner$1 = await log.spinner("Validating rule...");
@@ -6275,19 +6748,27 @@ async function getSingleFilePath(inputPath) {
6275
6748
  if (basename(inputPath) === RULE_CONFIG_FILENAME) return;
6276
6749
  return inputPath;
6277
6750
  }
6751
+ async function getQuickPublishDirectory(inputPath) {
6752
+ if (!inputPath) return;
6753
+ const pathStat = await stat(inputPath).catch(() => null);
6754
+ if (!pathStat?.isDirectory()) return;
6755
+ const configStat = await stat(`${inputPath}/${RULE_CONFIG_FILENAME}`).catch(() => null);
6756
+ if (configStat?.isFile()) return;
6757
+ return inputPath;
6758
+ }
6278
6759
  function normalizePathForInference(value) {
6279
6760
  return value.replace(/\\/g, "/");
6280
6761
  }
6281
6762
  function stripExtension(value) {
6282
6763
  return value.replace(/\.[^/.]+$/, "");
6283
6764
  }
6284
- function inferSingleFileDefaults(filePath) {
6765
+ function inferFileDefaults(filePath) {
6285
6766
  const normalized = normalizePathForInference(filePath);
6286
6767
  const segments = normalized.split("/").filter(Boolean);
6287
6768
  const fileName = segments.at(-1) ?? "";
6288
6769
  const instructionPlatforms = inferInstructionPlatformsFromFileName(fileName);
6289
6770
  if (instructionPlatforms.length > 0) return {
6290
- type: "instruction",
6771
+ ruleType: "instruction",
6291
6772
  ...instructionPlatforms.length === 1 ? { platform: instructionPlatforms[0] } : {}
6292
6773
  };
6293
6774
  const platform = inferPlatformFromPath(filePath);
@@ -6295,7 +6776,7 @@ function inferSingleFileDefaults(filePath) {
6295
6776
  const inferredType = inferTypeFromPath(platform, filePath);
6296
6777
  const result = { platform };
6297
6778
  if (inferredType) {
6298
- result.type = inferredType;
6779
+ result.ruleType = inferredType;
6299
6780
  if (inferredType !== "instruction") result.name = normalizeName(stripExtension(fileName));
6300
6781
  }
6301
6782
  return result;
@@ -6311,34 +6792,24 @@ function buildConfigPublishOverrides(options) {
6311
6792
  if (options.tags !== void 0) overrides.tags = options.tags;
6312
6793
  return Object.keys(overrides).length > 0 ? overrides : void 0;
6313
6794
  }
6314
- async function resolveSingleFileInputs(options, filePath, ctx) {
6315
- const inferred = inferSingleFileDefaults(filePath);
6795
+ async function resolveQuickPublishInputs(options, source, ctx) {
6316
6796
  const isInteractive = !ctx.yes && process.stdin.isTTY;
6317
- const hasRequiredArgs = options.name !== void 0 && options.platform !== void 0 && options.type !== void 0;
6318
- if (!(isInteractive || hasRequiredArgs)) throw new Error("Publishing a single file in non-interactive mode requires --name, --platform, and --type.");
6319
- const selectedPlatforms = options.platform ? parsePlatformSelection(options.platform) : void 0;
6320
- if (selectedPlatforms && selectedPlatforms.length > 1) throw new Error("Publishing a single file requires exactly one --platform value.");
6321
- let selectedPlatform = selectedPlatforms?.[0] ? normalizePlatformInput(selectedPlatforms[0]) : inferred.platform;
6322
- if (!selectedPlatform) {
6323
- if (!isInteractive) throw new Error("Missing --platform");
6324
- const selection = await p$1.select({
6325
- message: "Platform",
6326
- options: PLATFORM_IDS.map((id) => ({
6327
- value: id,
6328
- label: id
6329
- }))
6330
- });
6331
- if (p$1.isCancel(selection)) throw new Error("Cancelled");
6332
- selectedPlatform = selection;
6333
- }
6334
- let selectedType = options.type ?? inferred.type;
6335
- if (!selectedType) {
6336
- if (!isInteractive) throw new Error("Missing --type");
6337
- const candidates = getValidTypes(selectedPlatform).filter((t) => supportsInstallPath({
6338
- platform: selectedPlatform,
6797
+ const isDirectory = source.type === "directory";
6798
+ const isFile = source.type === "file";
6799
+ const fileInferred = isFile ? inferFileDefaults(source.path) : {};
6800
+ const parsedPlatforms = options.platform ? parsePlatformSelection(options.platform).map(normalizePlatformInput) : void 0;
6801
+ let selectedType = options.type ?? fileInferred.ruleType;
6802
+ const platformsForTypeCheck = parsedPlatforms && parsedPlatforms.length > 0 ? parsedPlatforms : fileInferred.platform ? [fileInferred.platform] : [];
6803
+ if (isFile && !selectedType) {
6804
+ if (!isInteractive) throw new Error("Publishing a single file in non-interactive mode requires --name, --platform, and --type.");
6805
+ if (platformsForTypeCheck.length === 0) throw new Error("Missing --platform");
6806
+ const candidateSets = platformsForTypeCheck.map((plat) => getValidTypes(plat).filter((t) => supportsInstallPath({
6807
+ platform: plat,
6339
6808
  type: t,
6340
6809
  scope: "project"
6341
- }));
6810
+ })));
6811
+ const candidates = candidateSets.length === 1 ? candidateSets[0] : candidateSets.reduce((acc, set) => acc.filter((t) => set.includes(t)));
6812
+ if (candidates.length === 0) throw new Error(`No common type supports all selected platforms: ${platformsForTypeCheck.join(", ")}`);
6342
6813
  const selection = await p$1.select({
6343
6814
  message: "Type",
6344
6815
  options: candidates.map((t) => ({
@@ -6349,117 +6820,127 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6349
6820
  if (p$1.isCancel(selection)) throw new Error("Cancelled");
6350
6821
  selectedType = selection;
6351
6822
  }
6352
- const platform = selectedPlatform;
6353
- const type = selectedType;
6354
- const nameValue = options.name ?? await (async () => {
6355
- if (!isInteractive) throw new Error("Missing --name");
6356
- const input = await p$1.text({
6357
- message: "Rule name",
6358
- placeholder: "my-rule",
6359
- validate: check(nameSchema)
6360
- });
6361
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6362
- return input;
6363
- })();
6364
- const normalizedName = normalizeName(nameValue);
6365
- const nameCheck = nameSchema.safeParse(normalizedName);
6366
- if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6367
- const bundlePath = getInstallPath({
6823
+ if (options.type && platformsForTypeCheck.length > 0) {
6824
+ const ruleType = selectedType;
6825
+ for (const platform of platformsForTypeCheck) if (!supportsInstallPath({
6826
+ platform,
6827
+ type: ruleType,
6828
+ scope: "project"
6829
+ })) throw new Error(`Type "${ruleType}" is not supported for project installs on platform "${platform}".`);
6830
+ }
6831
+ const collected = await collectRuleInputs({
6832
+ directory: isDirectory ? source.path : dirname(source.path),
6833
+ defaults: {
6834
+ name: options.name ?? fileInferred.name,
6835
+ title: options.title,
6836
+ description: options.description,
6837
+ platforms: parsedPlatforms,
6838
+ license: options.license ?? fileInferred.license,
6839
+ tags: options.tags,
6840
+ ruleType: selectedType
6841
+ },
6842
+ nonInteractive: !isInteractive,
6843
+ detectType: isDirectory
6844
+ });
6845
+ const finalRuleType = isFile ? selectedType : collected.ruleType ?? (collected.isSkill ? "skill" : "instruction");
6846
+ for (const platform of collected.platforms) if (!supportsInstallPath({
6368
6847
  platform,
6369
- type,
6370
- name: normalizedName,
6848
+ type: finalRuleType,
6371
6849
  scope: "project"
6372
- });
6373
- if (!bundlePath) throw new Error(`Type "${type}" is not supported for project installs on platform "${platform}".`);
6374
- const defaultTitle = toTitleCase(normalizedName);
6375
- const finalTitle = options.title ?? await (async () => {
6376
- if (!isInteractive) return defaultTitle;
6377
- const input = await p$1.text({
6378
- message: "Title",
6379
- defaultValue: defaultTitle,
6380
- placeholder: defaultTitle
6381
- });
6382
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6383
- return input;
6384
- })();
6385
- const finalDescription = options.description ?? await (async () => {
6386
- if (!isInteractive) return "";
6387
- const input = await p$1.text({
6388
- message: "Description (optional)",
6389
- placeholder: "Describe what this rule does...",
6390
- validate: check(descriptionSchema)
6391
- });
6392
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6393
- return input;
6394
- })();
6395
- const finalTags = await (async () => {
6396
- if (options.tags) return tagsSchema.parse(options.tags);
6397
- if (!isInteractive) return [];
6398
- const input = await p$1.text({
6399
- message: "Tags (optional)",
6400
- placeholder: "comma-separated, e.g. typescript, react",
6401
- validate: check(tagsInputSchema)
6402
- });
6403
- if (p$1.isCancel(input)) throw new Error("Cancelled");
6404
- return tagsInputSchema.parse(input);
6405
- })();
6406
- const finalLicense = options.license ?? "MIT";
6850
+ })) throw new Error(`Type "${finalRuleType}" is not supported for project installs on platform "${platform}".`);
6407
6851
  if (isInteractive && !ctx.dryRun) {
6408
6852
  log.print("");
6409
- log.print(ui.header("Quick publish"));
6410
- log.print(ui.keyValue("File", ui.path(filePath)));
6411
- log.print(ui.keyValue("Name", ui.code(normalizedName)));
6412
- log.print(ui.keyValue("Title", finalTitle));
6413
- log.print(ui.keyValue("Description", finalDescription));
6414
- log.print(ui.keyValue("Platform", platform));
6415
- log.print(ui.keyValue("Type", type));
6416
- log.print(ui.keyValue("Installs to", ui.path(bundlePath)));
6417
- if (finalTags.length > 0) log.print(ui.keyValue("Tags", finalTags.join(", ")));
6853
+ log.print(ui.rulePreview({
6854
+ header: "Quick publish",
6855
+ path: source.path,
6856
+ pathLabel: isDirectory ? "Directory" : "File",
6857
+ name: collected.name,
6858
+ title: collected.title,
6859
+ description: collected.description,
6860
+ platforms: collected.platforms,
6861
+ type: finalRuleType,
6862
+ tags: collected.tags,
6863
+ showHints: true
6864
+ }));
6418
6865
  log.print("");
6419
6866
  const confirm = await p$1.confirm({
6420
- message: "Publish this file?",
6867
+ message: isDirectory ? "Publish this directory?" : "Publish this file?",
6421
6868
  initialValue: true
6422
6869
  });
6423
6870
  if (p$1.isCancel(confirm) || !confirm) throw new Error("Cancelled");
6424
6871
  }
6425
6872
  return {
6426
- filePath,
6427
- name: normalizedName,
6428
- platform,
6429
- type,
6430
- title: finalTitle,
6431
- description: finalDescription,
6432
- tags: finalTags,
6433
- license: finalLicense,
6434
- bundlePath
6873
+ source,
6874
+ name: collected.name,
6875
+ platforms: collected.platforms,
6876
+ platformPaths: isFile ? {} : collected.platformPaths,
6877
+ ruleType: finalRuleType,
6878
+ title: collected.title,
6879
+ description: collected.description,
6880
+ tags: collected.tags,
6881
+ license: collected.license
6435
6882
  };
6436
6883
  }
6437
- async function buildRuleInputFromSingleFile(inputs) {
6438
- const content = await readFile(inputs.filePath);
6884
+ async function buildRuleInputFromQuickPublish(inputs) {
6885
+ const platformEntries = inputs.platforms.map((platform) => {
6886
+ const path$1 = inputs.platformPaths[platform];
6887
+ return path$1 ? {
6888
+ platform,
6889
+ path: path$1
6890
+ } : { platform };
6891
+ });
6439
6892
  const config$1 = {
6440
6893
  $schema: RULE_SCHEMA_URL,
6441
6894
  name: inputs.name,
6442
- type: inputs.type,
6895
+ type: inputs.ruleType,
6443
6896
  title: inputs.title,
6444
6897
  description: inputs.description,
6445
6898
  license: inputs.license,
6446
6899
  tags: inputs.tags,
6447
- platforms: [{ platform: inputs.platform }]
6900
+ platforms: platformEntries
6901
+ };
6902
+ if (inputs.source.type === "file") {
6903
+ const content = await readFile(inputs.source.path);
6904
+ const platformFiles$1 = [];
6905
+ for (const platform of inputs.platforms) {
6906
+ const bundlePath = getInstallPath({
6907
+ platform,
6908
+ type: inputs.ruleType,
6909
+ name: inputs.name,
6910
+ scope: "project"
6911
+ });
6912
+ if (!bundlePath) throw new Error(`Type "${inputs.ruleType}" is not supported for project installs on platform "${platform}".`);
6913
+ platformFiles$1.push({
6914
+ platform,
6915
+ files: [{
6916
+ path: bundlePath,
6917
+ content
6918
+ }]
6919
+ });
6920
+ }
6921
+ return {
6922
+ name: inputs.name,
6923
+ config: config$1,
6924
+ platformFiles: platformFiles$1
6925
+ };
6926
+ }
6927
+ const loadedConfig = {
6928
+ configPath: `${inputs.source.path}/agentrules.json`,
6929
+ config: {
6930
+ ...config$1,
6931
+ platforms: platformEntries
6932
+ },
6933
+ configDir: inputs.source.path
6448
6934
  };
6935
+ const platformFiles = await collectPlatformFiles(loadedConfig);
6449
6936
  return {
6450
6937
  name: inputs.name,
6451
6938
  config: config$1,
6452
- platformFiles: [{
6453
- platform: inputs.platform,
6454
- files: [{
6455
- path: inputs.bundlePath,
6456
- content
6457
- }]
6458
- }]
6939
+ platformFiles
6459
6940
  };
6460
6941
  }
6461
6942
  async function finalizePublish(options) {
6462
- const { publishInput, dryRun, version: version$2, spinner: spinner$1, ctx } = options;
6943
+ const { publishInput, dryRun, version: version$2, spinner: spinner$1, ctx, quickPublish, yes } = options;
6463
6944
  const totalFileCount = publishInput.variants.reduce((sum, v) => sum + v.files.length, 0);
6464
6945
  const platformList = publishInput.variants.map((v) => v.platform).join(", ");
6465
6946
  let totalSize = 0;
@@ -6498,6 +6979,7 @@ async function finalizePublish(options) {
6498
6979
  log.print("");
6499
6980
  }
6500
6981
  log.print(ui.hint("Run without --dry-run to publish."));
6982
+ await promptToCreateConfig(quickPublish, yes);
6501
6983
  return {
6502
6984
  success: true,
6503
6985
  preview: {
@@ -6545,6 +7027,7 @@ async function finalizePublish(options) {
6545
7027
  }
6546
7028
  log.info("");
6547
7029
  log.info(ui.keyValue("Now live at", ui.link(data.url)));
7030
+ await promptToCreateConfig(quickPublish, yes);
6548
7031
  return {
6549
7032
  success: true,
6550
7033
  rule: {
@@ -6635,165 +7118,15 @@ async function discoverRuleDirs(inputDir) {
6635
7118
  return ruleDirs.sort();
6636
7119
  }
6637
7120
 
6638
- //#endregion
6639
- //#region src/commands/rule/init.ts
6640
- /** Default rule name when none specified */
6641
- const DEFAULT_RULE_NAME$1 = "my-rule";
6642
- /**
6643
- * Initialize a rule in a directory (rule root).
6644
- *
6645
- * Structure:
6646
- * - ruleDir/agentrules.json - rule config
6647
- * - ruleDir/* - rule files (collected by default)
6648
- * - ruleDir/README.md, ruleDir/LICENSE.md, ruleDir/INSTALL.txt - optional metadata (not bundled)
6649
- */
6650
- async function initRule(options) {
6651
- const ruleDir = options.directory ?? process.cwd();
6652
- log.debug(`Initializing rule in: ${ruleDir}`);
6653
- const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6654
- const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6655
- if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6656
- const platforms = platformInputs.map(normalizePlatformEntryInput);
6657
- const name = normalizeName(options.name ?? DEFAULT_RULE_NAME$1);
6658
- const title = options.title ?? toTitleCase(name);
6659
- const description = options.description ?? "";
6660
- const license = options.license ?? "MIT";
6661
- const platformLabels = platforms.map((p$2) => typeof p$2 === "string" ? p$2 : p$2.platform).join(", ");
6662
- log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6663
- const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
6664
- if (!options.force && await fileExists(configPath)) throw new Error(`${RULE_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
6665
- const config$1 = {
6666
- $schema: RULE_SCHEMA_URL,
6667
- name,
6668
- ...options.type && { type: options.type },
6669
- title,
6670
- version: 1,
6671
- description,
6672
- tags: options.tags ?? [],
6673
- license,
6674
- platforms
6675
- };
6676
- let createdDir;
6677
- if (await directoryExists(ruleDir)) log.debug(`Directory exists: ${ruleDir}`);
6678
- else {
6679
- await mkdir(ruleDir, { recursive: true });
6680
- createdDir = ruleDir;
6681
- log.debug(`Created directory: ${ruleDir}`);
6682
- }
6683
- const content = `${JSON.stringify(config$1, null, 2)}\n`;
6684
- await writeFile(configPath, content, "utf8");
6685
- log.debug(`Wrote config file: ${configPath}`);
6686
- log.debug("Rule initialization complete.");
6687
- return {
6688
- configPath,
6689
- rule: config$1,
6690
- createdDir
6691
- };
6692
- }
6693
- function normalizePlatform(input) {
6694
- const normalized = input.toLowerCase();
6695
- if (!isSupportedPlatform(normalized)) throw new Error(`Unknown platform "${input}". Supported: ${PLATFORM_IDS.join(", ")}`);
6696
- return normalized;
6697
- }
6698
- function normalizePlatformEntryInput(input) {
6699
- if (typeof input === "string") return normalizePlatform(input);
6700
- const platform = normalizePlatform(input.platform);
6701
- const path$1 = typeof input.path === "string" ? input.path.trim() : "";
6702
- if (path$1.length === 0 || path$1 === ".") return platform;
6703
- return {
6704
- platform,
6705
- path: path$1
6706
- };
6707
- }
6708
-
6709
7121
  //#endregion
6710
7122
  //#region src/commands/rule/init-interactive.ts
6711
- const DEFAULT_RULE_NAME = "my-rule";
6712
- /**
6713
- * Parse comma-separated tags string into array.
6714
- */
6715
- function parseTags(input) {
6716
- if (typeof input !== "string") return [];
6717
- if (input.trim().length === 0) return [];
6718
- return input.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0);
6719
- }
6720
- /**
6721
- * Validator for comma-separated tags input.
6722
- */
6723
- function checkTags(value) {
6724
- const tags = parseTags(value);
6725
- const result = tagsSchema.safeParse(tags);
6726
- if (!result.success) return result.error.issues[0]?.message;
6727
- }
6728
7123
  /**
6729
7124
  * Run interactive init flow with clack prompts.
6730
7125
  */
6731
7126
  async function initInteractive(options) {
6732
- const { directory, name: nameOption, title: titleOption, description: descriptionOption, platforms: platformsOption, platformPaths, license: licenseOption } = options;
7127
+ const { directory, platformPaths } = options;
6733
7128
  let { force } = options;
6734
- const defaultName = nameOption ?? DEFAULT_RULE_NAME;
6735
7129
  p.intro("Create a new rule");
6736
- const validatedPlatforms = [];
6737
- if (platformsOption) for (const platform of platformsOption) {
6738
- if (!isSupportedPlatform(platform)) {
6739
- p.cancel(`Unknown platform "${platform}"`);
6740
- process.exit(1);
6741
- }
6742
- validatedPlatforms.push(platform);
6743
- }
6744
- const selectedPlatforms = validatedPlatforms.length > 0 ? validatedPlatforms : await (async () => {
6745
- const platformChoices = await p.multiselect({
6746
- message: "Platforms (select one or more)",
6747
- options: PLATFORM_IDS.map((id) => ({
6748
- value: id,
6749
- label: id
6750
- })),
6751
- required: true
6752
- });
6753
- if (p.isCancel(platformChoices)) {
6754
- p.cancel("Cancelled");
6755
- process.exit(0);
6756
- }
6757
- return platformChoices;
6758
- })();
6759
- const platformEntries = await (async () => {
6760
- if (selectedPlatforms.length === 0) return [];
6761
- const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6762
- const value = platformPaths?.[platform];
6763
- return typeof value === "string" && value.trim().length > 0;
6764
- });
6765
- if (hasCompletePathMapping) return selectedPlatforms.map((platform) => {
6766
- const path$1 = platformPaths?.[platform]?.trim();
6767
- if (!path$1 || path$1 === ".") return platform;
6768
- return {
6769
- platform,
6770
- path: path$1
6771
- };
6772
- });
6773
- if (selectedPlatforms.length === 1) return selectedPlatforms;
6774
- const entries = [];
6775
- for (const platform of selectedPlatforms) {
6776
- const mappedPath = platformPaths?.[platform]?.trim();
6777
- const suggestedPath = mappedPath ?? (await directoryExists(join(directory, platform)) ? platform : ".");
6778
- const input = await p.text({
6779
- message: `Folder for ${platform} files ('.' = same folder as agentrules.json)`,
6780
- placeholder: suggestedPath,
6781
- defaultValue: suggestedPath
6782
- });
6783
- if (p.isCancel(input)) {
6784
- p.cancel("Cancelled");
6785
- process.exit(0);
6786
- }
6787
- const trimmed = input.trim();
6788
- const resolvedPath = trimmed.length > 0 ? trimmed : suggestedPath;
6789
- if (resolvedPath === ".") entries.push(platform);
6790
- else entries.push({
6791
- platform,
6792
- path: resolvedPath
6793
- });
6794
- }
6795
- return entries;
6796
- })();
6797
7130
  const configPath = join(directory, RULE_CONFIG_FILENAME);
6798
7131
  if (!force && await fileExists(configPath)) {
6799
7132
  const overwrite = await p.confirm({
@@ -6806,75 +7139,58 @@ async function initInteractive(options) {
6806
7139
  }
6807
7140
  force = true;
6808
7141
  }
6809
- const result = await p.group({
6810
- name: () => p.text({
6811
- message: "Rule name",
6812
- placeholder: normalizeName(defaultName),
6813
- defaultValue: normalizeName(defaultName),
6814
- validate: check(nameSchema)
6815
- }),
6816
- title: ({ results }) => {
6817
- const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
6818
- return p.text({
6819
- message: "Title",
6820
- defaultValue: defaultTitle,
6821
- placeholder: defaultTitle
6822
- });
6823
- },
6824
- description: () => p.text({
6825
- message: "Description",
6826
- placeholder: "Describe what this rule does...",
6827
- defaultValue: descriptionOption,
6828
- validate: check(descriptionSchema)
6829
- }),
6830
- tags: () => p.text({
6831
- message: "Tags (comma-separated, optional)",
6832
- placeholder: "e.g., typescript, testing, react",
6833
- validate: checkTags
6834
- }),
6835
- license: async () => {
6836
- const defaultLicense = licenseOption ?? "MIT";
6837
- const choice = await p.select({
6838
- message: "License",
6839
- options: [...COMMON_LICENSES.map((id) => ({
6840
- value: id,
6841
- label: id
6842
- })), {
6843
- value: "__other__",
6844
- label: "Other (enter SPDX identifier)"
6845
- }],
6846
- initialValue: defaultLicense
6847
- });
6848
- if (p.isCancel(choice)) {
6849
- p.cancel("Cancelled");
6850
- process.exit(0);
6851
- }
6852
- if (choice === "__other__") {
6853
- const custom = await p.text({
6854
- message: "License (SPDX identifier)",
6855
- placeholder: "e.g., MPL-2.0, AGPL-3.0-only",
6856
- validate: check(licenseSchema)
6857
- });
6858
- if (p.isCancel(custom)) {
6859
- p.cancel("Cancelled");
6860
- process.exit(0);
6861
- }
6862
- return custom;
6863
- }
6864
- return choice;
7142
+ let collected;
7143
+ try {
7144
+ collected = await collectRuleInputs({
7145
+ directory,
7146
+ defaults: {
7147
+ name: options.name,
7148
+ title: options.title,
7149
+ description: options.description,
7150
+ platforms: options.platforms,
7151
+ platformPaths,
7152
+ license: options.license
7153
+ },
7154
+ nonInteractive: false
7155
+ });
7156
+ } catch (error$2) {
7157
+ if (error$2 instanceof Error && error$2.message === "Cancelled") {
7158
+ p.cancel("Cancelled");
7159
+ process.exit(0);
6865
7160
  }
6866
- }, { onCancel: () => {
6867
- p.cancel("Cancelled");
6868
- return process.exit(0);
6869
- } });
7161
+ throw error$2;
7162
+ }
7163
+ const platformEntries = collected.platforms.map((platform) => {
7164
+ const path$1 = collected.platformPaths[platform];
7165
+ return path$1 ? {
7166
+ platform,
7167
+ path: path$1
7168
+ } : platform;
7169
+ });
7170
+ log.print("");
7171
+ log.print(ui.rulePreview({
7172
+ header: "Rule configuration",
7173
+ path: directory,
7174
+ pathLabel: "Directory",
7175
+ name: collected.name,
7176
+ title: collected.title || toTitleCase(collected.name),
7177
+ description: collected.description,
7178
+ platforms: collected.platforms,
7179
+ type: collected.isSkill ? "skill" : void 0,
7180
+ tags: collected.tags,
7181
+ license: collected.license,
7182
+ showHints: true
7183
+ }));
7184
+ log.print("");
6870
7185
  const initOptions = {
6871
7186
  directory,
6872
- name: result.name,
6873
- title: result.title.trim() || void 0,
6874
- description: result.description,
6875
- tags: parseTags(result.tags),
7187
+ name: collected.name,
7188
+ type: collected.isSkill ? "skill" : void 0,
7189
+ title: collected.title || void 0,
7190
+ description: collected.description,
7191
+ tags: collected.tags,
6876
7192
  platforms: platformEntries,
6877
- license: result.license,
7193
+ license: collected.license,
6878
7194
  force
6879
7195
  };
6880
7196
  const initResult = await initRule(initOptions);
@@ -7347,7 +7663,7 @@ program.command("add <item>").description("Download and install a rule from the
7347
7663
  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) => {
7348
7664
  const targetDir = directory ?? process.cwd();
7349
7665
  const defaultName = directory ? basename(directory) : void 0;
7350
- const platformInputs = options.platform?.flatMap((p$2) => p$2.split(",").map((s) => s.trim())).filter((p$2) => p$2.length > 0);
7666
+ const platformInputs = options.platform?.flatMap((p$3) => p$3.split(",").map((s) => s.trim())).filter((p$3) => p$3.length > 0);
7351
7667
  const platformIds = [];
7352
7668
  const platformPaths = {};
7353
7669
  if (platformInputs) for (const input of platformInputs) {
@@ -7388,7 +7704,7 @@ program.command("init").description("Initialize a new rule").argument("[director
7388
7704
  }
7389
7705
  const nextSteps$1 = [
7390
7706
  "Add your rule files in this directory",
7391
- "Add tags (recommended) and features (recommended) to agentrules.json",
7707
+ "Add tags and features to agentrules.json",
7392
7708
  `Run ${ui.command("agentrules publish")} to publish your rule`
7393
7709
  ];
7394
7710
  log.print(`\n${ui.header("Next steps")}`);
@@ -7412,7 +7728,7 @@ program.command("init").description("Initialize a new rule").argument("[director
7412
7728
  }
7413
7729
  const nextSteps = [
7414
7730
  "Add your rule files in this directory",
7415
- "Add tags (recommended) and features (recommended) to agentrules.json",
7731
+ "Add tags and features to agentrules.json",
7416
7732
  `Run ${ui.command("agentrules publish")} to publish your rule`
7417
7733
  ];
7418
7734
  log.print(`\n${ui.header("Next steps")}`);
@@ -7421,13 +7737,13 @@ program.command("init").description("Initialize a new rule").argument("[director
7421
7737
  program.command("validate").description("Validate an agentrules.json configuration").argument("[path]", "Path to agentrules.json or directory").action(handle(async (path$1) => {
7422
7738
  const result = await validateRule({ path: path$1 });
7423
7739
  if (result.valid && result.rule) {
7424
- const p$2 = result.rule;
7425
- const platforms = p$2.platforms.map((entry) => entry.platform).join(", ");
7426
- log.success(p$2.title);
7427
- if (p$2.description) log.print(ui.keyValue("Description", p$2.description));
7428
- log.print(ui.keyValue("License", p$2.license));
7740
+ const p$3 = result.rule;
7741
+ const platforms = p$3.platforms.map((entry) => entry.platform).join(", ");
7742
+ log.success(p$3.title);
7743
+ if (p$3.description) log.print(ui.keyValue("Description", p$3.description));
7744
+ log.print(ui.keyValue("License", p$3.license));
7429
7745
  log.print(ui.keyValue("Platforms", platforms));
7430
- if (p$2.tags?.length) log.print(ui.keyValue("Tags", p$2.tags.join(", ")));
7746
+ if (p$3.tags?.length) log.print(ui.keyValue("Tags", p$3.tags.join(", ")));
7431
7747
  } else if (!result.valid) log.error(`Invalid: ${ui.path(result.configPath)}`);
7432
7748
  if (result.errors.length > 0) {
7433
7749
  log.print("");