Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ npx heymark link <GitHub-Repository-URL> --branch <branch-name>

npx heymark sync . # sync all tools
npx heymark sync cursor claude-code # sync selected tools
npx heymark sync . --dry-run # preview sync without writing files

npx heymark clean . # clean all generated outputs
npx heymark clean cursor claude-code # clean selected tool outputs
npx heymark clean . --dry-run # preview clean without removing files

npx heymark validate # validate skill frontmatter and naming

npx heymark help
```
Expand Down
27 changes: 24 additions & 3 deletions src/commands/clean/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,35 @@ const { cleaner } = require("@/commands/cleaner");
const { selectTools } = require("@/commands/select-tools");
const { readCache } = require("@/skill-repo/cache-folder");

function extractDryRun(flags) {
const dryRunFlags = new Set(["--dry-run", "-n"]);
const dryRun = flags.some((flag) => dryRunFlags.has(flag));
const positional = flags.filter((flag) => !dryRunFlags.has(flag));
return { dryRun, positional };
}

function runClean(flags, context) {
const selectedTools = selectTools(flags, context.tools);
const { skills } = readCache(context.cwd);
const { dryRun, positional } = extractDryRun(flags);
const selectedTools = selectTools(positional, context.tools);
const { skills } = readCache(context.cwd, { update: false });

const previousDryRun = process.env.HEYMARK_DRY_RUN;
if (dryRun) {
process.env.HEYMARK_DRY_RUN = "1";
console.log("[Clean]");
console.log(" mode: dry-run (no files will be removed)");
console.log("");
}

const skillNames = skills.map((s) => s.name);
const cleanedCount = cleaner(context.tools, selectedTools, skillNames, context.cwd);

console.log(`[Done] ${cleanedCount} tools cleaned.`);
if (dryRun) {
if (previousDryRun === undefined) delete process.env.HEYMARK_DRY_RUN;
else process.env.HEYMARK_DRY_RUN = previousDryRun;
}

console.log(`[Done] ${cleanedCount} tools cleaned${dryRun ? " (dry-run)" : ""}.`);
}

module.exports = {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const COMMAND_LINK = "link";
const COMMAND_SYNC = "sync";
const COMMAND_CLEAN = "clean";
const COMMAND_HELP = "help";
const COMMAND_VALIDATE = "validate";

const LATEST_VERSION_COMMAND = "npx heymark@latest";

Expand All @@ -10,5 +11,6 @@ module.exports = {
COMMAND_SYNC,
COMMAND_CLEAN,
COMMAND_HELP,
COMMAND_VALIDATE,
LATEST_VERSION_COMMAND,
};
6 changes: 6 additions & 0 deletions src/commands/help/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ Usage:
heymark link <repo-url>
heymark sync .
heymark sync <tool1> <tool2> ...
heymark sync . --dry-run
heymark clean .
heymark clean <tool1> <tool2> ...
heymark clean . --dry-run
heymark validate

Link flags:
--branch | -b
--folder | -f

Dry-run (sync, clean):
--dry-run | -n

Supported tools:
${toolLines}

Expand Down
26 changes: 24 additions & 2 deletions src/commands/sync/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@ const { readCache } = require("@/skill-repo/cache-folder");
const { readConfig } = require("@/skill-repo/config-file");
const { SKILL_REPO_DEFAULT_BRANCH } = require("@/skill-repo/constants");

function extractDryRun(flags) {
const dryRunFlags = new Set(["--dry-run", "-n"]);
const dryRun = flags.some((flag) => dryRunFlags.has(flag));
const positional = flags.filter((flag) => !dryRunFlags.has(flag));
return { dryRun, positional };
}

function runSync(flags, context) {
const selectedTools = selectTools(flags, context.tools);
const { dryRun, positional } = extractDryRun(flags);
const selectedTools = selectTools(positional, context.tools);
const { skills } = readCache(context.cwd);
const config = readConfig(context.cwd);

const previousDryRun = process.env.HEYMARK_DRY_RUN;
if (dryRun) {
process.env.HEYMARK_DRY_RUN = "1";
}

console.log("[Sync]");
if (config) {
console.log(` repo: ${config.repoUrl}`);
Expand All @@ -17,6 +30,10 @@ function runSync(flags, context) {
}
console.log("");

if (dryRun) {
console.log(" mode: dry-run (no files will be written or removed)");
}

const skillNames = skills.map((s) => s.name);
cleaner(context.tools, selectedTools, skillNames, context.cwd);

Expand All @@ -26,8 +43,13 @@ function runSync(flags, context) {
console.log(` ${tool.name.padEnd(16)} -> ${tool.output} (${count} skills)`);
}

if (dryRun) {
if (previousDryRun === undefined) delete process.env.HEYMARK_DRY_RUN;
else process.env.HEYMARK_DRY_RUN = previousDryRun;
}

console.log("");
console.log(`[Done] ${selectedTools.length} tools synced.`);
console.log(`[Done] ${selectedTools.length} tools synced${dryRun ? " (dry-run)" : ""}.`);
}

module.exports = {
Expand Down
57 changes: 57 additions & 0 deletions src/commands/validate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { readCache } = require("@/skill-repo/cache-folder");

function runValidate(flags, context) {
if (flags.length > 0) {
console.error(`[Error] Unknown: ${flags.join(", ")}. validate takes no arguments.`);
process.exit(1);
}

const { skills } = readCache(context.cwd, { update: false });
const errors = [];
const seenNames = new Set();

for (const skill of skills) {
const label = `${skill.fileName}`;

if (!skill.metadata || typeof skill.metadata.description !== "string" || !skill.metadata.description.trim()) {
errors.push(`[${label}] Missing required frontmatter key: description`);
}

if (
skill.metadata
&& Object.prototype.hasOwnProperty.call(skill.metadata, "alwaysApply")
&& typeof skill.metadata.alwaysApply !== "boolean"
) {
errors.push(`[${label}] alwaysApply must be boolean (true/false)`);
}

if (
skill.metadata
&& Object.prototype.hasOwnProperty.call(skill.metadata, "globs")
&& typeof skill.metadata.globs !== "string"
) {
errors.push(`[${label}] globs must be a string`);
}

const normalizedName = (skill.name || "").trim().toLowerCase();
if (!normalizedName) {
errors.push(`[${label}] Skill name is empty`);
} else if (seenNames.has(normalizedName)) {
errors.push(`[${label}] Duplicate skill name detected: ${skill.name}`);
} else {
seenNames.add(normalizedName);
}
}

if (errors.length > 0) {
console.error("[Validate] Failed");
errors.forEach((error) => console.error(` - ${error}`));
process.exit(1);
}

console.log(`[Validate] OK (${skills.length} skills)`);
}

module.exports = {
runValidate,
};
8 changes: 7 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

require("./alias.js");

const { COMMAND_LINK, COMMAND_SYNC, COMMAND_CLEAN, COMMAND_HELP } = require("@/commands/constants");
const { COMMAND_LINK, COMMAND_SYNC, COMMAND_CLEAN, COMMAND_HELP, COMMAND_VALIDATE } = require("@/commands/constants");
const { runLink } = require("@/commands/link");
const { runSync } = require("@/commands/sync");
const { runClean } = require("@/commands/clean");
const { runHelp } = require("@/commands/help");
const { runValidate } = require("@/commands/validate");
const { loadTools } = require("@/tools/loader");

function main() {
Expand Down Expand Up @@ -45,6 +46,11 @@ function main() {
return;
}

if (command === COMMAND_VALIDATE) {
runValidate(flags, context);
return;
}

console.error(`[Error] Unknown command: ${command}. Run: heymark help`);
process.exit(1);
}
Expand Down
24 changes: 22 additions & 2 deletions src/skill-repo/cache-folder.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,28 @@ function writeCache(cwd) {
return { config, cloneFolderPath };
}

function readCache(cwd) {
const { config, cloneFolderPath } = writeCache(cwd);
function readCache(cwd, options = {}) {
const update = options.update !== false;
let config, cloneFolderPath;

if (update) {
const result = writeCache(cwd);
config = result.config;
cloneFolderPath = result.cloneFolderPath;
} else {
config = readConfig(cwd);
if (!config) {
console.error(
`[Error] Not linked. Run: heymark link <repo-url> (config: ${HEYMARK.DIR}/${HEYMARK.CONFIG_FILE})`
);
process.exit(1);
}
cloneFolderPath = getCloneFolderPath(cwd, config.repoUrl);
if (!fs.existsSync(cloneFolderPath) || !fs.statSync(cloneFolderPath).isDirectory()) {
console.error("[Error] Cache not found. Run: heymark sync");
process.exit(1);
}
}

const folder = config.folder || "";
const skillsFolderPath = folder ? path.join(cloneFolderPath, folder) : cloneFolderPath;
Expand Down
17 changes: 14 additions & 3 deletions src/tools/skill-per-file.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
const fs = require("fs");
const path = require("path");

function isDryRun() {
return process.env.HEYMARK_DRY_RUN === "1";
}

function generate({ cwd, dir, skills, getFileName, createContent }) {
const destDir = path.join(cwd, dir);
fs.mkdirSync(destDir, { recursive: true });

if (!isDryRun()) {
fs.mkdirSync(destDir, { recursive: true });
}

for (const skill of skills) {
const filePath = path.join(destDir, getFileName(skill));
fs.writeFileSync(filePath, createContent(skill), "utf8");
if (!isDryRun()) {
fs.writeFileSync(filePath, createContent(skill), "utf8");
}
}

return skills.length;
Expand All @@ -19,7 +28,9 @@ function clean(cwd, dir) {
return [];
}

fs.rmSync(targetPath, { recursive: true, force: true });
if (!isDryRun()) {
fs.rmSync(targetPath, { recursive: true, force: true });
}
return [dir];
}

Expand Down
10 changes: 8 additions & 2 deletions src/tools/skill-per-folder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ const fs = require("fs");
const path = require("path");
const { clean } = require("@/tools/skill-per-file");

function isDryRun() {
return process.env.HEYMARK_DRY_RUN === "1";
}

function generate({ cwd, dir, fileName, skills, createContent }) {
for (const skill of skills) {
const skillDir = path.join(cwd, dir, skill.name);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, fileName), createContent(skill), "utf8");
if (!isDryRun()) {
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, fileName), createContent(skill), "utf8");
}
}

return skills.length;
Expand Down