@agentrules/cli 0.2.1 → 0.3.0

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 +1 -1
  2. package/dist/index.js +300 -65
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -105,7 +105,7 @@ A typical single-platform rule structure is:
105
105
  .
106
106
  ├── agentrules.json # Rule config (created by init)
107
107
  ├── README.md # Shown on registry page (optional, not bundled)
108
- ├── LICENSE.md # Full license text (optional, not bundled)
108
+ ├── LICENSE.md # Full license text (optional, not bundled; LICENSE.txt also supported)
109
109
  ├── INSTALL.txt # Shown after install (optional, not bundled)
110
110
  ├── AGENTS.md # Instruction file (optional)
111
111
  └── command/
package/dist/index.js CHANGED
@@ -63,6 +63,11 @@ const PLATFORMS = {
63
63
  description: "Custom tool",
64
64
  project: "{platformDir}/tool/{name}.ts",
65
65
  global: "{platformDir}/tool/{name}.ts"
66
+ },
67
+ skill: {
68
+ description: "Agent skill",
69
+ project: "{platformDir}/skill/{name}/SKILL.md",
70
+ global: "{platformDir}/skill/{name}/SKILL.md"
66
71
  }
67
72
  }
68
73
  },
@@ -112,6 +117,11 @@ const PLATFORMS = {
112
117
  description: "Custom slash command",
113
118
  project: "{platformDir}/commands/{name}.md",
114
119
  global: "{platformDir}/commands/{name}.md"
120
+ },
121
+ skill: {
122
+ description: "Agent skill",
123
+ project: "{platformDir}/skills/{name}/SKILL.md",
124
+ global: "{platformDir}/skills/{name}/SKILL.md"
115
125
  }
116
126
  }
117
127
  },
@@ -129,6 +139,11 @@ const PLATFORMS = {
129
139
  description: "Custom prompt",
130
140
  project: null,
131
141
  global: "{platformDir}/prompts/{name}.md"
142
+ },
143
+ skill: {
144
+ description: "Agent skill",
145
+ project: "{platformDir}/skills/{name}/SKILL.md",
146
+ global: "{platformDir}/skills/{name}/SKILL.md"
132
147
  }
133
148
  }
134
149
  }
@@ -265,6 +280,41 @@ function inferTypeFromPath(platform, filePath) {
265
280
  if (!nextDir) return;
266
281
  return getProjectTypeDirMap(platform).get(nextDir);
267
282
  }
283
+ /**
284
+ * Get the install directory for a type (parent directory of the install path).
285
+ * For skills, this is the directory containing SKILL.md.
286
+ */
287
+ function getInstallDir({ platform, type, name }) {
288
+ const installPath = getInstallPath({
289
+ platform,
290
+ type,
291
+ name,
292
+ scope: "project"
293
+ });
294
+ if (!installPath) return null;
295
+ const lastSlash = installPath.lastIndexOf("/");
296
+ if (lastSlash === -1) return null;
297
+ return installPath.slice(0, lastSlash);
298
+ }
299
+ /**
300
+ * Normalize skill files by finding SKILL.md anchor and adjusting all paths.
301
+ * Strips any existing path prefix to prevent duplication.
302
+ */
303
+ function normalizeSkillFiles({ files, installDir }) {
304
+ const marker = files.find((f) => f.path === "SKILL.md" || f.path.endsWith("/SKILL.md"));
305
+ if (!marker) throw new Error("SKILL.md not found in files");
306
+ const skillRoot = marker.path === "SKILL.md" ? "." : marker.path.slice(0, marker.path.lastIndexOf("/"));
307
+ return files.map((f) => {
308
+ let relative$1;
309
+ if (skillRoot === ".") relative$1 = f.path;
310
+ else if (f.path.startsWith(`${skillRoot}/`)) relative$1 = f.path.slice(skillRoot.length + 1);
311
+ else relative$1 = f.path;
312
+ return {
313
+ ...f,
314
+ path: `${installDir}/${relative$1}`
315
+ };
316
+ });
317
+ }
268
318
 
269
319
  //#endregion
270
320
  //#region ../core/src/utils/encoding.ts
