@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.
- package/README.md +1 -1
- package/dist/index.js +343 -78
- 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}/${
|
|
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:
|
|
5562
|
-
* - If
|
|
5563
|
-
* -
|
|
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,
|
|
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
|
|
5570
|
-
if (!type) return
|
|
5571
|
-
return
|
|
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
|
|
5810
|
-
|
|
5811
|
-
|
|
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
|
|
5946
|
-
const readmeContent = await
|
|
5947
|
-
const licenseContent = await
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
6287
|
-
const inferred =
|
|
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
|
|
6290
|
-
|
|
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("
|
|
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.
|
|
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
|
|
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(
|
|
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",
|
|
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
|
-
|
|
6602
|
+
source,
|
|
6399
6603
|
name: normalizedName,
|
|
6400
6604
|
platform,
|
|
6401
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
6570
|
-
await
|
|
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
|
-
...
|
|
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: () =>
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
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.
|
|
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.
|
|
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",
|