@agentrules/cli 0.2.0 → 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 +343 -78
  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
  }
@@ -162,6 +177,21 @@ function getInstallPath({ platform, type, name, scope = "project" }) {
162
177
  if (template.includes("{name}") && !name) throw new Error(`Missing name for install path: platform="${platform}" type="${type}" scope="${scope}"`);
163
178
  return template.replace("{platformDir}", rootDir).replace("{name}", name ?? "");
164
179
  }
180
+ /**
181
+ * Get the relative install path for a type (without the root directory prefix).
182
+ * Returns the path relative to the platform's root directory.
183
+ *
184
+ * Example: For codex instruction with scope="global", returns "AGENTS.md"
185
+ * (not "~/.codex/AGENTS.md")
186
+ */
187
+ function getRelativeInstallPath({ platform, type, name, scope = "project" }) {
188
+ const typeConfig = getTypeConfig(platform, type);
189
+ if (!typeConfig) return null;
190
+ const template = scope === "project" ? typeConfig.project : typeConfig.global;
191
+ if (!template) return null;
192
+ if (template.includes("{name}") && !name) throw new Error(`Missing name for install path: platform="${platform}" type="${type}" scope="${scope}"`);
193
+ return template.replace("{platformDir}/", "").replace("{platformDir}", "").replace("{name}", name ?? "");
194
+ }
165
195
 
166
196
  //#endregion
167
197
  //#region ../core/src/platform/utils.ts
@@ -250,6 +280,41 @@ function inferTypeFromPath(platform, filePath) {
250
280
  if (!nextDir) return;
251
281
  return getProjectTypeDirMap(platform).get(nextDir);
252
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
+ }
253
318
 
254
319
  //#endregion
255
320
  //#region ../core/src/utils/encoding.ts
@@ -3757,7 +3822,7 @@ function normalizeBundleBase(base) {
3757
3822
  }
3758
3823
  function getBundlePath(base, slug, platform, version$2) {
3759
3824
  const prefix = base ? `${base}/` : "";
3760
- return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${platform}/${version$2}`;
3825
+ return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${version$2}/${platform}.json`;
3761
3826
  }