@@ -5834,9 +5884,27 @@ async function directoryExists(path$1) {
5834
5884
 
5835
5885
  //#endregion
5836
5886
  //#region src/lib/rule-utils.ts
5837
- const INSTALL_FILENAME = "INSTALL.txt";
5838
- const README_FILENAME = "README.md";
5839
- const LICENSE_FILENAME = "LICENSE.md";
5887
+ const SKILL_FILENAME = "SKILL.md";
5888
+ /**
5889
+ * Parse SKILL.md frontmatter for name and license.
5890
+ * Only extracts simple key: value pairs we need for quick publish defaults.
5891
+ */
5892
+ function parseSkillFrontmatter(content) {
5893
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
5894
+ if (!match?.[1]) return {};
5895
+ const frontmatter = match[1];
5896
+ const result = {};
5897
+ const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
5898
+ if (nameMatch?.[1]) result.name = nameMatch[1].trim();
5899
+ const licenseMatch = frontmatter.match(/^license:\s*["']?([^"'\n]+)["']?\s*$/m);
5900
+ if (licenseMatch?.[1]) result.license = licenseMatch[1].trim();
5901
+ return result;
5902
+ }
5903
+ const METADATA_FILES = {
5904
+ install: ["INSTALL.txt"],
5905
+ readme: ["README.md"],
5906
+ license: ["LICENSE.md", "LICENSE.txt"]
5907
+ };
5840
5908
  /**
5841
5909
  * Files/directories that are always excluded from rules.
5842
5910
  * These are never useful in a rule bundle.
@@ -5970,9 +6038,9 @@ async function loadRule(ruleDir, overrides) {
5970
6038
  }
5971
6039
  async function collectMetadata(loaded) {
5972
6040
  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));
6041
+ const installMessage = await readFirstMatch(configDir, METADATA_FILES.install);
6042
+ const readmeContent = await readFirstMatch(configDir, METADATA_FILES.readme);
6043
+ const licenseContent = await readFirstMatch(configDir, METADATA_FILES.license);
5976
6044
  return {
5977
6045
  installMessage,
5978
6046
  readmeContent,
@@ -5990,6 +6058,34 @@ async function collectPlatformFiles(loaded) {
5990
6058
  const resolvedSourcePath = sourcePath ?? ".";
5991
6059
  const filesDir = join(configDir, resolvedSourcePath);
5992
6060
  log.debug(`Files for ${platform}: source=${resolvedSourcePath}, dir=${filesDir}`);
6061
+ const filesDirExists = await directoryExists(filesDir);
6062
+ const rootExclude = [RULE_CONFIG_FILENAME];
6063
+ if (filesDir === configDir) rootExclude.push(...METADATA_FILES.readme, ...METADATA_FILES.license, ...METADATA_FILES.install);
6064
+ const collectedFiles = filesDirExists ? await collectFiles(filesDir, rootExclude, ignorePatterns) : [];
6065
+ if (collectedFiles.length === 0) {
6066
+ if (!filesDirExists) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in the platform entry.`);
6067
+ throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
6068
+ }
6069
+ if (config$1.type === "skill") {
6070
+ const installDir = getInstallDir({
6071
+ platform,
6072
+ type: "skill",
6073
+ name: config$1.name
6074
+ });
6075
+ if (!installDir) throw new Error(`Platform "${platform}" does not support skill type.`);
6076
+ const normalizedFiles = normalizeSkillFiles({
6077
+ files: collectedFiles,
6078
+ installDir
6079
+ });
6080
+ platformFiles.push({
6081
+ platform,
6082
+ files: normalizedFiles.map((f) => ({
6083
+ path: f.path,
6084
+ content: typeof f.content === "string" ? f.content : new TextDecoder().decode(f.content)
6085
+ }))
6086
+ });
6087
+ continue;
6088
+ }
5993
6089
  const treatInstructionAsRoot = config$1.type === void 0 || config$1.type === "instruction";
5994
6090
  const instructionProjectPath = treatInstructionAsRoot ? getInstallPath({
5995
6091
  platform,
@@ -5997,10 +6093,6 @@ async function collectPlatformFiles(loaded) {
5997
6093
  scope: "project"
5998
6094
  }) : null;
5999
6095
  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
6096
  const publishFiles = [];
6005
6097
  const seenPublishPaths = new Set();
6006
6098
  for (const file of collectedFiles) {
@@ -6020,10 +6112,7 @@ async function collectPlatformFiles(loaded) {
6020
6112
  content: instructionContent
6021
6113
  });
6022
6114
  }
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
- }
6115
+ if (publishFiles.length === 0) throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
6027
6116
  platformFiles.push({
6028
6117
  platform,
6029
6118
  files: publishFiles
@@ -6092,6 +6181,16 @@ async function readFileIfExists(path$1) {
6092
6181
  if (await fileExists(path$1)) return await readFile(path$1, "utf8");
6093
6182
  return;
6094
6183
  }
6184
+ /**
6185
+ * Try reading files from a list of candidates, return first match.
6186
+ */
6187
+ async function readFirstMatch(dir, filenames) {
6188
+ for (const filename of filenames) {
6189
+ const content = await readFileIfExists(join(dir, filename));
6190
+ if (content !== void 0) return content;
6191
+ }
6192
+ return;
6193
+ }
6095
6194
 
6096
6195
  //#endregion
6097
6196
  //#region src/lib/zod-validator.ts
@@ -6139,11 +6238,12 @@ async function publish(options = {}) {
6139
6238
  }
6140
6239
  if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
6141
6240
  const filePath = await getSingleFilePath(path$1);
6241
+ const quickDir = await getQuickPublishDirectory(path$1);
6142
6242
  let publishInput;
6143
6243
  if (filePath) {
6144
6244
  let resolved;
6145
6245
  try {
6146
- resolved = await resolveSingleFileInputs({
6246
+ resolved = await resolveQuickPublishInputs({
6147
6247
  name,
6148
6248
  platform,
6149
6249
  type,
@@ -6151,7 +6251,10 @@ async function publish(options = {}) {
6151
6251
  description,
6152
6252
  tags,
6153
6253
  license
6154
- }, filePath, {
6254
+ }, {
6255
+ type: "file",
6256
+ path: filePath
6257
+ }, {
6155
6258
  dryRun,
6156
6259
  yes
6157
6260
  });
@@ -6166,7 +6269,7 @@ async function publish(options = {}) {
6166
6269
  const fileSpinner = await log.spinner("Building bundle...");
6167
6270
  try {
6168
6271
  publishInput = await buildPublishInput({
6169
- rule: await buildRuleInputFromSingleFile(resolved),
6272
+ rule: await buildRuleInputFromQuickPublish(resolved),
6170
6273
  version: version$2
6171
6274
  });
6172
6275
  log.debug(`Built publish input for platforms: ${publishInput.variants.map((v) => v.platform).join(", ")}`);
@@ -6187,6 +6290,57 @@ async function publish(options = {}) {
6187
6290
  ctx
6188
6291
  });
6189
6292
  }
6293
+ if (quickDir) {
6294
+ let resolved;
6295
+ try {
6296
+ resolved = await resolveQuickPublishInputs({
6297
+ name,
6298
+ platform,
6299
+ type,
6300
+ title,
6301
+ description,
6302
+ tags,
6303
+ license
6304
+ }, {
6305
+ type: "directory",
6306
+ path: quickDir.path,
6307
+ entryFile: quickDir.entryFile
6308
+ }, {
6309
+ dryRun,
6310
+ yes
6311
+ });
6312
+ } catch (error$2) {
6313
+ const message = getErrorMessage(error$2);
6314
+ log.error(message);
6315
+ return {
6316
+ success: false,
6317
+ error: message
6318
+ };
6319
+ }
6320
+ const dirSpinner = await log.spinner("Building bundle...");
6321
+ try {
6322
+ publishInput = await buildPublishInput({
6323
+ rule: await buildRuleInputFromQuickPublish(resolved),
6324
+ version: version$2
6325
+ });
6326
+ log.debug(`Built publish input for platforms: ${publishInput.variants.map((v) => v.platform).join(", ")}`);
6327
+ } catch (error$2) {
6328
+ const message = getErrorMessage(error$2);
6329
+ dirSpinner.fail("Failed to build bundle");
6330
+ log.error(message);
6331
+ return {
6332
+ success: false,
6333
+ error: message
6334
+ };
6335
+ }
6336
+ return await finalizePublish({
6337
+ publishInput,
6338
+ dryRun,
6339
+ version: version$2,
6340
+ spinner: dirSpinner,
6341
+ ctx
6342
+ });
6343
+ }
6190
6344
  const spinner$1 = await log.spinner("Validating rule...");
6191
6345
  const configPath = await resolveConfigPath(path$1);
6192
6346
  log.debug(`Resolved config path: ${configPath}`);
@@ -6275,19 +6429,34 @@ async function getSingleFilePath(inputPath) {
6275
6429
  if (basename(inputPath) === RULE_CONFIG_FILENAME) return;
6276
6430
  return inputPath;
6277
6431
  }
6432
+ async function getQuickPublishDirectory(inputPath) {
6433
+ if (!inputPath) return;
6434
+ const pathStat = await stat(inputPath).catch(() => null);
6435
+ if (!pathStat?.isDirectory()) return;
6436
+ const configStat = await stat(`${inputPath}/${RULE_CONFIG_FILENAME}`).catch(() => null);
6437
+ 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;
6446
+ }
6278
6447
  function normalizePathForInference(value) {
6279
6448
  return value.replace(/\\/g, "/");
6280
6449
  }
6281
6450
  function stripExtension(value) {
6282
6451
  return value.replace(/\.[^/.]+$/, "");
6283
6452
  }
6284
- function inferSingleFileDefaults(filePath) {
6453
+ function inferFileDefaults(filePath) {
6285
6454
  const normalized = normalizePathForInference(filePath);
6286
6455
  const segments = normalized.split("/").filter(Boolean);
6287
6456
  const fileName = segments.at(-1) ?? "";
6288
6457
  const instructionPlatforms = inferInstructionPlatformsFromFileName(fileName);
6289
6458
  if (instructionPlatforms.length > 0) return {
6290
- type: "instruction",
6459
+ ruleType: "instruction",
6291
6460
  ...instructionPlatforms.length === 1 ? { platform: instructionPlatforms[0] } : {}
6292
6461
  };
6293
6462
  const platform = inferPlatformFromPath(filePath);
@@ -6295,11 +6464,22 @@ function inferSingleFileDefaults(filePath) {
6295
6464
  const inferredType = inferTypeFromPath(platform, filePath);
6296
6465
  const result = { platform };
6297
6466
  if (inferredType) {
6298
- result.type = inferredType;
6467
+ result.ruleType = inferredType;
6299
6468
  if (inferredType !== "instruction") result.name = normalizeName(stripExtension(fileName));
6300
6469
  }
6301
6470
  return result;
6302
6471
  }
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
+ }
6303
6483
  function buildConfigPublishOverrides(options) {
6304
6484
  const overrides = {};
6305
6485
  if (options.name !== void 0) overrides.name = options.name;
@@ -6311,13 +6491,17 @@ function buildConfigPublishOverrides(options) {
6311
6491
  if (options.tags !== void 0) overrides.tags = options.tags;
6312
6492
  return Object.keys(overrides).length > 0 ? overrides : void 0;
6313
6493
  }
6314
- async function resolveSingleFileInputs(options, filePath, ctx) {
6315
- const inferred = inferSingleFileDefaults(filePath);
6494
+ async function resolveQuickPublishInputs(options, source, ctx) {
6495
+ const inferred = source.type === "file" ? inferFileDefaults(source.path) : await inferDirectoryDefaults(source.path, source.entryFile, "skill");
6316
6496
  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.");
6497
+ 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
+ }
6319
6503
  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.");
6504
+ if (selectedPlatforms && selectedPlatforms.length > 1) throw new Error("Quick publish requires exactly one --platform value.");
6321
6505
  let selectedPlatform = selectedPlatforms?.[0] ? normalizePlatformInput(selectedPlatforms[0]) : inferred.platform;
6322
6506
  if (!selectedPlatform) {
6323
6507
  if (!isInteractive) throw new Error("Missing --platform");
@@ -6331,7 +6515,7 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6331
6515
  if (p$1.isCancel(selection)) throw new Error("Cancelled");
6332
6516
  selectedPlatform = selection;
6333
6517
  }
6334
- let selectedType = options.type ?? inferred.type;
6518
+ let selectedType = options.type ?? inferred.ruleType;
6335
6519
  if (!selectedType) {
6336
6520
  if (!isInteractive) throw new Error("Missing --type");
6337
6521
  const candidates = getValidTypes(selectedPlatform).filter((t) => supportsInstallPath({
@@ -6350,8 +6534,8 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6350
6534
  selectedType = selection;
6351
6535
  }
6352
6536
  const platform = selectedPlatform;
6353
- const type = selectedType;
6354
- const nameValue = options.name ?? await (async () => {
6537
+ const ruleType = selectedType;
6538
+ const nameValue = options.name ?? inferred.name ?? await (async () => {
6355
6539
  if (!isInteractive) throw new Error("Missing --name");
6356
6540
  const input = await p$1.text({
6357
6541
  message: "Rule name",
@@ -6364,13 +6548,6 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6364
6548
  const normalizedName = normalizeName(nameValue);
6365
6549
  const nameCheck = nameSchema.safeParse(normalizedName);
6366
6550
  if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6367
- const bundlePath = getInstallPath({
6368
- platform,
6369
- type,
6370
- name: normalizedName,
6371
- scope: "project"
6372
- });
6373
- if (!bundlePath) throw new Error(`Type "${type}" is not supported for project installs on platform "${platform}".`);
6374
6551
  const defaultTitle = toTitleCase(normalizedName);
6375
6552
  const finalTitle = options.title ?? await (async () => {
6376
6553
  if (!isInteractive) return defaultTitle;
@@ -6403,59 +6580,80 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6403
6580
  if (p$1.isCancel(input)) throw new Error("Cancelled");
6404
6581
  return tagsInputSchema.parse(input);
6405
6582
  })();
6406
- const finalLicense = options.license ?? "MIT";
6583
+ const finalLicense = options.license ?? inferred.license ?? "MIT";
6407
6584
  if (isInteractive && !ctx.dryRun) {
6408
6585
  log.print("");
6409
6586
  log.print(ui.header("Quick publish"));
6410
- log.print(ui.keyValue("File", ui.path(filePath)));
6587
+ log.print(ui.keyValue(isDirectory ? "Directory" : "File", ui.path(source.path)));
6411
6588
  log.print(ui.keyValue("Name", ui.code(normalizedName)));
6412
6589
  log.print(ui.keyValue("Title", finalTitle));
6413
6590
  log.print(ui.keyValue("Description", finalDescription));
6414
6591
  log.print(ui.keyValue("Platform", platform));
6415
- log.print(ui.keyValue("Type", type));
6416
- log.print(ui.keyValue("Installs to", ui.path(bundlePath)));
6592
+ log.print(ui.keyValue("Type", ruleType));
6417
6593
  if (finalTags.length > 0) log.print(ui.keyValue("Tags", finalTags.join(", ")));
6418
6594
  log.print("");
6419
6595
  const confirm = await p$1.confirm({
6420
- message: "Publish this file?",
6596
+ message: isDirectory ? "Publish this directory?" : "Publish this file?",
6421
6597
  initialValue: true
6422
6598
  });
6423
6599
  if (p$1.isCancel(confirm) || !confirm) throw new Error("Cancelled");
6424
6600
  }
6425
6601
  return {
6426
- filePath,
6602
+ source,
6427
6603
  name: normalizedName,
6428
6604
  platform,
6429
- type,
6605
+ ruleType,
6430
6606
  title: finalTitle,
6431
6607
  description: finalDescription,
6432
6608
  tags: finalTags,
6433
- license: finalLicense,
6434
- bundlePath
6609
+ license: finalLicense
6435
6610
  };
6436
6611
  }
6437
- async function buildRuleInputFromSingleFile(inputs) {
6438
- const content = await readFile(inputs.filePath);
6612
+ async function buildRuleInputFromQuickPublish(inputs) {
6439
6613
  const config$1 = {
6440
6614
  $schema: RULE_SCHEMA_URL,
6441
6615
  name: inputs.name,
6442
- type: inputs.type,
6616
+ type: inputs.ruleType,
6443
6617
  title: inputs.title,
6444
6618
  description: inputs.description,
6445
6619
  license: inputs.license,
6446
6620
  tags: inputs.tags,
6447
6621
  platforms: [{ platform: inputs.platform }]
6448
6622
  };
6623
+ if (inputs.source.type === "file") {
6624
+ 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,
6637
+ files: [{
6638
+ path: bundlePath,
6639
+ content
6640
+ }]
6641
+ }]
6642
+ };
6643
+ }
6644
+ const loadedConfig = {
6645
+ configPath: `${inputs.source.path}/agentrules.json`,
6646
+ config: {
6647
+ ...config$1,
6648
+ platforms: [{ platform: inputs.platform }]
6649
+ },
6650
+ configDir: inputs.source.path
6651
+ };
6652
+ const platformFiles = await collectPlatformFiles(loadedConfig);
6449
6653
  return {
6450
6654
  name: inputs.name,
6451
6655
  config: config$1,
6452
- platformFiles: [{
6453
- platform: inputs.platform,
6454
- files: [{
6455
- path: inputs.bundlePath,
6456
- content
6457
- }]
6458
- }]
6656
+ platformFiles
6459
6657
  };
6460
6658
  }
6461
6659
  async function finalizePublish(options) {
@@ -6640,6 +6838,19 @@ async function discoverRuleDirs(inputDir) {
6640
6838
  /** Default rule name when none specified */
6641
6839
  const DEFAULT_RULE_NAME$1 = "my-rule";
6642
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
+ /**
6643
6854
  * Initialize a rule in a directory (rule root).
6644
6855
  *
6645
6856
  * Structure:
@@ -6650,14 +6861,17 @@ const DEFAULT_RULE_NAME$1 = "my-rule";
6650
6861
  async function initRule(options) {
6651
6862
  const ruleDir = options.directory ?? process.cwd();
6652
6863
  log.debug(`Initializing rule in: ${ruleDir}`);
6864
+ const skillInfo = await detectSkillDirectory(ruleDir);
6865
+ const isSkillDirectory = skillInfo !== void 0;
6653
6866
  const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6654
6867
  const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6655
6868
  if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6656
6869
  const platforms = platformInputs.map(normalizePlatformEntryInput);
6657
- const name = normalizeName(options.name ?? DEFAULT_RULE_NAME$1);
6870
+ const name = normalizeName(options.name ?? skillInfo?.name ?? DEFAULT_RULE_NAME$1);
6658
6871
  const title = options.title ?? toTitleCase(name);
6659
6872
  const description = options.description ?? "";
6660
- const license = options.license ?? "MIT";
6873
+ const license = options.license ?? skillInfo?.license ?? "MIT";
6874
+ const ruleType = isSkillDirectory ? "skill" : options.type;
6661
6875
  const platformLabels = platforms.map((p$2) => typeof p$2 === "string" ? p$2 : p$2.platform).join(", ");
6662
6876
  log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6663
6877
  const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
@@ -6665,7 +6879,7 @@ async function initRule(options) {
6665
6879
  const config$1 = {
6666
6880
  $schema: RULE_SCHEMA_URL,
6667
6881
  name,
6668
- ...options.type && { type: options.type },
6882
+ ...ruleType && { type: ruleType },
6669
6883
  title,
6670
6884
  version: 1,
6671
6885
  description,
@@ -6731,8 +6945,22 @@ function checkTags(value) {
6731
6945
  async function initInteractive(options) {
6732
6946
  const { directory, name: nameOption, title: titleOption, description: descriptionOption, platforms: platformsOption, platformPaths, license: licenseOption } = options;
6733
6947
  let { force } = options;
6734
- const defaultName = nameOption ?? DEFAULT_RULE_NAME;
6735
6948
  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";
6736
6964
  const validatedPlatforms = [];
6737
6965
  if (platformsOption) for (const platform of platformsOption) {
6738
6966
  if (!isSupportedPlatform(platform)) {
@@ -6758,6 +6986,7 @@ async function initInteractive(options) {
6758
6986
  })();
6759
6987
  const platformEntries = await (async () => {
6760
6988
  if (selectedPlatforms.length === 0) return [];
6989
+ if (useSkillDefaults) return selectedPlatforms;
6761
6990
  const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6762
6991
  const value = platformPaths?.[platform];
6763
6992
  return typeof value === "string" && value.trim().length > 0;
@@ -6807,12 +7036,18 @@ async function initInteractive(options) {
6807
7036
  force = true;
6808
7037
  }
6809
7038
  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
- }),
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
+ },
6816
7051
  title: ({ results }) => {
6817
7052
  const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
6818
7053
  return p.text({
@@ -6833,7 +7068,6 @@ async function initInteractive(options) {
6833
7068
  validate: checkTags
6834
7069
  }),
6835
7070
  license: async () => {
6836
- const defaultLicense = licenseOption ?? "MIT";
6837
7071
  const choice = await p.select({
6838
7072
  message: "License",
6839
7073
  options: [...COMMON_LICENSES.map((id) => ({
@@ -6870,6 +7104,7 @@ async function initInteractive(options) {
6870
7104
  const initOptions = {
6871
7105
  directory,
6872
7106
  name: result.name,
7107
+ type: useSkillDefaults ? "skill" : void 0,
6873
7108
  title: result.title.trim() || void 0,
6874
7109
  description: result.description,
6875
7110
  tags: parseTags(result.tags),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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.2.1",
56
+ "@agentrules/core": "0.3.0",
57
57
  "@clack/prompts": "^0.11.0",
58
58
  "chalk": "^5.4.1",
59
59
  "commander": "^12.1.0",