heymark 2.0.0 → 2.1.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 (38) hide show
  1. package/LICENSE +21 -21
  2. package/README.ko.md +121 -123
  3. package/README.md +123 -125
  4. package/package.json +57 -56
  5. package/src/alias.js +41 -0
  6. package/src/commands/clean/index.js +17 -0
  7. package/src/commands/cleaner.js +27 -0
  8. package/src/commands/constants.js +14 -0
  9. package/src/commands/help/index.js +34 -0
  10. package/src/commands/link/flags/branch.js +18 -0
  11. package/src/commands/link/flags/folder.js +18 -0
  12. package/src/commands/link/index.js +58 -0
  13. package/src/commands/select-tools.js +35 -0
  14. package/src/commands/sync/index.js +35 -0
  15. package/src/index.js +52 -0
  16. package/src/skill-repo/cache-folder.js +77 -0
  17. package/src/skill-repo/config-file.js +87 -0
  18. package/src/skill-repo/constants.js +14 -0
  19. package/src/skill-repo/skill-file-parser.js +79 -0
  20. package/src/tools/antigravity/index.js +33 -0
  21. package/src/tools/claude-code/index.js +33 -0
  22. package/src/tools/codex/index.js +33 -0
  23. package/src/tools/constants.js +52 -0
  24. package/src/tools/copilot/index.js +40 -0
  25. package/src/tools/cursor/index.js +39 -0
  26. package/src/tools/loader.js +17 -0
  27. package/src/tools/openclaw/index.js +72 -0
  28. package/src/tools/skill-per-file.js +29 -0
  29. package/src/tools/skill-per-folder.js +18 -0
  30. package/scripts/cli.js +0 -375
  31. package/scripts/lib/config.js +0 -126
  32. package/scripts/lib/parser.js +0 -125
  33. package/scripts/lib/repo.js +0 -97
  34. package/scripts/tools/antigravity.js +0 -49
  35. package/scripts/tools/claude.js +0 -49
  36. package/scripts/tools/codex.js +0 -49
  37. package/scripts/tools/copilot.js +0 -61
  38. package/scripts/tools/cursor.js +0 -48