3762
3827
  function ensureKnownPlatform(platform, slug) {
3763
3828
  if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
@@ -5534,7 +5599,7 @@ function resolveInstallTarget(platform, type, name, options) {
5534
5599
  mode: "global",
5535
5600
  platform,
5536
5601
  platformDir,
5537
- globalDir,
5602
+ globalDir: globalRoot,
5538
5603
  type,
5539
5604
  name,
5540
5605
  label: `global path ${globalRoot}`
@@ -5558,17 +5623,20 @@ function resolveInstallTarget(platform, type, name, options) {
5558
5623
  *
5559
5624
  * - Project scope: use file.path directly
5560
5625
  * - Global scope:
5561
- * - If path starts with platformDir: transform to globalDir
5562
- * - If type is "multi" and path doesn't start with platformDir: skip (return null)
5563
- * - Otherwise: use getInstallPath for root-level files (e.g., instruction)
5626
+ * - If path starts with platformDir: strip prefix (target.root is already globalDir)
5627
+ * - If typed bundle and root file: use type template to get relative path
5628
+ * - If freeform bundle and root file: use path as-is (relative to globalDir)
5629
+ *
5630
+ * IMPORTANT: For global scope, this returns RELATIVE paths only.
5631
+ * The caller combines with target.root (which is the expanded globalDir).
5564
5632
  */
5565
5633
  function resolvePath(filePath, target) {
5566
- const { platform, platformDir, globalDir, type, name, mode } = target;
5634
+ const { platform, platformDir, type, name, mode } = target;
5567
5635
  if (mode === "project") return filePath;
5568
5636
  const platformDirPrefix = `${platformDir}/`;
5569
- if (filePath.startsWith(platformDirPrefix)) return `${globalDir}/${filePath.slice(platformDirPrefix.length)}`;
5570
- if (!type) return null;
5571
- return getInstallPath({
5637
+ if (filePath.startsWith(platformDirPrefix)) return filePath.slice(platformDirPrefix.length);
5638
+ if (!type) return filePath;
5639
+ return getRelativeInstallPath({
5572
5640
  platform,
5573
5641
  type,
5574
5642
  name,
@@ -5578,12 +5646,22 @@ function resolvePath(filePath, target) {
5578
5646
  function computeDestinationPath(pathInput, target) {
5579
5647
  const normalized = normalizeBundlePath(pathInput);
5580
5648
  if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
5649
+ validateBundlePath(normalized, pathInput);
5581
5650
  const resolvedPath = resolvePath(normalized, target);
5582
5651
  if (resolvedPath === null) return null;
5583
5652
  const destination = resolve(target.root, resolvedPath);
5584
5653
  ensureWithinRoot(destination, target.root);
5585
5654
  return destination;
5586
5655
  }
5656
+ /**
5657
+ * Validate bundle path for dangerous patterns.
5658
+ * Throws if path contains traversal or home directory references.
5659
+ */
5660
+ function validateBundlePath(normalized, original) {
5661
+ if (normalized.includes("..")) throw new Error(`Refusing to install file with path traversal: ${original}`);
5662
+ if (normalized.startsWith("~")) throw new Error(`Refusing to install file with home directory reference: ${original}`);
5663
+ if (normalized.includes("/~/") || normalized.includes("\\~\\")) throw new Error(`Refusing to install file with embedded home directory reference: ${original}`);
5664
+ }
5587
5665
  function parseInput(input, explicitPlatform, explicitVersion) {
5588
5666
  let normalized = input.toLowerCase().trim();
5589
5667
  let parsedVersion;
@@ -5806,9 +5884,27 @@ async function directoryExists(path$1) {
5806
5884
 
5807
5885
  //#endregion
5808
5886
  //#region src/lib/rule-utils.ts
5809
- const INSTALL_FILENAME = "INSTALL.txt";
5810
- const README_FILENAME = "README.md";
5811
- 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
+ };
5812
5908
  /**
5813
5909
  * Files/directories that are always excluded from rules.
5814
5910
  * These are never useful in a rule bundle.
@@ -5942,9 +6038,9 @@ async function loadRule(ruleDir, overrides) {
5942
6038
  }
5943
6039
  async function collectMetadata(loaded) {
5944
6040
  const { configDir } = loaded;
5945
- const installMessage = await readFileIfExists(join(configDir, INSTALL_FILENAME));
5946
- const readmeContent = await readFileIfExists(join(configDir, README_FILENAME));
5947
- 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);
5948
6044
  return {
5949
6045
  installMessage,
5950
6046
  readmeContent,
@@ -5962,6 +6058,34 @@ async function collectPlatformFiles(loaded) {
5962
6058
  const resolvedSourcePath = sourcePath ?? ".";
5963
6059
  const filesDir = join(configDir, resolvedSourcePath);
5964
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
+ }
5965
6089
  const treatInstructionAsRoot = config$1.type === void 0 || config$1.type === "instruction";
5966
6090
  const instructionProjectPath = treatInstructionAsRoot ? getInstallPath({
5967
6091
  platform,
@@ -5969,10 +6093,6 @@ async function collectPlatformFiles(loaded) {
5969
6093
  scope: "project"
5970
6094
  }) : null;
5971
6095
  const instructionContent = instructionProjectPath ? await readFileIfExists(join(configDir, instructionProjectPath)) : void 0;
5972
- const filesDirExists = await directoryExists(filesDir);
5973
- const rootExclude = [RULE_CONFIG_FILENAME];
5974
- if (filesDir === configDir) rootExclude.push(README_FILENAME, LICENSE_FILENAME, INSTALL_FILENAME);
5975
- const collectedFiles = filesDirExists ? await collectFiles(filesDir, rootExclude, ignorePatterns) : [];
5976
6096
  const publishFiles = [];
5977
6097
  const seenPublishPaths = new Set();
5978
6098
  for (const file of collectedFiles) {
@@ -5992,10 +6112,7 @@ async function collectPlatformFiles(loaded) {
5992
6112
  content: instructionContent
5993
6113
  });
5994
6114
  }
5995
- if (publishFiles.length === 0) {
5996
- if (!filesDirExists) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in the platform entry.`);
5997
- throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
5998
- }
6115
+ if (publishFiles.length === 0) throw new Error(`No files found in ${filesDir}. Rules must include at least one file.`);
5999
6116
  platformFiles.push({
6000
6117
  platform,
6001
6118
  files: publishFiles
@@ -6064,6 +6181,16 @@ async function readFileIfExists(path$1) {
6064
6181
  if (await fileExists(path$1)) return await readFile(path$1, "utf8");
6065
6182
  return;
6066
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
+ }
6067
6194
 
6068
6195
  //#endregion
6069
6196
  //#region src/lib/zod-validator.ts
@@ -6111,11 +6238,12 @@ async function publish(options = {}) {
6111
6238
  }
6112
6239
  if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
6113
6240
  const filePath = await getSingleFilePath(path$1);
6241
+ const quickDir = await getQuickPublishDirectory(path$1);
6114
6242
  let publishInput;
6115
6243
  if (filePath) {
6116
6244
  let resolved;
6117
6245
  try {
6118
- resolved = await resolveSingleFileInputs({
6246
+ resolved = await resolveQuickPublishInputs({
6119
6247
  name,
6120
6248
  platform,
6121
6249
  type,
@@ -6123,7 +6251,10 @@ async function publish(options = {}) {
6123
6251
  description,
6124
6252
  tags,
6125
6253
  license
6126
- }, filePath, {
6254
+ }, {
6255
+ type: "file",
6256
+ path: filePath
6257
+ }, {
6127
6258
  dryRun,
6128
6259
  yes
6129
6260
  });
@@ -6138,7 +6269,7 @@ async function publish(options = {}) {
6138
6269
  const fileSpinner = await log.spinner("Building bundle...");
6139
6270
  try {
6140
6271
  publishInput = await buildPublishInput({
6141
- rule: await buildRuleInputFromSingleFile(resolved),
6272
+ rule: await buildRuleInputFromQuickPublish(resolved),
6142
6273
  version: version$2
6143
6274
  });
6144
6275
  log.debug(`Built publish input for platforms: ${publishInput.variants.map((v) => v.platform).join(", ")}`);
@@ -6159,6 +6290,57 @@ async function publish(options = {}) {
6159
6290
  ctx
6160
6291
  });
6161
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
+ }
6162
6344
  const spinner$1 = await log.spinner("Validating rule...");
6163
6345
  const configPath = await resolveConfigPath(path$1);
6164
6346
  log.debug(`Resolved config path: ${configPath}`);
@@ -6247,19 +6429,34 @@ async function getSingleFilePath(inputPath) {
6247
6429
  if (basename(inputPath) === RULE_CONFIG_FILENAME) return;
6248
6430
  return inputPath;
6249
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
+ }
6250
6447
  function normalizePathForInference(value) {
6251
6448
  return value.replace(/\\/g, "/");
6252
6449
  }
6253
6450
  function stripExtension(value) {
6254
6451
  return value.replace(/\.[^/.]+$/, "");
6255
6452
  }
6256
- function inferSingleFileDefaults(filePath) {
6453
+ function inferFileDefaults(filePath) {
6257
6454
  const normalized = normalizePathForInference(filePath);
6258
6455
  const segments = normalized.split("/").filter(Boolean);
6259
6456
  const fileName = segments.at(-1) ?? "";
6260
6457
  const instructionPlatforms = inferInstructionPlatformsFromFileName(fileName);
6261
6458
  if (instructionPlatforms.length > 0) return {
6262
- type: "instruction",
6459
+ ruleType: "instruction",
6263
6460
  ...instructionPlatforms.length === 1 ? { platform: instructionPlatforms[0] } : {}
6264
6461
  };
6265
6462
  const platform = inferPlatformFromPath(filePath);
@@ -6267,11 +6464,22 @@ function inferSingleFileDefaults(filePath) {
6267
6464
  const inferredType = inferTypeFromPath(platform, filePath);
6268
6465
  const result = { platform };
6269
6466
  if (inferredType) {
6270
- result.type = inferredType;
6467
+ result.ruleType = inferredType;
6271
6468
  if (inferredType !== "instruction") result.name = normalizeName(stripExtension(fileName));
6272
6469
  }
6273
6470
  return result;
6274
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
+ }
6275
6483
  function buildConfigPublishOverrides(options) {
6276
6484
  const overrides = {};
6277
6485
  if (options.name !== void 0) overrides.name = options.name;
@@ -6283,13 +6491,17 @@ function buildConfigPublishOverrides(options) {
6283
6491
  if (options.tags !== void 0) overrides.tags = options.tags;
6284
6492
  return Object.keys(overrides).length > 0 ? overrides : void 0;
6285
6493
  }
6286
- async function resolveSingleFileInputs(options, filePath, ctx) {
6287
- 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");
6288
6496
  const isInteractive = !ctx.yes && process.stdin.isTTY;
6289
- const hasRequiredArgs = options.name !== void 0 && options.platform !== void 0 && options.type !== void 0;
6290
- 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
+ }
6291
6503
  const selectedPlatforms = options.platform ? parsePlatformSelection(options.platform) : void 0;
6292
- 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.");
6293
6505
  let selectedPlatform = selectedPlatforms?.[0] ? normalizePlatformInput(selectedPlatforms[0]) : inferred.platform;
6294
6506
  if (!selectedPlatform) {
6295
6507
  if (!isInteractive) throw new Error("Missing --platform");
@@ -6303,7 +6515,7 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6303
6515
  if (p$1.isCancel(selection)) throw new Error("Cancelled");
6304
6516
  selectedPlatform = selection;
6305
6517
  }
6306
- let selectedType = options.type ?? inferred.type;
6518
+ let selectedType = options.type ?? inferred.ruleType;
6307
6519
  if (!selectedType) {
6308
6520
  if (!isInteractive) throw new Error("Missing --type");
6309
6521
  const candidates = getValidTypes(selectedPlatform).filter((t) => supportsInstallPath({
@@ -6322,8 +6534,8 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6322
6534
  selectedType = selection;
6323
6535
  }
6324
6536
  const platform = selectedPlatform;
6325
- const type = selectedType;
6326
- const nameValue = options.name ?? await (async () => {
6537
+ const ruleType = selectedType;
6538
+ const nameValue = options.name ?? inferred.name ?? await (async () => {
6327
6539
  if (!isInteractive) throw new Error("Missing --name");
6328
6540
  const input = await p$1.text({
6329
6541
  message: "Rule name",
@@ -6336,13 +6548,6 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6336
6548
  const normalizedName = normalizeName(nameValue);
6337
6549
  const nameCheck = nameSchema.safeParse(normalizedName);
6338
6550
  if (!nameCheck.success) throw new Error(nameCheck.error.issues[0]?.message ?? "Invalid name");
6339
- const bundlePath = getInstallPath({
6340
- platform,
6341
- type,
6342
- name: normalizedName,
6343
- scope: "project"
6344
- });
6345
- if (!bundlePath) throw new Error(`Type "${type}" is not supported for project installs on platform "${platform}".`);
6346
6551
  const defaultTitle = toTitleCase(normalizedName);
6347
6552
  const finalTitle = options.title ?? await (async () => {
6348
6553
  if (!isInteractive) return defaultTitle;
@@ -6375,59 +6580,80 @@ async function resolveSingleFileInputs(options, filePath, ctx) {
6375
6580
  if (p$1.isCancel(input)) throw new Error("Cancelled");
6376
6581
  return tagsInputSchema.parse(input);
6377
6582
  })();
6378
- const finalLicense = options.license ?? "MIT";
6583
+ const finalLicense = options.license ?? inferred.license ?? "MIT";
6379
6584
  if (isInteractive && !ctx.dryRun) {
6380
6585
  log.print("");
6381
6586
  log.print(ui.header("Quick publish"));
6382
- log.print(ui.keyValue("File", ui.path(filePath)));
6587
+ log.print(ui.keyValue(isDirectory ? "Directory" : "File", ui.path(source.path)));
6383
6588
  log.print(ui.keyValue("Name", ui.code(normalizedName)));
6384
6589
  log.print(ui.keyValue("Title", finalTitle));
6385
6590
  log.print(ui.keyValue("Description", finalDescription));
6386
6591
  log.print(ui.keyValue("Platform", platform));
6387
- log.print(ui.keyValue("Type", type));
6388
- log.print(ui.keyValue("Installs to", ui.path(bundlePath)));
6592
+ log.print(ui.keyValue("Type", ruleType));
6389
6593
  if (finalTags.length > 0) log.print(ui.keyValue("Tags", finalTags.join(", ")));
6390
6594
  log.print("");
6391
6595
  const confirm = await p$1.confirm({
6392
- message: "Publish this file?",
6596
+ message: isDirectory ? "Publish this directory?" : "Publish this file?",
6393
6597
  initialValue: true
6394
6598
  });
6395
6599
  if (p$1.isCancel(confirm) || !confirm) throw new Error("Cancelled");
6396
6600
  }
6397
6601
  return {
6398
- filePath,
6602
+ source,
6399
6603
  name: normalizedName,
6400
6604
  platform,
6401
- type,
6605
+ ruleType,
6402
6606
  title: finalTitle,
6403
6607
  description: finalDescription,
6404
6608
  tags: finalTags,
6405
- license: finalLicense,
6406
- bundlePath
6609
+ license: finalLicense
6407
6610
  };
6408
6611
  }
6409
- async function buildRuleInputFromSingleFile(inputs) {
6410
- const content = await readFile(inputs.filePath);
6612
+ async function buildRuleInputFromQuickPublish(inputs) {
6411
6613
  const config$1 = {
6412
6614
  $schema: RULE_SCHEMA_URL,
6413
6615
  name: inputs.name,
6414
- type: inputs.type,
6616
+ type: inputs.ruleType,
6415
6617
  title: inputs.title,
6416
6618
  description: inputs.description,
6417
6619
  license: inputs.license,
6418
6620
  tags: inputs.tags,
6419
6621
  platforms: [{ platform: inputs.platform }]
6420
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);
6421
6653
  return {
6422
6654
  name: inputs.name,
6423
6655
  config: config$1,
6424
- platformFiles: [{
6425
- platform: inputs.platform,
6426
- files: [{
6427
- path: inputs.bundlePath,
6428
- content
6429
- }]
6430
- }]
6656
+ platformFiles
6431
6657
  };
6432
6658
  }
6433
6659
  async function finalizePublish(options) {
@@ -6563,11 +6789,13 @@ async function buildRegistry(options) {
6563
6789
  await mkdir(outputDir, { recursive: true });
6564
6790
  const indent$1 = compact ? void 0 : 2;
6565
6791
  for (const bundle of result.bundles) {
6566
- const bundleDir = join(outputDir, STATIC_BUNDLE_DIR, bundle.slug, bundle.platform);
6567
- await mkdir(bundleDir, { recursive: true });
6568
6792
  const bundleJson = JSON.stringify(bundle, null, indent$1);
6569
- await writeFile(join(bundleDir, bundle.version), bundleJson);
6570
- await writeFile(join(bundleDir, LATEST_VERSION), bundleJson);
6793
+ const versionedDir = join(outputDir, STATIC_BUNDLE_DIR, bundle.slug, bundle.version);
6794
+ await mkdir(versionedDir, { recursive: true });
6795
+ await writeFile(join(versionedDir, `${bundle.platform}.json`), bundleJson);
6796
+ const latestDir = join(outputDir, STATIC_BUNDLE_DIR, bundle.slug, LATEST_VERSION);
6797
+ await mkdir(latestDir, { recursive: true });
6798
+ await writeFile(join(latestDir, `${bundle.platform}.json`), bundleJson);
6571
6799
  }
6572
6800
  for (const rule of result.rules) {
6573
6801
  const ruleJson = JSON.stringify(rule, null, indent$1);
@@ -6610,6 +6838,19 @@ async function discoverRuleDirs(inputDir) {
6610
6838
  /** Default rule name when none specified */
6611
6839
  const DEFAULT_RULE_NAME$1 = "my-rule";
6612
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
+ /**
6613
6854
  * Initialize a rule in a directory (rule root).
6614
6855
  *
6615
6856
  * Structure:
@@ -6620,14 +6861,17 @@ const DEFAULT_RULE_NAME$1 = "my-rule";
6620
6861
  async function initRule(options) {
6621
6862
  const ruleDir = options.directory ?? process.cwd();
6622
6863
  log.debug(`Initializing rule in: ${ruleDir}`);
6864
+ const skillInfo = await detectSkillDirectory(ruleDir);
6865
+ const isSkillDirectory = skillInfo !== void 0;
6623
6866
  const inferredPlatform = getPlatformFromDir(basename(ruleDir));
6624
6867
  const platformInputs = options.platforms ?? (inferredPlatform ? [inferredPlatform] : []);
6625
6868
  if (platformInputs.length === 0) throw new Error(`Cannot determine platform. Specify --platform (${PLATFORM_IDS.join(", ")}).`);
6626
6869
  const platforms = platformInputs.map(normalizePlatformEntryInput);
6627
- const name = normalizeName(options.name ?? DEFAULT_RULE_NAME$1);
6870
+ const name = normalizeName(options.name ?? skillInfo?.name ?? DEFAULT_RULE_NAME$1);
6628
6871
  const title = options.title ?? toTitleCase(name);
6629
6872
  const description = options.description ?? "";
6630
- const license = options.license ?? "MIT";
6873
+ const license = options.license ?? skillInfo?.license ?? "MIT";
6874
+ const ruleType = isSkillDirectory ? "skill" : options.type;
6631
6875
  const platformLabels = platforms.map((p$2) => typeof p$2 === "string" ? p$2 : p$2.platform).join(", ");
6632
6876
  log.debug(`Rule name: ${name}, platforms: ${platformLabels}`);
6633
6877
  const configPath = join(ruleDir, RULE_CONFIG_FILENAME);
@@ -6635,7 +6879,7 @@ async function initRule(options) {
6635
6879
  const config$1 = {
6636
6880
  $schema: RULE_SCHEMA_URL,
6637
6881
  name,
6638
- ...options.type && { type: options.type },
6882
+ ...ruleType && { type: ruleType },
6639
6883
  title,
6640
6884
  version: 1,
6641
6885
  description,
@@ -6701,8 +6945,22 @@ function checkTags(value) {
6701
6945
  async function initInteractive(options) {
6702
6946
  const { directory, name: nameOption, title: titleOption, description: descriptionOption, platforms: platformsOption, platformPaths, license: licenseOption } = options;
6703
6947
  let { force } = options;
6704
- const defaultName = nameOption ?? DEFAULT_RULE_NAME;
6705
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";
6706
6964
  const validatedPlatforms = [];
6707
6965
  if (platformsOption) for (const platform of platformsOption) {
6708
6966
  if (!isSupportedPlatform(platform)) {
@@ -6728,6 +6986,7 @@ async function initInteractive(options) {
6728
6986
  })();
6729
6987
  const platformEntries = await (async () => {
6730
6988
  if (selectedPlatforms.length === 0) return [];
6989
+ if (useSkillDefaults) return selectedPlatforms;
6731
6990
  const hasCompletePathMapping = selectedPlatforms.every((platform) => {
6732
6991
  const value = platformPaths?.[platform];
6733
6992
  return typeof value === "string" && value.trim().length > 0;
@@ -6777,12 +7036,18 @@ async function initInteractive(options) {
6777
7036
  force = true;
6778
7037
  }
6779
7038
  const result = await p.group({
6780
- name: () => p.text({
6781
- message: "Rule name",
6782
- placeholder: normalizeName(defaultName),
6783
- defaultValue: normalizeName(defaultName),
6784
- validate: check(nameSchema)
6785
- }),
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
+ },
6786
7051
  title: ({ results }) => {
6787
7052
  const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
6788
7053
  return p.text({
@@ -6803,7 +7068,6 @@ async function initInteractive(options) {
6803
7068
  validate: checkTags
6804
7069
  }),
6805
7070
  license: async () => {
6806
- const defaultLicense = licenseOption ?? "MIT";
6807
7071
  const choice = await p.select({
6808
7072
  message: "License",
6809
7073
  options: [...COMMON_LICENSES.map((id) => ({
@@ -6840,6 +7104,7 @@ async function initInteractive(options) {
6840
7104
  const initOptions = {
6841
7105
  directory,
6842
7106
  name: result.name,
7107
+ type: useSkillDefaults ? "skill" : void 0,
6843
7108
  title: result.title.trim() || void 0,
6844
7109
  description: result.description,
6845
7110
  tags: parseTags(result.tags),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.2.0",
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.0",
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",