heymark 1.1.2 → 1.2.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 +170 -170
- package/README.md +170 -170
- package/package.json +56 -57
- package/scripts/lib/config.js +110 -73
- package/scripts/lib/parser.js +125 -82
- package/scripts/lib/repo.js +52 -35
- package/scripts/sync.js +330 -239
- package/scripts/tools/antigravity.js +53 -41
- package/scripts/tools/claude.js +53 -42
- package/scripts/tools/codex.js +53 -41
- package/scripts/tools/copilot.js +65 -41
- package/scripts/tools/cursor.js +52 -41
package/scripts/lib/parser.js
CHANGED
|
@@ -1,82 +1,125 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
parseFrontmatter
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
CHANGED
|
@@ -5,47 +5,75 @@ const path = require("path");
|
|
|
5
5
|
const { execSync } = require("child_process");
|
|
6
6
|
|
|
7
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 = /([^/]+\/[^/]+?)(?:\/|$)/;
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
13
|
+
* Convert repository URL to a stable cache directory name.
|
|
11
14
|
* https://github.com/org/repo -> org-repo
|
|
12
15
|
* git@github.com:org/repo.git -> org-repo
|
|
16
|
+
* @param {string} repoUrl
|
|
17
|
+
* @returns {string}
|
|
13
18
|
*/
|
|
14
|
-
function sanitizeRepoName(
|
|
15
|
-
let
|
|
16
|
-
if (
|
|
17
|
-
|
|
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);
|
|
19
26
|
if (match) {
|
|
20
27
|
return match[1].replace(/\//g, "-");
|
|
21
28
|
}
|
|
22
|
-
|
|
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;
|
|
23
54
|
}
|
|
24
55
|
|
|
25
56
|
/**
|
|
26
|
-
*
|
|
27
|
-
* Private
|
|
28
|
-
* @param {string} projectRoot
|
|
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
|
|
29
60
|
* @param {{ rulesSource: string, branch?: string, rulesSourceDir?: string }} config
|
|
30
|
-
* @returns {string}
|
|
61
|
+
* @returns {string}
|
|
31
62
|
*/
|
|
32
63
|
function getRulesDirFromRepo(projectRoot, config) {
|
|
33
|
-
const
|
|
34
|
-
const branch = config.branch ||
|
|
35
|
-
const
|
|
64
|
+
const repoUrl = config.rulesSource;
|
|
65
|
+
const branch = config.branch || DEFAULT_BRANCH;
|
|
66
|
+
const rulesSourceDir = config.rulesSourceDir || "";
|
|
36
67
|
|
|
37
68
|
const cacheBase = path.join(projectRoot, CACHE_DIR_NAME);
|
|
38
|
-
const repoName = sanitizeRepoName(
|
|
69
|
+
const repoName = sanitizeRepoName(repoUrl);
|
|
39
70
|
const clonePath = path.join(cacheBase, repoName);
|
|
40
71
|
|
|
41
72
|
if (!fs.existsSync(clonePath)) {
|
|
42
73
|
fs.mkdirSync(cacheBase, { recursive: true });
|
|
43
74
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cwd: projectRoot,
|
|
47
|
-
});
|
|
48
|
-
} catch (err) {
|
|
75
|
+
cloneRulesRepo(projectRoot, clonePath, branch, repoUrl);
|
|
76
|
+
} catch {
|
|
49
77
|
console.error("[Error] Failed to clone rules repository.");
|
|
50
78
|
console.error(" For private repos, ensure you have access (SSH key or HTTPS token).");
|
|
51
79
|
console.error(" Example: heymark init https://github.com/org/repo.git");
|
|
@@ -53,24 +81,13 @@ function getRulesDirFromRepo(projectRoot, config) {
|
|
|
53
81
|
}
|
|
54
82
|
} else {
|
|
55
83
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stdio: "pipe",
|
|
60
|
-
cwd: clonePath,
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// pull 실패 시(네트워크 등) 기존 클론 내용으로 진행
|
|
84
|
+
updateRulesRepo(clonePath, branch);
|
|
85
|
+
} catch {
|
|
86
|
+
// Continue with cached clone when fetch or pull fails.
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(rulesDir) || !fs.statSync(rulesDir).isDirectory()) {
|
|
70
|
-
console.error(`[Error] Rules directory not found in repo: ${subDir || "(root)"}`);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
return rulesDir;
|
|
90
|
+
return resolveRulesDirectory(clonePath, rulesSourceDir);
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
module.exports = {
|