heymark 1.2.0 → 2.0.1
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.ko.md +61 -111
- package/README.md +63 -111
- package/package.json +4 -4
- 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 +44 -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/skill-per-file.js +29 -0
- package/src/tools/skill-per-folder.js +18 -0
- package/scripts/lib/config.js +0 -110
- package/scripts/lib/parser.js +0 -125
- package/scripts/lib/repo.js +0 -97
- package/scripts/sync.js +0 -330
- package/scripts/tools/antigravity.js +0 -53
- package/scripts/tools/claude.js +0 -53
- package/scripts/tools/codex.js +0 -53
- package/scripts/tools/copilot.js +0 -65
- package/scripts/tools/cursor.js +0 -52
|
@@ -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/lib/config.js
DELETED
|
@@ -1,110 +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 {{ rulesSource: string, branch?: string, rulesSourceDir?: string }} RuleBookConfig
|
|
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 {RuleBookConfig | null}
|
|
28
|
-
*/
|
|
29
|
-
function normalizeConfig(value) {
|
|
30
|
-
if (!value || typeof value !== "object") {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const raw =
|
|
35
|
-
/** @type {{ rulesSource?: unknown, branch?: unknown, rulesSourceDir?: unknown }} */ (
|
|
36
|
-
value
|
|
37
|
-
);
|
|
38
|
-
if (typeof raw.rulesSource !== "string" || !raw.rulesSource.trim()) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
rulesSource: raw.rulesSource.trim(),
|
|
44
|
-
branch:
|
|
45
|
-
typeof raw.branch === "string" && raw.branch.trim()
|
|
46
|
-
? raw.branch.trim()
|
|
47
|
-
: DEFAULT_BRANCH,
|
|
48
|
-
rulesSourceDir: typeof raw.rulesSourceDir === "string" ? raw.rulesSourceDir.trim() : "",
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Read config from project root.
|
|
54
|
-
* @param {string} projectRoot
|
|
55
|
-
* @returns {RuleBookConfig | null}
|
|
56
|
-
*/
|
|
57
|
-
function loadConfig(projectRoot) {
|
|
58
|
-
const configPath = getConfigPath(projectRoot);
|
|
59
|
-
if (!fs.existsSync(configPath)) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const raw = fs.readFileSync(configPath, "utf8");
|
|
65
|
-
return normalizeConfig(JSON.parse(raw));
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create initial config file in .heymark/config.json.
|
|
73
|
-
* @param {string} projectRoot
|
|
74
|
-
* @param {RuleBookConfig} config
|
|
75
|
-
* @returns {string}
|
|
76
|
-
*/
|
|
77
|
-
function writeConfig(projectRoot, config) {
|
|
78
|
-
const configDir = path.join(projectRoot, CONFIG_DIR);
|
|
79
|
-
if (!fs.existsSync(configDir)) {
|
|
80
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const normalized = normalizeConfig(config);
|
|
84
|
-
if (!normalized) {
|
|
85
|
-
throw new Error("Invalid config payload");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const configPath = path.join(configDir, CONFIG_FILENAME);
|
|
89
|
-
const toWrite = {
|
|
90
|
-
rulesSource: normalized.rulesSource,
|
|
91
|
-
branch: normalized.branch || DEFAULT_BRANCH,
|
|
92
|
-
};
|
|
93
|
-
if (normalized.rulesSourceDir) {
|
|
94
|
-
toWrite.rulesSourceDir = normalized.rulesSourceDir;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fs.writeFileSync(configPath, JSON.stringify(toWrite, null, 2), "utf8");
|
|
98
|
-
return configPath;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
module.exports = {
|
|
102
|
-
CONFIG_DIR,
|
|
103
|
-
CONFIG_FILENAME,
|
|
104
|
-
CONFIG_RELATIVE,
|
|
105
|
-
DEFAULT_BRANCH,
|
|
106
|
-
getConfigPath,
|
|
107
|
-
loadConfig,
|
|
108
|
-
normalizeConfig,
|
|
109
|
-
writeConfig,
|
|
110
|
-
};
|
package/scripts/lib/parser.js
DELETED
|
@@ -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
|
-
};
|
package/scripts/lib/repo.js
DELETED
|
@@ -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 cloneRulesRepo(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 updateRulesRepo(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 resolveRulesDirectory(clonePath, rulesSourceDir) {
|
|
48
|
-
const rulesDir = rulesSourceDir ? path.join(clonePath, rulesSourceDir) : clonePath;
|
|
49
|
-
if (!fs.existsSync(rulesDir) || !fs.statSync(rulesDir).isDirectory()) {
|
|
50
|
-
console.error(`[Error] Rules directory not found in repo: ${rulesSourceDir || "(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 {{ rulesSource: string, branch?: string, rulesSourceDir?: string }} config
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
function getRulesDirFromRepo(projectRoot, config) {
|
|
64
|
-
const repoUrl = config.rulesSource;
|
|
65
|
-
const branch = config.branch || DEFAULT_BRANCH;
|
|
66
|
-
const rulesSourceDir = config.rulesSourceDir || "";
|
|
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
|
-
cloneRulesRepo(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 init https://github.com/org/repo.git");
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
try {
|
|
84
|
-
updateRulesRepo(clonePath, branch);
|
|
85
|
-
} catch {
|
|
86
|
-
// Continue with cached clone when fetch or pull fails.
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return resolveRulesDirectory(clonePath, rulesSourceDir);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = {
|
|
94
|
-
CACHE_DIR_NAME,
|
|
95
|
-
getRulesDirFromRepo,
|
|
96
|
-
sanitizeRepoName,
|
|
97
|
-
};
|
package/scripts/sync.js
DELETED
|
@@ -1,330 +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 { getRulesDirFromRepo } = require("./lib/repo");
|
|
9
|
-
|
|
10
|
-
const SCRIPT_DIR = __dirname;
|
|
11
|
-
const PROJECT_ROOT = process.cwd();
|
|
12
|
-
|
|
13
|
-
const INIT_SUBCOMMAND = "init";
|
|
14
|
-
const OPTION_TOOLS = "--tools";
|
|
15
|
-
const OPTION_SOURCE = "--source";
|
|
16
|
-
const OPTION_CLEAN = "--clean";
|
|
17
|
-
const OPTION_PREVIEW = "--preview";
|
|
18
|
-
const OPTION_HELP = "--help";
|
|
19
|
-
const SHORT_TOOLS = "-t";
|
|
20
|
-
const SHORT_SOURCE = "-s";
|
|
21
|
-
const SHORT_CLEAN = "-c";
|
|
22
|
-
const SHORT_PREVIEW = "-p";
|
|
23
|
-
const SHORT_HELP = "-h";
|
|
24
|
-
const OPTION_BRANCH = "--branch";
|
|
25
|
-
const OPTION_DIR = "--dir";
|
|
26
|
-
const OPTION_SAMPLES = "--samples";
|
|
27
|
-
const SHORT_BRANCH = "-b";
|
|
28
|
-
const SHORT_DIR = "-d";
|
|
29
|
-
const SAMPLES_REPO_URL = "https://github.com/MosslandOpenDevs/heymark.git";
|
|
30
|
-
const SAMPLES_SOURCE_DIR = "samples";
|
|
31
|
-
|
|
32
|
-
function exitWithError(message, details = []) {
|
|
33
|
-
console.error(`[Error] ${message}`);
|
|
34
|
-
details.forEach((detail) => console.error(` ${detail}`));
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function discoverTools() {
|
|
39
|
-
const toolsDir = path.join(SCRIPT_DIR, "tools");
|
|
40
|
-
const registry = {};
|
|
41
|
-
|
|
42
|
-
fs.readdirSync(toolsDir)
|
|
43
|
-
.filter((fileName) => fileName.endsWith(".js"))
|
|
44
|
-
.sort()
|
|
45
|
-
.forEach((file) => {
|
|
46
|
-
const key = path.basename(file, ".js");
|
|
47
|
-
registry[key] = require(path.join(toolsDir, file));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return registry;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parseToolList(rawValue) {
|
|
54
|
-
const parsed = rawValue
|
|
55
|
-
.split(",")
|
|
56
|
-
.map((tool) => tool.trim().toLowerCase())
|
|
57
|
-
.filter(Boolean);
|
|
58
|
-
|
|
59
|
-
if (parsed.length === 0) {
|
|
60
|
-
exitWithError("--tools requires a comma-separated list.");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return parsed;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseArgs(availableTools) {
|
|
67
|
-
const args = process.argv.slice(2);
|
|
68
|
-
const config = {
|
|
69
|
-
tools: Object.keys(availableTools),
|
|
70
|
-
clean: false,
|
|
71
|
-
preview: false,
|
|
72
|
-
help: false,
|
|
73
|
-
source: null,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
for (let i = 0; i < args.length; i++) {
|
|
77
|
-
const arg = args[i];
|
|
78
|
-
|
|
79
|
-
if (arg === OPTION_TOOLS || arg === SHORT_TOOLS) {
|
|
80
|
-
const value = args[++i];
|
|
81
|
-
if (!value) {
|
|
82
|
-
exitWithError("--tools requires a comma-separated list.");
|
|
83
|
-
}
|
|
84
|
-
config.tools = parseToolList(value);
|
|
85
|
-
} else if (arg === OPTION_CLEAN || arg === SHORT_CLEAN) {
|
|
86
|
-
config.clean = true;
|
|
87
|
-
} else if (arg === OPTION_PREVIEW || arg === SHORT_PREVIEW) {
|
|
88
|
-
config.preview = true;
|
|
89
|
-
} else if (arg === OPTION_SOURCE || arg === SHORT_SOURCE) {
|
|
90
|
-
const value = args[++i];
|
|
91
|
-
if (!value) {
|
|
92
|
-
exitWithError("--source requires a GitHub repository URL.");
|
|
93
|
-
}
|
|
94
|
-
config.source = value.trim();
|
|
95
|
-
} else if (arg === OPTION_HELP || arg === SHORT_HELP) {
|
|
96
|
-
config.help = true;
|
|
97
|
-
} else {
|
|
98
|
-
exitWithError(`Unknown option: ${arg}`, ["Use --help for usage information."]);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const invalidTools = config.tools.filter((tool) => !availableTools[tool]);
|
|
103
|
-
if (invalidTools.length > 0) {
|
|
104
|
-
exitWithError(`Unknown tool(s): ${invalidTools.join(", ")}`, [
|
|
105
|
-
`Available: ${Object.keys(availableTools).join(", ")}`,
|
|
106
|
-
]);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return config;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function showHelp(tools) {
|
|
113
|
-
const toolLines = Object.entries(tools)
|
|
114
|
-
.map(([toolKey, toolDefinition]) => {
|
|
115
|
-
const paddedKey = toolKey.padEnd(10);
|
|
116
|
-
const paddedName = toolDefinition.name.padEnd(16);
|
|
117
|
-
return ` ${paddedKey} ${paddedName} -> ${toolDefinition.output}`;
|
|
118
|
-
})
|
|
119
|
-
.join("\n");
|
|
120
|
-
|
|
121
|
-
console.log(`
|
|
122
|
-
AI Coding Tool Convention Sync
|
|
123
|
-
|
|
124
|
-
Reads *.md from a GitHub repository (public or private) and generates
|
|
125
|
-
tool-specific configuration files for various AI coding assistants.
|
|
126
|
-
Same rules everywhere: A computer, B computer, same remote repo.
|
|
127
|
-
|
|
128
|
-
Usage:
|
|
129
|
-
heymark init <repo-url> Set rules source (creates ${CONFIG_RELATIVE})
|
|
130
|
-
heymark [options] Sync from configured or --source repo
|
|
131
|
-
|
|
132
|
-
Options:
|
|
133
|
-
--source, -s <url> GitHub repo URL for this run (overrides ${CONFIG_RELATIVE})
|
|
134
|
-
--tools, -t <list> Comma-separated tool names (default: all)
|
|
135
|
-
--clean, -c Remove all generated files
|
|
136
|
-
--preview, -p Preview what will be generated without writing
|
|
137
|
-
--help, -h Show this help message
|
|
138
|
-
|
|
139
|
-
Rules source (in order):
|
|
140
|
-
1. --source <repo-url>
|
|
141
|
-
2. ${CONFIG_RELATIVE} (set via 'heymark init <repo-url>')
|
|
142
|
-
Private repos: use SSH (git@github.com:org/repo.git) or HTTPS with token.
|
|
143
|
-
|
|
144
|
-
Available tools:
|
|
145
|
-
${toolLines}
|
|
146
|
-
|
|
147
|
-
Examples:
|
|
148
|
-
heymark init https://github.com/org/my-rules.git
|
|
149
|
-
heymark init https://github.com/org/my-rules.git --dir rules --branch main
|
|
150
|
-
heymark
|
|
151
|
-
heymark -s https://github.com/org/other-rules.git
|
|
152
|
-
heymark -t cursor,claude
|
|
153
|
-
heymark -c
|
|
154
|
-
heymark -p
|
|
155
|
-
`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseInitArgs(initArgs) {
|
|
159
|
-
if (initArgs.length === 1 && initArgs[0] === OPTION_SAMPLES) {
|
|
160
|
-
return {
|
|
161
|
-
rulesSource: SAMPLES_REPO_URL,
|
|
162
|
-
branch: DEFAULT_BRANCH,
|
|
163
|
-
rulesSourceDir: SAMPLES_SOURCE_DIR,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (initArgs.includes(OPTION_SAMPLES)) {
|
|
168
|
-
exitWithError("--samples cannot be combined with other init arguments.", [
|
|
169
|
-
"Use: heymark init --samples",
|
|
170
|
-
]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const repoUrl = initArgs[0];
|
|
174
|
-
if (!repoUrl || repoUrl.startsWith("--")) {
|
|
175
|
-
exitWithError("init requires a GitHub repository URL.", [
|
|
176
|
-
"Sample preset: heymark init --samples",
|
|
177
|
-
"Example: heymark init https://github.com/org/my-rules.git",
|
|
178
|
-
"Example: heymark init git@github.com:org/my-rules.git",
|
|
179
|
-
"Optional: --branch <branch> --dir <subdir> (e.g. --dir rules)",
|
|
180
|
-
]);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let branch = DEFAULT_BRANCH;
|
|
184
|
-
let rulesSourceDir = "";
|
|
185
|
-
|
|
186
|
-
for (let i = 1; i < initArgs.length; i++) {
|
|
187
|
-
const arg = initArgs[i];
|
|
188
|
-
|
|
189
|
-
if (arg === OPTION_BRANCH || arg === SHORT_BRANCH) {
|
|
190
|
-
const value = initArgs[++i];
|
|
191
|
-
if (!value) {
|
|
192
|
-
exitWithError("--branch requires a branch name.");
|
|
193
|
-
}
|
|
194
|
-
branch = value.trim();
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (arg === OPTION_DIR || arg === SHORT_DIR) {
|
|
199
|
-
const value = initArgs[++i];
|
|
200
|
-
if (!value) {
|
|
201
|
-
exitWithError("--dir requires a directory path.");
|
|
202
|
-
}
|
|
203
|
-
rulesSourceDir = value.trim();
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
exitWithError(`Unknown option for init: ${arg}`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return { rulesSource: repoUrl.trim(), branch, rulesSourceDir };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function runInit(initArgs) {
|
|
214
|
-
const config = parseInitArgs(initArgs);
|
|
215
|
-
const configPath = writeConfig(PROJECT_ROOT, config);
|
|
216
|
-
|
|
217
|
-
console.log(
|
|
218
|
-
`[Init] Rules source saved to ${path.relative(PROJECT_ROOT, configPath) || configPath}`
|
|
219
|
-
);
|
|
220
|
-
console.log(` rulesSource: ${config.rulesSource}`);
|
|
221
|
-
if (config.branch !== DEFAULT_BRANCH) {
|
|
222
|
-
console.log(` branch: ${config.branch}`);
|
|
223
|
-
}
|
|
224
|
-
if (config.rulesSourceDir) {
|
|
225
|
-
console.log(` rulesSourceDir: ${config.rulesSourceDir}`);
|
|
226
|
-
}
|
|
227
|
-
console.log("");
|
|
228
|
-
console.log("Run 'heymark' to fetch rules from the repo and generate tool configs.");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function resolveRulesDir(config) {
|
|
232
|
-
const repoConfig = config.source
|
|
233
|
-
? { rulesSource: config.source, branch: DEFAULT_BRANCH, rulesSourceDir: "" }
|
|
234
|
-
: loadConfig(PROJECT_ROOT);
|
|
235
|
-
|
|
236
|
-
if (!repoConfig) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return getRulesDirFromRepo(PROJECT_ROOT, repoConfig);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function cleanGeneratedFiles(tools, selectedTools, ruleNames, onlyPrintWhenDeleted) {
|
|
244
|
-
for (const toolKey of selectedTools) {
|
|
245
|
-
const cleanedPaths = tools[toolKey].clean(ruleNames, PROJECT_ROOT);
|
|
246
|
-
if (onlyPrintWhenDeleted && cleanedPaths.length === 0) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
cleanedPaths.forEach((filePath) => {
|
|
251
|
-
console.log(` Deleted: ${filePath}`);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function main() {
|
|
257
|
-
const args = process.argv.slice(2);
|
|
258
|
-
const subcommand = args[0];
|
|
259
|
-
if (subcommand === INIT_SUBCOMMAND) {
|
|
260
|
-
runInit(args.slice(1));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const tools = discoverTools();
|
|
265
|
-
const config = parseArgs(tools);
|
|
266
|
-
|
|
267
|
-
if (config.help) {
|
|
268
|
-
showHelp(tools);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const rulesSourceDir = resolveRulesDir(config);
|
|
273
|
-
if (!rulesSourceDir) {
|
|
274
|
-
console.error("[Error] Rules source not set.");
|
|
275
|
-
console.error(" Run: heymark init <github-repo-url>");
|
|
276
|
-
console.error(" Example: heymark init https://github.com/org/my-rules.git");
|
|
277
|
-
console.error(" Or use: heymark --source <repo-url>");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const rulesRelPath = path.relative(PROJECT_ROOT, rulesSourceDir) || ".";
|
|
282
|
-
|
|
283
|
-
console.log("[Sync] Starting convention sync...");
|
|
284
|
-
console.log(` Source: ${rulesRelPath} (from remote repo)`);
|
|
285
|
-
console.log(` Target: ${PROJECT_ROOT}`);
|
|
286
|
-
console.log(` Tools: ${config.tools.join(", ")}`);
|
|
287
|
-
console.log("");
|
|
288
|
-
|
|
289
|
-
const rules = loadRules(rulesSourceDir);
|
|
290
|
-
console.log(`[Load] ${rules.length} rule(s): ${rules.map((rule) => rule.name).join(", ")}`);
|
|
291
|
-
console.log("");
|
|
292
|
-
|
|
293
|
-
const ruleNames = rules.map((rule) => rule.name);
|
|
294
|
-
|
|
295
|
-
if (config.clean) {
|
|
296
|
-
console.log("[Clean] Removing generated files...");
|
|
297
|
-
cleanGeneratedFiles(tools, config.tools, ruleNames, false);
|
|
298
|
-
console.log("");
|
|
299
|
-
console.log(`[Done] Cleaned ${config.tools.length} tool(s) successfully.`);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (config.preview) {
|
|
304
|
-
console.log("[Preview] Would generate:");
|
|
305
|
-
for (const toolKey of config.tools) {
|
|
306
|
-
const toolDefinition = tools[toolKey];
|
|
307
|
-
const summary = `${toolDefinition.name.padEnd(16)} -> ${toolDefinition.output}`;
|
|
308
|
-
console.log(` ${summary} (${rules.length} rules)`);
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Ensure regenerated output is always fresh.
|
|
314
|
-
console.log("[Clean] Removing existing generated files...");
|
|
315
|
-
cleanGeneratedFiles(tools, config.tools, ruleNames, true);
|
|
316
|
-
console.log("");
|
|
317
|
-
|
|
318
|
-
console.log("[Generate]");
|
|
319
|
-
for (const toolKey of config.tools) {
|
|
320
|
-
const toolDefinition = tools[toolKey];
|
|
321
|
-
const count = toolDefinition.generate(rules, PROJECT_ROOT);
|
|
322
|
-
const summary = `${toolDefinition.name.padEnd(16)} -> ${toolDefinition.output}`;
|
|
323
|
-
console.log(` ${summary} (${count} rules)`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
console.log("");
|
|
327
|
-
console.log(`[Done] ${config.tools.length} tool(s) synced successfully.`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
main();
|