@@ -1,126 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const CONFIG_DIR = ".heymark";
7
- const CONFIG_FILENAME = "config.json";
8
- const DEFAULT_BRANCH = "main";
9
- const CONFIG_RELATIVE = path.join(CONFIG_DIR, CONFIG_FILENAME);
10
-
11
- /**
12
- * @typedef {{ repoUrl: string, branch?: string, folder?: string }} THeymarkConfig
13
- */
14
-
15
- /**
16
- * Resolve config file path from project root.
17
- * @param {string} projectRoot
18
- * @returns {string}
19
- */
20
- function getConfigPath(projectRoot) {
21
- return path.join(projectRoot, CONFIG_DIR, CONFIG_FILENAME);
22
- }
23
-
24
- /**
25
- * Normalize and validate config payload.
26
- * @param {unknown} value
27
- * @returns {THeymarkConfig | null}
28
- */
29
- function normalizeConfig(value) {
30
- if (!value || typeof value !== "object") {
31
- return null;
32
- }
33
-
34
- const raw = /** @type {{
35
- * repoUrl?: unknown,
36
- * branch?: unknown,
37
- * folder?: unknown,
38
- * rulesSource?: unknown,
39
- * rulesSourceDir?: unknown
40
- * }} */ (value);
41
-
42
- const repoUrl =
43
- typeof raw.repoUrl === "string" && raw.repoUrl.trim()
44
- ? raw.repoUrl.trim()
45
- : typeof raw.rulesSource === "string" && raw.rulesSource.trim()
46
- ? raw.rulesSource.trim()
47
- : "";
48
-
49
- if (!repoUrl) {
50
- return null;
51
- }
52
-
53
- return {
54
- repoUrl,
55
- branch:
56
- typeof raw.branch === "string" && raw.branch.trim()
57
- ? raw.branch.trim()
58
- : DEFAULT_BRANCH,
59
- folder:
60
- typeof raw.folder === "string"
61
- ? raw.folder.trim()
62
- : typeof raw.rulesSourceDir === "string"
63
- ? raw.rulesSourceDir.trim()
64
- : "",
65
- };
66
- }
67
-
68
- /**
69
- * Read config from project root.
70
- * @param {string} projectRoot
71
- * @returns {THeymarkConfig | null}
72
- */
73
- function loadConfig(projectRoot) {
74
- const configPath = getConfigPath(projectRoot);
75
- if (!fs.existsSync(configPath)) {
76
- return null;
77
- }
78
-
79
- try {
80
- const raw = fs.readFileSync(configPath, "utf8");
81
- return normalizeConfig(JSON.parse(raw));
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- /**
88
- * Create initial config file in .heymark/config.json.
89
- * @param {string} projectRoot
90
- * @param {THeymarkConfig} config
91
- * @returns {string}
92
- */
93
- function writeConfig(projectRoot, config) {
94
- const configDir = path.join(projectRoot, CONFIG_DIR);
95
- if (!fs.existsSync(configDir)) {
96
- fs.mkdirSync(configDir, { recursive: true });
97
- }
98
-
99
- const normalized = normalizeConfig(config);
100
- if (!normalized) {
101
- throw new Error("Invalid config payload");
102
- }
103
-
104
- const configPath = path.join(configDir, CONFIG_FILENAME);
105
- const toWrite = {
106
- repoUrl: normalized.repoUrl,
107
- branch: normalized.branch || DEFAULT_BRANCH,
108
- };
109
- if (normalized.folder) {
110
- toWrite.folder = normalized.folder;
111
- }
112
-
113
- fs.writeFileSync(configPath, JSON.stringify(toWrite, null, 2), "utf8");
114
- return configPath;
115
- }
116
-
117
- module.exports = {
118
- CONFIG_DIR,
119
- CONFIG_FILENAME,
120
- CONFIG_RELATIVE,
121
- DEFAULT_BRANCH,
122
- getConfigPath,
123
- loadConfig,
124
- normalizeConfig,
125
- writeConfig,
126
- };
@@ -1,125 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const GENERATED_MARKER = "<!-- @generated by sync-rules - DO NOT EDIT -->";
7
- const FRONTMATTER_REGEX = /^---\r?\n([\s\S]+?)\r?\n---\r?\n?([\s\S]*)$/;
8
- const MARKDOWN_EXTENSION = ".md";
9
- const RULE_SEPARATOR = "\n\n---\n\n";
10
-
11
- /**
12
- * @param {string} rawValue
13
- * @returns {string | boolean}
14
- */
15
- function parseMetadataValue(rawValue) {
16
- let value = rawValue.trim();
17
- if (
18
- (value.startsWith('"') && value.endsWith('"')) ||
19
- (value.startsWith("'") && value.endsWith("'"))
20
- ) {
21
- value = value.slice(1, -1);
22
- }
23
-
24
- if (value === "true") {
25
- return true;
26
- }
27
- if (value === "false") {
28
- return false;
29
- }
30
-
31
- return value;
32
- }
33
-
34
- /**
35
- * @param {string} metadataBlock
36
- * @returns {Record<string, string | boolean>}
37
- */
38
- function parseMetadataBlock(metadataBlock) {
39
- const metadata = {};
40
-
41
- metadataBlock.split(/\r?\n/).forEach((line) => {
42
- const separatorIndex = line.indexOf(":");
43
- if (separatorIndex === -1) {
44
- return;
45
- }
46
-
47
- const key = line.slice(0, separatorIndex).trim();
48
- const rawValue = line.slice(separatorIndex + 1);
49
- metadata[key] = parseMetadataValue(rawValue);
50
- });
51
-
52
- return metadata;
53
- }
54
-
55
- function parseFrontmatter(content) {
56
- const match = content.match(FRONTMATTER_REGEX);
57
- if (!match) {
58
- return { metadata: {}, body: content.trim() };
59
- }
60
-
61
- const [, metadataBlock, body] = match;
62
- return {
63
- metadata: parseMetadataBlock(metadataBlock),
64
- body: body.trim(),
65
- };
66
- }
67
-
68
- function loadMarkdownFiles(rulesDir) {
69
- return fs
70
- .readdirSync(rulesDir)
71
- .filter((fileName) => fileName.endsWith(MARKDOWN_EXTENSION))
72
- .sort();
73
- }
74
-
75
- function createRuleFromFile(rulesDir, fileName) {
76
- const filePath = path.join(rulesDir, fileName);
77
- const raw = fs.readFileSync(filePath, "utf8");
78
- const { metadata, body } = parseFrontmatter(raw);
79
- const baseName = path.basename(fileName, MARKDOWN_EXTENSION);
80
-
81
- return {
82
- fileName,
83
- name: metadata.name || baseName,
84
- description: metadata.description || baseName,
85
- globs: metadata.globs || "",
86
- alwaysApply: metadata.alwaysApply === true,
87
- metadata,
88
- body,
89
- };
90
- }
91
-
92
- function loadRules(rulesDir) {
93
- if (!fs.existsSync(rulesDir)) {
94
- console.error(`[Error] Rules directory not found: ${rulesDir}`);
95
- console.error(" Ensure 'rules/' exists relative to the script location.");
96
- process.exit(1);
97
- }
98
-
99
- const files = loadMarkdownFiles(rulesDir);
100
-
101
- if (files.length === 0) {
102
- console.error(`[Error] No .md files found in: ${rulesDir}`);
103
- process.exit(1);
104
- }
105
-
106
- return files.map((fileName) => createRuleFromFile(rulesDir, fileName));
107
- }
108
-
109
- function mergeRuleBodies(rules) {
110
- return rules.map((rule) => rule.body).join(RULE_SEPARATOR);
111
- }
112
-
113
- function writeMergedFile(filePath, rules) {
114
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
- const content = `${GENERATED_MARKER}\n\n${mergeRuleBodies(rules)}\n`;
116
- fs.writeFileSync(filePath, content, "utf8");
117
- }
118
-
119
- module.exports = {
120
- GENERATED_MARKER,
121
- parseFrontmatter,
122
- loadRules,
123
- mergeRuleBodies,
124
- writeMergedFile,
125
- };
@@ -1,97 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { execSync } = require("child_process");
6
-
7
- const CACHE_DIR_NAME = path.join(".heymark", "cache");
8
- const DEFAULT_BRANCH = "main";
9
- const GITHUB_REPO_PATTERN = /github\.com[:/]([^/]+\/[^/]+?)(?:\/|$)/;
10
- const GENERIC_REPO_PATTERN = /([^/]+\/[^/]+?)(?:\/|$)/;
11
-
12
- /**
13
- * Convert repository URL to a stable cache directory name.
14
- * https://github.com/org/repo -> org-repo
15
- * git@github.com:org/repo.git -> org-repo
16
- * @param {string} repoUrl
17
- * @returns {string}
18
- */
19
- function sanitizeRepoName(repoUrl) {
20
- let normalized = repoUrl.trim();
21
- if (normalized.endsWith(".git")) {
22
- normalized = normalized.slice(0, -4);
23
- }
24
-
25
- const match = normalized.match(GITHUB_REPO_PATTERN) || normalized.match(GENERIC_REPO_PATTERN);
26
- if (match) {
27
- return match[1].replace(/\//g, "-");
28
- }
29
-
30
- return normalized.replace(/[^a-zA-Z0-9._-]/g, "-") || "repo";
31
- }
32
-
33
- function cloneLinkedRepo(projectRoot, clonePath, branch, repoUrl) {
34
- execSync(`git clone --depth 1 --branch "${branch}" "${repoUrl}" "${clonePath}"`, {
35
- stdio: "inherit",
36
- cwd: projectRoot,
37
- });
38
- }
39
-
40
- function updateLinkedRepo(clonePath, branch) {
41
- execSync(`git fetch origin && git checkout --quiet . && git pull --quiet origin "${branch}"`, {
42
- stdio: "pipe",
43
- cwd: clonePath,
44
- });
45
- }
46
-
47
- function resolveLinkedFolder(clonePath, folder) {
48
- const rulesDir = folder ? path.join(clonePath, folder) : clonePath;
49
- if (!fs.existsSync(rulesDir) || !fs.statSync(rulesDir).isDirectory()) {
50
- console.error(`[Error] Rules directory not found in repo: ${folder || "(root)"}`);
51
- process.exit(1);
52
- }
53
- return rulesDir;
54
- }
55
-
56
- /**
57
- * Clone or update remote repository and return local rules directory.
58
- * Private repositories require user git credentials (SSH key or token).
59
- * @param {string} projectRoot
60
- * @param {{ repoUrl: string, branch?: string, folder?: string }} config
61
- * @returns {string}
62
- */
63
- function getLinkedRulesDir(projectRoot, config) {
64
- const repoUrl = config.repoUrl;
65
- const branch = config.branch || DEFAULT_BRANCH;
66
- const folder = config.folder || "";
67
-
68
- const cacheBase = path.join(projectRoot, CACHE_DIR_NAME);
69
- const repoName = sanitizeRepoName(repoUrl);
70
- const clonePath = path.join(cacheBase, repoName);
71
-
72
- if (!fs.existsSync(clonePath)) {
73
- fs.mkdirSync(cacheBase, { recursive: true });
74
- try {
75
- cloneLinkedRepo(projectRoot, clonePath, branch, repoUrl);
76
- } catch {
77
- console.error("[Error] Failed to clone rules repository.");
78
- console.error(" For private repos, ensure you have access (SSH key or HTTPS token).");
79
- console.error(" Example: heymark link https://github.com/org/repo.git");
80
- process.exit(1);
81
- }
82
- } else {
83
- try {
84
- updateLinkedRepo(clonePath, branch);
85
- } catch {
86
- // Continue with cached clone when fetch or pull fails.
87
- }
88
- }
89
-
90
- return resolveLinkedFolder(clonePath, folder);
91
- }
92
-
93
- module.exports = {
94
- CACHE_DIR_NAME,
95
- getLinkedRulesDir,
96
- sanitizeRepoName,
97
- };
@@ -1,49 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const SKILLS_DIR = path.join(".agent", "skills");
7
- const SKILL_FILE_NAME = "SKILL.md";
8
-
9
- function getSkillDir(projectRoot, ruleName) {
10
- return path.join(projectRoot, SKILLS_DIR, ruleName);
11
- }
12
-
13
- function createSkillContent(rule) {
14
- const frontmatterLines = [
15
- "---",
16
- `name: ${rule.name}`,
17
- `description: "${rule.description}"`,
18
- "---",
19
- ];
20
-
21
- return `${frontmatterLines.join("\n")}\n\n${rule.body}\n`;
22
- }
23
-
24
- module.exports = {
25
- name: "Antigravity",
26
- output: ".agent/skills/*/SKILL.md",
27
-
28
- generate(rules, projectRoot) {
29
- for (const rule of rules) {
30
- const skillDir = getSkillDir(projectRoot, rule.name);
31
- fs.mkdirSync(skillDir, { recursive: true });
32
- const filePath = path.join(skillDir, SKILL_FILE_NAME);
33
- const content = createSkillContent(rule);
34
- fs.writeFileSync(filePath, content, "utf8");
35
- }
36
-
37
- return rules.length;
38
- },
39
-
40
- clean(ruleNames, projectRoot) {
41
- const skillsDirPath = path.join(projectRoot, SKILLS_DIR);
42
- if (!fs.existsSync(skillsDirPath)) {
43
- return [];
44
- }
45
-
46
- fs.rmSync(skillsDirPath, { recursive: true, force: true });
47
- return [SKILLS_DIR];
48
- },
49
- };
@@ -1,49 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const SKILLS_DIR = path.join(".claude", "skills");
7
- const SKILL_FILE_NAME = "SKILL.md";
8
-
9
- function getSkillDir(projectRoot, ruleName) {
10
- return path.join(projectRoot, SKILLS_DIR, ruleName);
11
- }
12
-
13
- function createSkillContent(rule) {
14
- const frontmatterLines = [
15
- "---",
16
- `name: ${rule.name}`,
17
- `description: "${rule.description}"`,
18
- "---",
19
- ];
20
-
21
- return `${frontmatterLines.join("\n")}\n\n${rule.body}\n`;
22
- }
23
-
24
- module.exports = {
25
- name: "Claude Code",
26
- output: ".claude/skills/*/SKILL.md",
27
-
28
- generate(rules, projectRoot) {
29
- for (const rule of rules) {
30
- const skillDir = getSkillDir(projectRoot, rule.name);
31
- fs.mkdirSync(skillDir, { recursive: true });
32
- const filePath = path.join(skillDir, SKILL_FILE_NAME);
33
- const content = createSkillContent(rule);
34
- fs.writeFileSync(filePath, content, "utf8");
35
- }
36
-
37
- return rules.length;
38
- },
39
-
40
- clean(ruleNames, projectRoot) {
41
- const skillsDirPath = path.join(projectRoot, SKILLS_DIR);
42
- if (!fs.existsSync(skillsDirPath)) {
43
- return [];
44
- }
45
-
46
- fs.rmSync(skillsDirPath, { recursive: true, force: true });
47
- return [SKILLS_DIR];
48
- },
49
- };
@@ -1,49 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const SKILLS_DIR = path.join(".agents", "skills");
7
- const SKILL_FILE_NAME = "SKILL.md";
8
-
9
- function getSkillDir(projectRoot, ruleName) {
10
- return path.join(projectRoot, SKILLS_DIR, ruleName);
11
- }
12
-
13
- function createSkillContent(rule) {
14
- const frontmatterLines = [
15
- "---",
16
- `name: ${rule.name}`,
17
- `description: "${rule.description}"`,
18
- "---",
19
- ];
20
-
21
- return `${frontmatterLines.join("\n")}\n\n${rule.body}\n`;
22
- }
23
-
24
- module.exports = {
25
- name: "OpenAI Codex",
26
- output: ".agents/skills/*/SKILL.md",
27
-
28
- generate(rules, projectRoot) {
29
- for (const rule of rules) {
30
- const skillDir = getSkillDir(projectRoot, rule.name);
31
- fs.mkdirSync(skillDir, { recursive: true });
32
- const filePath = path.join(skillDir, SKILL_FILE_NAME);
33
- const content = createSkillContent(rule);
34
- fs.writeFileSync(filePath, content, "utf8");
35
- }
36
-
37
- return rules.length;
38
- },
39
-
40
- clean(ruleNames, projectRoot) {
41
- const skillsDirPath = path.join(projectRoot, SKILLS_DIR);
42
- if (!fs.existsSync(skillsDirPath)) {
43
- return [];
44
- }
45
-
46
- fs.rmSync(skillsDirPath, { recursive: true, force: true });
47
- return [SKILLS_DIR];
48
- },
49
- };
@@ -1,61 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const INSTRUCTIONS_DIR = path.join(".github", "instructions");
7
- const FILE_SUFFIX = ".instructions.md";
8
- const DEFAULT_GLOB = "**";
9
-
10
- function getInstructionPath(projectRoot, ruleName) {
11
- return path.join(projectRoot, INSTRUCTIONS_DIR, `${ruleName}${FILE_SUFFIX}`);
12
- }
13
-
14
- function normalizeGlobs(globsValue) {
15
- if (!globsValue) {
16
- return [DEFAULT_GLOB];
17
- }
18
-
19
- const globs = globsValue
20
- .split(",")
21
- .map((glob) => glob.trim())
22
- .filter(Boolean);
23
-
24
- return globs.length > 0 ? globs : [DEFAULT_GLOB];
25
- }
26
-
27
- function createInstructionContent(rule) {
28
- const applyToLines = normalizeGlobs(rule.globs)
29
- .map((glob) => ` - "${glob}"`)
30
- .join("\n");
31
- const header = `applyTo:\n${applyToLines}\n---`;
32
- return `${header}\n\n${rule.body}\n`;
33
- }
34
-
35
- module.exports = {
36
- name: "GitHub Copilot",
37
- output: ".github/instructions/*.instructions.md",
38
-
39
- generate(rules, projectRoot) {
40
- const destDir = path.join(projectRoot, INSTRUCTIONS_DIR);
41
- fs.mkdirSync(destDir, { recursive: true });
42
-
43
- for (const rule of rules) {
44
- const filePath = getInstructionPath(projectRoot, rule.name);
45
- const content = createInstructionContent(rule);
46
- fs.writeFileSync(filePath, content, "utf8");
47
- }
48
-
49
- return rules.length;
50
- },
51
-
52
- clean(ruleNames, projectRoot) {
53
- const instructionsDirPath = path.join(projectRoot, INSTRUCTIONS_DIR);
54
- if (!fs.existsSync(instructionsDirPath)) {
55
- return [];
56
- }
57
-
58
- fs.rmSync(instructionsDirPath, { recursive: true, force: true });
59
- return [INSTRUCTIONS_DIR];
60
- },
61
- };
@@ -1,48 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const RULES_DIR = path.join(".cursor", "rules");
7
- const FILE_SUFFIX = ".mdc";
8
-
9
- function getRulePath(projectRoot, ruleName) {
10
- return path.join(projectRoot, RULES_DIR, `${ruleName}${FILE_SUFFIX}`);
11
- }
12
-
13
- function createRuleContent(rule) {
14
- const frontmatterLines = ["---", `description: "${rule.description}"`];
15
- if (rule.globs) {
16
- frontmatterLines.push(`globs: "${rule.globs}"`);
17
- }
18
- frontmatterLines.push(`alwaysApply: ${rule.alwaysApply}`);
19
- frontmatterLines.push("---");
20
-
21
- return `${frontmatterLines.join("\n")}\n\n${rule.body}\n`;
22
- }
23
-
24
- module.exports = {
25
- name: "Cursor",
26
- output: ".cursor/rules/*.mdc",
27
-
28
- generate(rules, projectRoot) {
29
- const destDir = path.join(projectRoot, RULES_DIR);
30
- fs.mkdirSync(destDir, { recursive: true });
31
-
32
- for (const rule of rules) {
33
- fs.writeFileSync(getRulePath(projectRoot, rule.name), createRuleContent(rule), "utf8");
34
- }
35
-
36
- return rules.length;
37
- },
38
-
39
- clean(ruleNames, projectRoot) {
40
- const rulesDirPath = path.join(projectRoot, RULES_DIR);
41
- if (!fs.existsSync(rulesDirPath)) {
42
- return [];
43
- }
44
-
45
- fs.rmSync(rulesDirPath, { recursive: true, force: true });
46
- return [RULES_DIR];
47
- },
48
- };