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.
- package/LICENSE +21 -21
- package/README.ko.md +121 -123
- package/README.md +123 -125
- package/package.json +57 -56
- package/src/alias.js +41 -0
- package/src/commands/clean/index.js +17 -0
- package/src/commands/cleaner.js +27 -0
- package/src/commands/constants.js +14 -0
- package/src/commands/help/index.js +34 -0
- package/src/commands/link/flags/branch.js +18 -0
- package/src/commands/link/flags/folder.js +18 -0
- package/src/commands/link/index.js +58 -0
- package/src/commands/select-tools.js +35 -0
- package/src/commands/sync/index.js +35 -0
- package/src/index.js +52 -0
- package/src/skill-repo/cache-folder.js +77 -0
- package/src/skill-repo/config-file.js +87 -0
- package/src/skill-repo/constants.js +14 -0
- package/src/skill-repo/skill-file-parser.js +79 -0
- package/src/tools/antigravity/index.js +33 -0
- package/src/tools/claude-code/index.js +33 -0
- package/src/tools/codex/index.js +33 -0
- package/src/tools/constants.js +52 -0
- package/src/tools/copilot/index.js +40 -0
- package/src/tools/cursor/index.js +39 -0
- package/src/tools/loader.js +17 -0
- package/src/tools/openclaw/index.js +72 -0
- package/src/tools/skill-per-file.js +29 -0
- package/src/tools/skill-per-folder.js +18 -0
- package/scripts/cli.js +0 -375
- package/scripts/lib/config.js +0 -126
- package/scripts/lib/parser.js +0 -125
- package/scripts/lib/repo.js +0 -97
- package/scripts/tools/antigravity.js +0 -49
- package/scripts/tools/claude.js +0 -49
- package/scripts/tools/codex.js +0 -49
- package/scripts/tools/copilot.js +0 -61
- package/scripts/tools/cursor.js +0 -48
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { CODEX } = require("@/tools/constants");
|
|
2
|
+
const { generate, clean } = require("@/tools/skill-per-folder");
|
|
3
|
+
|
|
4
|
+
function createContent(skill) {
|
|
5
|
+
const frontmatterLines = [
|
|
6
|
+
"---",
|
|
7
|
+
`name: ${skill.name}`,
|
|
8
|
+
`description: "${skill.description}"`,
|
|
9
|
+
"---",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
return `${frontmatterLines.join("\n")}\n\n${skill.body}\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
key: CODEX.KEY,
|
|
17
|
+
name: CODEX.NAME,
|
|
18
|
+
output: CODEX.OUTPUT_PATTERN,
|
|
19
|
+
|
|
20
|
+
generate(skills, cwd) {
|
|
21
|
+
return generate({
|
|
22
|
+
cwd,
|
|
23
|
+
dir: CODEX.SKILLS_DIR,
|
|
24
|
+
fileName: CODEX.SKILL_FILE_NAME,
|
|
25
|
+
skills,
|
|
26
|
+
createContent,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
clean(skillNames, cwd) {
|
|
31
|
+
return clean(cwd, CODEX.SKILLS_DIR);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const ANTIGRAVITY = {
|
|
4
|
+
KEY: "antigravity",
|
|
5
|
+
NAME: "Antigravity",
|
|
6
|
+
SKILLS_DIR: path.join(".agent", "skills"),
|
|
7
|
+
SKILL_FILE_NAME: "SKILL.md",
|
|
8
|
+
OUTPUT_PATTERN: ".agent/skills/*/SKILL.md",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const CLAUDE_CODE = {
|
|
12
|
+
KEY: "claude-code",
|
|
13
|
+
NAME: "Claude Code",
|
|
14
|
+
SKILLS_DIR: path.join(".claude", "skills"),
|
|
15
|
+
SKILL_FILE_NAME: "SKILL.md",
|
|
16
|
+
OUTPUT_PATTERN: ".claude/skills/*/SKILL.md",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CODEX = {
|
|
20
|
+
KEY: "codex",
|
|
21
|
+
NAME: "Codex",
|
|
22
|
+
SKILLS_DIR: path.join(".agents", "skills"),
|
|
23
|
+
SKILL_FILE_NAME: "SKILL.md",
|
|
24
|
+
OUTPUT_PATTERN: ".agents/skills/*/SKILL.md",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const COPILOT = {
|
|
28
|
+
KEY: "copilot",
|
|
29
|
+
NAME: "Copilot",
|
|
30
|
+
INSTRUCTIONS_DIR: path.join(".github", "instructions"),
|
|
31
|
+
FILE_SUFFIX: ".instructions.md",
|
|
32
|
+
DEFAULT_GLOB: "**",
|
|
33
|
+
OUTPUT_PATTERN: ".github/instructions/*.instructions.md",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const CURSOR = {
|
|
37
|
+
KEY: "cursor",
|
|
38
|
+
NAME: "Cursor",
|
|
39
|
+
SKILLS_DIR: path.join(".cursor", "rules"),
|
|
40
|
+
FILE_SUFFIX: ".mdc",
|
|
41
|
+
OUTPUT_PATTERN: ".cursor/rules/*.mdc",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const OPENCLAW = {
|
|
45
|
+
KEY: "openclaw",
|
|
46
|
+
NAME: "OpenClaw",
|
|
47
|
+
SKILLS_DIR: path.join(".openclaw", "skills"),
|
|
48
|
+
SKILL_FILE_NAME: "SKILL.md",
|
|
49
|
+
OUTPUT_PATTERN: "~/.openclaw/skills/*/SKILL.md",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = { ANTIGRAVITY, CLAUDE_CODE, CODEX, COPILOT, CURSOR, OPENCLAW };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { COPILOT } = require("@/tools/constants");
|
|
2
|
+
const { generate, clean } = require("@/tools/skill-per-file");
|
|
3
|
+
|
|
4
|
+
function getFileName(skill) {
|
|
5
|
+
return `${skill.name}${COPILOT.FILE_SUFFIX}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createContent(skill) {
|
|
9
|
+
const globs = skill.globs
|
|
10
|
+
? skill.globs
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((g) => g.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
: [];
|
|
15
|
+
const applyToLines = (globs.length > 0 ? globs : [COPILOT.DEFAULT_GLOB])
|
|
16
|
+
.map((glob) => ` - "${glob}"`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
const header = `applyTo:\n${applyToLines}\n---`;
|
|
19
|
+
return `${header}\n\n${skill.body}\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
key: COPILOT.KEY,
|
|
24
|
+
name: COPILOT.NAME,
|
|
25
|
+
output: COPILOT.OUTPUT_PATTERN,
|
|
26
|
+
|
|
27
|
+
generate(skills, cwd) {
|
|
28
|
+
return generate({
|
|
29
|
+
cwd,
|
|
30
|
+
dir: COPILOT.INSTRUCTIONS_DIR,
|
|
31
|
+
skills,
|
|
32
|
+
getFileName,
|
|
33
|
+
createContent,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
clean(skillNames, cwd) {
|
|
38
|
+
return clean(cwd, COPILOT.INSTRUCTIONS_DIR);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { CURSOR } = require("@/tools/constants");
|
|
2
|
+
const { generate, clean } = require("@/tools/skill-per-file");
|
|
3
|
+
|
|
4
|
+
function getFileName(skill) {
|
|
5
|
+
return `${skill.name}${CURSOR.FILE_SUFFIX}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createContent(skill) {
|
|
9
|
+
const frontmatterLines = ["---", `description: "${skill.description}"`];
|
|
10
|
+
|
|
11
|
+
if (skill.globs) {
|
|
12
|
+
frontmatterLines.push(`globs: "${skill.globs}"`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
frontmatterLines.push(`alwaysApply: ${skill.alwaysApply}`);
|
|
16
|
+
frontmatterLines.push("---");
|
|
17
|
+
|
|
18
|
+
return `${frontmatterLines.join("\n")}\n\n${skill.body}\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
key: CURSOR.KEY,
|
|
23
|
+
name: CURSOR.NAME,
|
|
24
|
+
output: CURSOR.OUTPUT_PATTERN,
|
|
25
|
+
|
|
26
|
+
generate(skills, cwd) {
|
|
27
|
+
return generate({
|
|
28
|
+
cwd,
|
|
29
|
+
dir: CURSOR.SKILLS_DIR,
|
|
30
|
+
skills,
|
|
31
|
+
getFileName,
|
|
32
|
+
createContent,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
clean(skillNames, cwd) {
|
|
37
|
+
return clean(cwd, CURSOR.SKILLS_DIR);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const TOOLS_DIR = path.join(__dirname);
|
|
5
|
+
|
|
6
|
+
function loadTools() {
|
|
7
|
+
const tools = fs
|
|
8
|
+
.readdirSync(TOOLS_DIR)
|
|
9
|
+
.filter((name) => fs.statSync(path.join(TOOLS_DIR, name)).isDirectory())
|
|
10
|
+
.map((name) => require(path.join(TOOLS_DIR, name)));
|
|
11
|
+
|
|
12
|
+
return Object.fromEntries(tools.map((tool) => [tool.key, tool]));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
loadTools,
|
|
17
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { OPENCLAW } = require("@/tools/constants");
|
|
5
|
+
|
|
6
|
+
function createContent(skill) {
|
|
7
|
+
const frontmatterLines = [
|
|
8
|
+
"---",
|
|
9
|
+
`name: ${skill.name}`,
|
|
10
|
+
`description: "${skill.description}"`,
|
|
11
|
+
"---",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
return `${frontmatterLines.join("\n")}\n\n${skill.body}\n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getSkillsDir() {
|
|
18
|
+
return path.join(os.homedir(), OPENCLAW.SKILLS_DIR);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generate(skills) {
|
|
22
|
+
const skillsDir = getSkillsDir();
|
|
23
|
+
|
|
24
|
+
for (const skill of skills) {
|
|
25
|
+
try {
|
|
26
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
27
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(skillDir, OPENCLAW.SKILL_FILE_NAME), createContent(skill), "utf8");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
31
|
+
throw new Error(`OpenClaw: failed to generate skill "${skill.name}" at ${skillDir}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return skills.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clean(skillNames) {
|
|
39
|
+
const skillsDir = getSkillsDir();
|
|
40
|
+
if (!fs.existsSync(skillsDir)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cleanedPaths = [];
|
|
45
|
+
for (const skillName of skillNames) {
|
|
46
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
47
|
+
if (!fs.existsSync(skillDir)) continue;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
51
|
+
cleanedPaths.push(path.join(skillsDir, skillName));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`OpenClaw: failed to clean skill "${skillName}" at ${skillDir}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return cleanedPaths;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
key: OPENCLAW.KEY,
|
|
62
|
+
name: OPENCLAW.NAME,
|
|
63
|
+
output: OPENCLAW.OUTPUT_PATTERN,
|
|
64
|
+
|
|
65
|
+
generate(skills) {
|
|
66
|
+
return generate(skills);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
clean(skillNames) {
|
|
70
|
+
return clean(skillNames);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function generate({ cwd, dir, skills, getFileName, createContent }) {
|
|
5
|
+
const destDir = path.join(cwd, dir);
|
|
6
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
7
|
+
|
|
8
|
+
for (const skill of skills) {
|
|
9
|
+
const filePath = path.join(destDir, getFileName(skill));
|
|
10
|
+
fs.writeFileSync(filePath, createContent(skill), "utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return skills.length;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clean(cwd, dir) {
|
|
17
|
+
const targetPath = path.join(cwd, dir);
|
|
18
|
+
if (!fs.existsSync(targetPath)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
23
|
+
return [dir];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
generate,
|
|
28
|
+
clean,
|
|
29
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { clean } = require("@/tools/skill-per-file");
|
|
4
|
+
|
|
5
|
+
function generate({ cwd, dir, fileName, skills, createContent }) {
|
|
6
|
+
for (const skill of skills) {
|
|
7
|
+
const skillDir = path.join(cwd, dir, skill.name);
|
|
8
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
9
|
+
fs.writeFileSync(path.join(skillDir, fileName), createContent(skill), "utf8");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return skills.length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
generate,
|
|
17
|
+
clean,
|
|
18
|
+
};
|
package/scripts/cli.js
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const { CONFIG_RELATIVE, DEFAULT_BRANCH, loadConfig, writeConfig } = require("./lib/config");
|
|
7
|
-
const { loadRules } = require("./lib/parser");
|
|
8
|
-
const { CACHE_DIR_NAME, getLinkedRulesDir, sanitizeRepoName } = require("./lib/repo");
|
|
9
|
-
|
|
10
|
-
const SCRIPT_DIR = __dirname;
|
|
11
|
-
const PROJECT_ROOT = process.cwd();
|
|
12
|
-
|
|
13
|
-
const COMMAND_LINK = "link";
|
|
14
|
-
const COMMAND_SYNC = "sync";
|
|
15
|
-
const COMMAND_CLEAN = "clean";
|
|
16
|
-
const COMMAND_STATUS = "status";
|
|
17
|
-
const COMMAND_HELP = "help";
|
|
18
|
-
|
|
19
|
-
const OPTION_BRANCH = "--branch";
|
|
20
|
-
const OPTION_FOLDER = "--folder";
|
|
21
|
-
const OPTION_SAMPLES = "--samples";
|
|
22
|
-
const SHORT_BRANCH = "-b";
|
|
23
|
-
const SHORT_FOLDER = "-f";
|
|
24
|
-
const TARGET_ALL = "all";
|
|
25
|
-
const TARGET_DOT = ".";
|
|
26
|
-
const LATEST_VERSION_COMMAND = "npx heymark@latest";
|
|
27
|
-
const SAMPLES_REPO_URL = "https://github.com/MosslandOpenDevs/heymark.git";
|
|
28
|
-
const SAMPLES_FOLDER = "samples";
|
|
29
|
-
|
|
30
|
-
function exitWithError(message, details = []) {
|
|
31
|
-
console.error(`[Error] ${message}`);
|
|
32
|
-
details.forEach((detail) => console.error(` ${detail}`));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function discoverTools() {
|
|
37
|
-
const toolsDir = path.join(SCRIPT_DIR, "tools");
|
|
38
|
-
const registry = {};
|
|
39
|
-
|
|
40
|
-
fs.readdirSync(toolsDir)
|
|
41
|
-
.filter((fileName) => fileName.endsWith(".js"))
|
|
42
|
-
.sort()
|
|
43
|
-
.forEach((file) => {
|
|
44
|
-
const key = path.basename(file, ".js");
|
|
45
|
-
registry[key] = require(path.join(toolsDir, file));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return registry;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function showHelp(tools) {
|
|
52
|
-
const toolLines = Object.entries(tools)
|
|
53
|
-
.map(([toolKey, toolDefinition]) => {
|
|
54
|
-
const paddedKey = toolKey.padEnd(10);
|
|
55
|
-
const paddedName = toolDefinition.name.padEnd(16);
|
|
56
|
-
return ` ${paddedKey} ${paddedName} -> ${toolDefinition.output}`;
|
|
57
|
-
})
|
|
58
|
-
.join("\n");
|
|
59
|
-
|
|
60
|
-
console.log(`
|
|
61
|
-
Heymark CLI
|
|
62
|
-
|
|
63
|
-
Reads *.md from a GitHub repository (public or private) and generates
|
|
64
|
-
tool-specific configuration files for various AI coding assistants.
|
|
65
|
-
Same rules everywhere: A computer, B computer, same remote repo.
|
|
66
|
-
|
|
67
|
-
Usage:
|
|
68
|
-
heymark help
|
|
69
|
-
heymark link <repo-url> [--branch <name>] [--folder <path>]
|
|
70
|
-
heymark link --samples
|
|
71
|
-
heymark sync [.|<tool>...]
|
|
72
|
-
heymark clean [.|<tool>...]
|
|
73
|
-
heymark status
|
|
74
|
-
heymark
|
|
75
|
-
|
|
76
|
-
Options:
|
|
77
|
-
--branch, -b <name> Branch name (used with 'link')
|
|
78
|
-
--folder, -f <path> Subdirectory path in repository (used with 'link')
|
|
79
|
-
--samples Use built-in sample Skill repository
|
|
80
|
-
|
|
81
|
-
Targets:
|
|
82
|
-
. All available tools
|
|
83
|
-
<tool>... Space-separated tool names (no commas)
|
|
84
|
-
|
|
85
|
-
Available tools:
|
|
86
|
-
${toolLines}
|
|
87
|
-
|
|
88
|
-
Examples:
|
|
89
|
-
heymark help
|
|
90
|
-
heymark link --samples
|
|
91
|
-
heymark link https://github.com/org/my-rules.git
|
|
92
|
-
heymark link https://github.com/org/my-rules.git --folder rules --branch main
|
|
93
|
-
heymark sync .
|
|
94
|
-
heymark sync cursor claude
|
|
95
|
-
heymark clean .
|
|
96
|
-
heymark status
|
|
97
|
-
heymark
|
|
98
|
-
|
|
99
|
-
Latest version:
|
|
100
|
-
${LATEST_VERSION_COMMAND}
|
|
101
|
-
`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function parseLinkArgs(args) {
|
|
105
|
-
if (args.length === 1 && args[0] === OPTION_SAMPLES) {
|
|
106
|
-
return {
|
|
107
|
-
repoUrl: SAMPLES_REPO_URL,
|
|
108
|
-
branch: DEFAULT_BRANCH,
|
|
109
|
-
folder: SAMPLES_FOLDER,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (args.includes(OPTION_SAMPLES)) {
|
|
114
|
-
exitWithError("--samples cannot be combined with other link arguments.", [
|
|
115
|
-
"Use: heymark link --samples",
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const repoUrl = args[0];
|
|
120
|
-
if (!repoUrl || repoUrl.startsWith("--")) {
|
|
121
|
-
exitWithError("link requires a GitHub repository URL.", [
|
|
122
|
-
"Example: heymark link https://github.com/org/my-rules.git",
|
|
123
|
-
"Example: heymark link git@github.com:org/my-rules.git",
|
|
124
|
-
"Optional: --branch <branch> --folder <subdir> (e.g. --folder rules)",
|
|
125
|
-
]);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let branch = DEFAULT_BRANCH;
|
|
129
|
-
let folder = "";
|
|
130
|
-
|
|
131
|
-
for (let i = 1; i < args.length; i++) {
|
|
132
|
-
const arg = args[i];
|
|
133
|
-
|
|
134
|
-
if (arg === OPTION_BRANCH || arg === SHORT_BRANCH) {
|
|
135
|
-
const value = args[++i];
|
|
136
|
-
if (!value) {
|
|
137
|
-
exitWithError("--branch requires a branch name.");
|
|
138
|
-
}
|
|
139
|
-
branch = value.trim();
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (arg === OPTION_FOLDER || arg === SHORT_FOLDER) {
|
|
144
|
-
const value = args[++i];
|
|
145
|
-
if (!value) {
|
|
146
|
-
exitWithError("--folder requires a folder path.");
|
|
147
|
-
}
|
|
148
|
-
folder = value.trim();
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
exitWithError(`Unknown option for link: ${arg}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { repoUrl: repoUrl.trim(), branch, folder };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function runLink(args) {
|
|
159
|
-
const config = parseLinkArgs(args);
|
|
160
|
-
const configPath = writeConfig(PROJECT_ROOT, config);
|
|
161
|
-
|
|
162
|
-
console.log(
|
|
163
|
-
`[Link] Linked repository saved to ${path.relative(PROJECT_ROOT, configPath) || configPath}`
|
|
164
|
-
);
|
|
165
|
-
console.log(` repoUrl: ${config.repoUrl}`);
|
|
166
|
-
if (config.branch !== DEFAULT_BRANCH) {
|
|
167
|
-
console.log(` branch: ${config.branch}`);
|
|
168
|
-
}
|
|
169
|
-
if (config.folder) {
|
|
170
|
-
console.log(` folder: ${config.folder}`);
|
|
171
|
-
}
|
|
172
|
-
console.log("");
|
|
173
|
-
console.log("Run 'heymark sync .' to fetch rules and generate tool configs.");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function loadLinkedConfigOrExit() {
|
|
177
|
-
const linkedConfig = loadConfig(PROJECT_ROOT);
|
|
178
|
-
if (linkedConfig) {
|
|
179
|
-
return linkedConfig;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
exitWithError("No linked repository found.", [
|
|
183
|
-
`Run: heymark ${COMMAND_LINK} <github-repo-url>`,
|
|
184
|
-
`Config: ${CONFIG_RELATIVE}`,
|
|
185
|
-
]);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function resolveSelectedTools(toolArgs, availableTools) {
|
|
189
|
-
const availableToolKeys = Object.keys(availableTools);
|
|
190
|
-
if (toolArgs.length === 0) {
|
|
191
|
-
return availableToolKeys;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const selectedTools = toolArgs.map((tool) => tool.trim().toLowerCase()).filter(Boolean);
|
|
195
|
-
if (selectedTools.length === 0) {
|
|
196
|
-
return availableToolKeys;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (selectedTools.some((tool) => tool.includes(","))) {
|
|
200
|
-
exitWithError("Tool names must be space-separated (no commas).", [
|
|
201
|
-
"Example: heymark sync cursor claude",
|
|
202
|
-
]);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const hasDotToken = selectedTools.includes(TARGET_DOT);
|
|
206
|
-
const hasAllToken = selectedTools.includes(TARGET_ALL);
|
|
207
|
-
|
|
208
|
-
if (hasAllToken) {
|
|
209
|
-
exitWithError("'all' is not supported. Use '.' for all tools.");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (hasDotToken) {
|
|
213
|
-
if (selectedTools.length > 1) {
|
|
214
|
-
exitWithError(`'${TARGET_DOT}' cannot be combined with tool names.`);
|
|
215
|
-
}
|
|
216
|
-
return availableToolKeys;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const invalidTools = selectedTools.filter((tool) => !availableTools[tool]);
|
|
220
|
-
if (invalidTools.length > 0) {
|
|
221
|
-
exitWithError(`Unknown tool(s): ${invalidTools.join(", ")}`, [
|
|
222
|
-
`Available: ${availableToolKeys.join(", ")}`,
|
|
223
|
-
]);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return Array.from(new Set(selectedTools));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function cleanGeneratedFiles(tools, selectedTools, ruleNames, onlyPrintWhenDeleted) {
|
|
230
|
-
for (const toolKey of selectedTools) {
|
|
231
|
-
const cleanedPaths = tools[toolKey].clean(ruleNames, PROJECT_ROOT);
|
|
232
|
-
if (onlyPrintWhenDeleted && cleanedPaths.length === 0) {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
cleanedPaths.forEach((filePath) => {
|
|
237
|
-
console.log(` Deleted: ${filePath}`);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function loadRulesFromLinkedRepo() {
|
|
243
|
-
const linkedConfig = loadLinkedConfigOrExit();
|
|
244
|
-
const rulesDir = getLinkedRulesDir(PROJECT_ROOT, linkedConfig);
|
|
245
|
-
const rules = loadRules(rulesDir);
|
|
246
|
-
return { linkedConfig, rulesDir, rules };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function printSyncContext(selectedTools, rulesDir) {
|
|
250
|
-
const rulesRelPath = path.relative(PROJECT_ROOT, rulesDir) || ".";
|
|
251
|
-
console.log("[Sync] Starting convention sync...");
|
|
252
|
-
console.log(` Source: ${rulesRelPath} (from linked repo)`);
|
|
253
|
-
console.log(` Target: ${PROJECT_ROOT}`);
|
|
254
|
-
console.log(` Tools: ${selectedTools.join(", ")}`);
|
|
255
|
-
console.log("");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function runSync(toolArgs, tools) {
|
|
259
|
-
const selectedTools = resolveSelectedTools(toolArgs, tools);
|
|
260
|
-
const { rulesDir, rules } = loadRulesFromLinkedRepo();
|
|
261
|
-
printSyncContext(selectedTools, rulesDir);
|
|
262
|
-
|
|
263
|
-
console.log(`[Load] ${rules.length} rule(s): ${rules.map((rule) => rule.name).join(", ")}`);
|
|
264
|
-
console.log("");
|
|
265
|
-
|
|
266
|
-
const ruleNames = rules.map((rule) => rule.name);
|
|
267
|
-
|
|
268
|
-
// Ensure regenerated output is always fresh.
|
|
269
|
-
console.log("[Clean] Removing existing generated files...");
|
|
270
|
-
cleanGeneratedFiles(tools, selectedTools, ruleNames, true);
|
|
271
|
-
console.log("");
|
|
272
|
-
|
|
273
|
-
console.log("[Generate]");
|
|
274
|
-
for (const toolKey of selectedTools) {
|
|
275
|
-
const toolDefinition = tools[toolKey];
|
|
276
|
-
const count = toolDefinition.generate(rules, PROJECT_ROOT);
|
|
277
|
-
const summary = `${toolDefinition.name.padEnd(16)} -> ${toolDefinition.output}`;
|
|
278
|
-
console.log(` ${summary} (${count} rules)`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
console.log("");
|
|
282
|
-
console.log(`[Done] ${selectedTools.length} tool(s) synced successfully.`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function runClean(toolArgs, tools) {
|
|
286
|
-
const selectedTools = resolveSelectedTools(toolArgs, tools);
|
|
287
|
-
const { rulesDir, rules } = loadRulesFromLinkedRepo();
|
|
288
|
-
const rulesRelPath = path.relative(PROJECT_ROOT, rulesDir) || ".";
|
|
289
|
-
|
|
290
|
-
console.log("[Clean] Removing generated files...");
|
|
291
|
-
console.log(` Source: ${rulesRelPath} (from linked repo)`);
|
|
292
|
-
console.log(` Tools: ${selectedTools.join(", ")}`);
|
|
293
|
-
console.log("");
|
|
294
|
-
|
|
295
|
-
const ruleNames = rules.map((rule) => rule.name);
|
|
296
|
-
cleanGeneratedFiles(tools, selectedTools, ruleNames, false);
|
|
297
|
-
|
|
298
|
-
console.log("");
|
|
299
|
-
console.log(`[Done] ${selectedTools.length} tool(s) cleaned successfully.`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function runStatus(tools) {
|
|
303
|
-
const linkedConfig = loadConfig(PROJECT_ROOT);
|
|
304
|
-
const toolKeys = Object.keys(tools);
|
|
305
|
-
|
|
306
|
-
console.log("[Status] Heymark");
|
|
307
|
-
console.log(` Project: ${PROJECT_ROOT}`);
|
|
308
|
-
console.log(` Config: ${CONFIG_RELATIVE}`);
|
|
309
|
-
console.log(` Tools: ${toolKeys.join(", ")}`);
|
|
310
|
-
console.log(` Latest: ${LATEST_VERSION_COMMAND}`);
|
|
311
|
-
console.log("");
|
|
312
|
-
|
|
313
|
-
if (!linkedConfig) {
|
|
314
|
-
console.log("No repository is linked yet.");
|
|
315
|
-
console.log(`Run: heymark ${COMMAND_LINK} <github-repo-url>`);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const cachePath = path.join(
|
|
320
|
-
PROJECT_ROOT,
|
|
321
|
-
CACHE_DIR_NAME,
|
|
322
|
-
sanitizeRepoName(linkedConfig.repoUrl)
|
|
323
|
-
);
|
|
324
|
-
const cacheState = fs.existsSync(cachePath) ? "ready" : "not-fetched";
|
|
325
|
-
|
|
326
|
-
console.log("Linked repository:");
|
|
327
|
-
console.log(` repoUrl: ${linkedConfig.repoUrl}`);
|
|
328
|
-
console.log(` branch: ${linkedConfig.branch || DEFAULT_BRANCH}`);
|
|
329
|
-
console.log(` folder: ${linkedConfig.folder || "(root)"}`);
|
|
330
|
-
console.log(` cache: ${cacheState}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function main() {
|
|
334
|
-
const tools = discoverTools();
|
|
335
|
-
const args = process.argv.slice(2);
|
|
336
|
-
|
|
337
|
-
if (args.length === 0) {
|
|
338
|
-
runStatus(tools);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const command = args[0];
|
|
343
|
-
const commandArgs = args.slice(1);
|
|
344
|
-
|
|
345
|
-
if (command === COMMAND_LINK) {
|
|
346
|
-
runLink(commandArgs);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
if (command === COMMAND_SYNC) {
|
|
350
|
-
runSync(commandArgs, tools);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (command === COMMAND_CLEAN) {
|
|
354
|
-
runClean(commandArgs, tools);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (command === COMMAND_STATUS) {
|
|
358
|
-
if (commandArgs.length > 0) {
|
|
359
|
-
exitWithError("status does not accept arguments.");
|
|
360
|
-
}
|
|
361
|
-
runStatus(tools);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (command === COMMAND_HELP) {
|
|
365
|
-
if (commandArgs.length > 0) {
|
|
366
|
-
exitWithError("help does not accept arguments.");
|
|
367
|
-
}
|
|
368
|
-
showHelp(tools);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
exitWithError(`Unknown command: ${command}`, ["Use 'heymark help' for usage information."]);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
main();
|