yeknal 1.0.5 → 1.0.8
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.md +96 -79
- package/bin/yeknal.js +588 -236
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,79 +1,96 @@
|
|
|
1
|
-
# yeknal
|
|
2
|
-
|
|
3
|
-
A CLI tool to
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
No installation needed
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx yeknal <
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
Run
|
|
16
|
-
|
|
17
|
-
### Security
|
|
18
|
-
|
|
19
|
-
Fetches
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx yeknal security
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- `Security-Master.md`
|
|
27
|
-
- `security-audit.log`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
79
|
-
|
|
1
|
+
# yeknal
|
|
2
|
+
|
|
3
|
+
A CLI tool to fetch project guideline templates and sync skill folders for AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
No installation needed; use `npx`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx yeknal <command>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Run commands from your project root:
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
Fetches `Security-Master.md` and runs a security audit.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx yeknal security
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Output:
|
|
26
|
+
- `Security-Master.md`
|
|
27
|
+
- `security-audit.log`
|
|
28
|
+
|
|
29
|
+
### Design
|
|
30
|
+
|
|
31
|
+
Fetches `Design.md`.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx yeknal design
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output:
|
|
38
|
+
- `Design.md`
|
|
39
|
+
|
|
40
|
+
### SEO
|
|
41
|
+
|
|
42
|
+
Fetches `SEO-Prompt.md`.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx yeknal seo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output:
|
|
49
|
+
- `SEO-Prompt.md`
|
|
50
|
+
|
|
51
|
+
### Skills
|
|
52
|
+
|
|
53
|
+
Syncs skill folders from the GitHub repository into detected local agent folders.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx yeknal skills
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Behavior:
|
|
60
|
+
- Source mode is GitHub download.
|
|
61
|
+
- Uses GitHub API + raw file download by default.
|
|
62
|
+
- If GitHub API rate limit is hit, it automatically falls back to `git clone` (Git must be installed).
|
|
63
|
+
- Top-level folders are included only if they contain `SKILL.md`.
|
|
64
|
+
- Excludes `Design`, `Security`, `Security_Raw`, and `SEO`.
|
|
65
|
+
- Sync targets (if parent folder exists):
|
|
66
|
+
- Gemini: `~/.gemini/antigravity` or `~/.antigravity`
|
|
67
|
+
- Codex: `~/.codex`
|
|
68
|
+
- Claude: `~/.claude`
|
|
69
|
+
- For each detected target, creates `<parent>/skills` if missing.
|
|
70
|
+
- Overwrites destination skill folders to keep targets in sync.
|
|
71
|
+
|
|
72
|
+
Optional environment variable overrides:
|
|
73
|
+
- `YEKNAL_GEMINI_PARENT`
|
|
74
|
+
- `YEKNAL_CODEX_PARENT`
|
|
75
|
+
- `YEKNAL_CLAUDE_PARENT`
|
|
76
|
+
- `YEKNAL_GITHUB_TOKEN` (or `GITHUB_TOKEN`) for higher GitHub API rate limits (optional)
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Pull security guidelines + run audit
|
|
82
|
+
npx yeknal security
|
|
83
|
+
|
|
84
|
+
# Pull design guidelines
|
|
85
|
+
npx yeknal design
|
|
86
|
+
|
|
87
|
+
# Pull SEO guidelines
|
|
88
|
+
npx yeknal seo
|
|
89
|
+
|
|
90
|
+
# Sync skill folders for Gemini/Codex/Claude
|
|
91
|
+
npx yeknal skills
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
ISC
|
package/bin/yeknal.js
CHANGED
|
@@ -1,236 +1,588 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* yeknal CLI
|
|
5
|
-
* Fetches
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// ==========================================
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log("
|
|
48
|
-
console.log(" npx yeknal
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* yeknal CLI
|
|
5
|
+
* - Fetches markdown templates (security, design, seo).
|
|
6
|
+
* - Syncs skill folders into local AI agent directories (skills command).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const https = require("https");
|
|
13
|
+
const { exec } = require("child_process");
|
|
14
|
+
|
|
15
|
+
const fsp = fs.promises;
|
|
16
|
+
|
|
17
|
+
// ==========================================
|
|
18
|
+
// USER CONFIGURATION
|
|
19
|
+
// ==========================================
|
|
20
|
+
const GITHUB_USERNAME = "tryraisins";
|
|
21
|
+
const GITHUB_REPO = "MD_Files";
|
|
22
|
+
const BRANCH = "main";
|
|
23
|
+
// ==========================================
|
|
24
|
+
|
|
25
|
+
const RAW_BASE_URL = `https://raw.githubusercontent.com/${GITHUB_USERNAME}/${GITHUB_REPO}/${BRANCH}`;
|
|
26
|
+
const API_BASE = `https://api.github.com/repos/${GITHUB_USERNAME}/${GITHUB_REPO}`;
|
|
27
|
+
const GITHUB_TOKEN = process.env.YEKNAL_GITHUB_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
28
|
+
|
|
29
|
+
const EXCLUDED_SKILL_FOLDERS = new Set(["Design", "Security", "Security_Raw", "SEO"]);
|
|
30
|
+
|
|
31
|
+
const singleFileConfigs = {
|
|
32
|
+
security: {
|
|
33
|
+
remotePath: "Security/Security-Master.md",
|
|
34
|
+
localName: "Security-Master.md",
|
|
35
|
+
},
|
|
36
|
+
design: {
|
|
37
|
+
remotePath: "Design/SKILL.md",
|
|
38
|
+
localName: "Design.md",
|
|
39
|
+
},
|
|
40
|
+
seo: {
|
|
41
|
+
remotePath: "SEO/seo-improvement-prompt.md",
|
|
42
|
+
localName: "SEO-Prompt.md",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function usage() {
|
|
47
|
+
console.log("\nUsage:");
|
|
48
|
+
console.log(" npx yeknal security");
|
|
49
|
+
console.log(" npx yeknal design");
|
|
50
|
+
console.log(" npx yeknal seo");
|
|
51
|
+
console.log(" npx yeknal skills\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isHttpSuccess(statusCode) {
|
|
55
|
+
return typeof statusCode === "number" && statusCode >= 200 && statusCode < 300;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getRequestHeaders(url) {
|
|
59
|
+
const isApiRequest = url.includes("api.github.com");
|
|
60
|
+
const headers = {
|
|
61
|
+
"User-Agent": "yeknal-cli",
|
|
62
|
+
Accept: isApiRequest ? "application/vnd.github+json" : "*/*",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (GITHUB_TOKEN) {
|
|
66
|
+
headers.Authorization = `Bearer ${GITHUB_TOKEN}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requestBuffer(url, redirectsRemaining = 5) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const req = https.get(
|
|
75
|
+
url,
|
|
76
|
+
{
|
|
77
|
+
headers: getRequestHeaders(url),
|
|
78
|
+
},
|
|
79
|
+
(res) => {
|
|
80
|
+
const statusCode = res.statusCode || 0;
|
|
81
|
+
|
|
82
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && res.headers.location) {
|
|
83
|
+
if (redirectsRemaining <= 0) {
|
|
84
|
+
reject(new Error(`Too many redirects requesting ${url}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
res.resume();
|
|
88
|
+
const nextUrl = new URL(res.headers.location, url).toString();
|
|
89
|
+
requestBuffer(nextUrl, redirectsRemaining - 1).then(resolve).catch(reject);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const chunks = [];
|
|
94
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
95
|
+
res.on("end", () => {
|
|
96
|
+
resolve({
|
|
97
|
+
statusCode,
|
|
98
|
+
body: Buffer.concat(chunks),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
req.on("error", reject);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function fetchJson(url) {
|
|
109
|
+
const response = await requestBuffer(url);
|
|
110
|
+
if (!isHttpSuccess(response.statusCode)) {
|
|
111
|
+
const bodyText = response.body.toString("utf8");
|
|
112
|
+
if (response.statusCode === 403 && bodyText.includes("API rate limit exceeded")) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`GitHub API rate limit exceeded.\n` +
|
|
115
|
+
`Set an auth token to increase limits:\n` +
|
|
116
|
+
` PowerShell: $env:YEKNAL_GITHUB_TOKEN=\"<your_token>\"\n` +
|
|
117
|
+
` Bash/zsh: export YEKNAL_GITHUB_TOKEN=\"<your_token>\"\n` +
|
|
118
|
+
`Then rerun: npx yeknal skills`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`GitHub API request failed (${response.statusCode}): ${url}\n${bodyText}`);
|
|
122
|
+
}
|
|
123
|
+
return JSON.parse(response.body.toString("utf8"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function downloadUrlToFile(url, localPath) {
|
|
127
|
+
const response = await requestBuffer(url);
|
|
128
|
+
if (!isHttpSuccess(response.statusCode)) {
|
|
129
|
+
throw new Error(`Failed to download file (${response.statusCode}): ${url}`);
|
|
130
|
+
}
|
|
131
|
+
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
132
|
+
await fsp.writeFile(localPath, response.body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildGitTreeApiUrl() {
|
|
136
|
+
return `${API_BASE}/git/trees/${encodeURIComponent(BRANCH)}?recursive=1`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function fetchRepoTree() {
|
|
140
|
+
const data = await fetchJson(buildGitTreeApiUrl());
|
|
141
|
+
if (!data || !Array.isArray(data.tree)) {
|
|
142
|
+
throw new Error("GitHub tree response was missing expected data.");
|
|
143
|
+
}
|
|
144
|
+
return data.tree;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function discoverSkillFolders(repoTree) {
|
|
148
|
+
const topLevelDirs = new Set();
|
|
149
|
+
const dirsWithSkill = new Set();
|
|
150
|
+
|
|
151
|
+
for (const entry of repoTree) {
|
|
152
|
+
if (entry.type === "tree" && typeof entry.path === "string" && !entry.path.includes("/")) {
|
|
153
|
+
if (!EXCLUDED_SKILL_FOLDERS.has(entry.path)) {
|
|
154
|
+
topLevelDirs.add(entry.path);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (entry.type === "blob" && typeof entry.path === "string") {
|
|
160
|
+
const parts = entry.path.split("/");
|
|
161
|
+
if (parts.length === 2 && parts[1] === "SKILL.md" && !EXCLUDED_SKILL_FOLDERS.has(parts[0])) {
|
|
162
|
+
dirsWithSkill.add(parts[0]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Array.from(topLevelDirs)
|
|
168
|
+
.filter((dir) => dirsWithSkill.has(dir))
|
|
169
|
+
.sort((a, b) => a.localeCompare(b));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function listFilesForFolder(repoTree, folderName) {
|
|
173
|
+
const prefix = `${folderName}/`;
|
|
174
|
+
return repoTree
|
|
175
|
+
.filter((entry) => entry.type === "blob" && typeof entry.path === "string")
|
|
176
|
+
.map((entry) => entry.path)
|
|
177
|
+
.filter((repoPath) => repoPath.startsWith(prefix))
|
|
178
|
+
.sort((a, b) => a.localeCompare(b));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildRawFileUrl(repoFilePath) {
|
|
182
|
+
const encodedPath = repoFilePath.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
183
|
+
return `${RAW_BASE_URL}/${encodedPath}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function copyDirRecursive(sourceDir, targetDir) {
|
|
187
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
188
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
189
|
+
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
192
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
193
|
+
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
await copyDirRecursive(sourcePath, targetPath);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (entry.isFile()) {
|
|
200
|
+
await fsp.copyFile(sourcePath, targetPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function execCommand(command) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
exec(command, (error, stdout, stderr) => {
|
|
208
|
+
if (error) {
|
|
209
|
+
reject(new Error(stderr || error.message));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
resolve({ stdout, stderr });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function isDirectory(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
const stats = await fsp.stat(filePath);
|
|
220
|
+
return stats.isDirectory();
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function discoverLocalSkillFolders(sourceRoot) {
|
|
227
|
+
const entries = await fsp.readdir(sourceRoot, { withFileTypes: true });
|
|
228
|
+
const folders = [];
|
|
229
|
+
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (!entry.isDirectory()) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const folderName = entry.name;
|
|
235
|
+
if (EXCLUDED_SKILL_FOLDERS.has(folderName)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const skillFilePath = path.join(sourceRoot, folderName, "SKILL.md");
|
|
240
|
+
if (await isDirectory(path.join(sourceRoot, folderName))) {
|
|
241
|
+
try {
|
|
242
|
+
await fsp.access(skillFilePath, fs.constants.F_OK);
|
|
243
|
+
folders.push(folderName);
|
|
244
|
+
} catch {
|
|
245
|
+
// not a skill folder
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return folders.sort((a, b) => a.localeCompare(b));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function downloadSkillsFromGit(tempRoot, skillFolders, repoTree) {
|
|
254
|
+
for (const folder of skillFolders) {
|
|
255
|
+
const localFolder = path.join(tempRoot, folder);
|
|
256
|
+
const folderFiles = listFilesForFolder(repoTree, folder);
|
|
257
|
+
|
|
258
|
+
await fsp.mkdir(localFolder, { recursive: true });
|
|
259
|
+
for (const repoPath of folderFiles) {
|
|
260
|
+
const relativePath = repoPath.slice(folder.length + 1);
|
|
261
|
+
const destinationPath = path.join(localFolder, relativePath);
|
|
262
|
+
await downloadUrlToFile(buildRawFileUrl(repoPath), destinationPath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function stageSkillsFromGitClone(tempRoot) {
|
|
268
|
+
const cloneRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "yeknal-repo-"));
|
|
269
|
+
const repoPath = path.join(cloneRoot, "repo");
|
|
270
|
+
const cloneUrl = `https://github.com/${GITHUB_USERNAME}/${GITHUB_REPO}.git`;
|
|
271
|
+
const cloneCommand = `git clone --depth 1 --branch ${BRANCH} ${cloneUrl} "${repoPath}"`;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await execCommand(cloneCommand);
|
|
275
|
+
const skillFolders = await discoverLocalSkillFolders(repoPath);
|
|
276
|
+
if (skillFolders.length === 0) {
|
|
277
|
+
throw new Error("No skill folders were discovered from the cloned repository.");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const folder of skillFolders) {
|
|
281
|
+
const sourceFolder = path.join(repoPath, folder);
|
|
282
|
+
const destinationFolder = path.join(tempRoot, folder);
|
|
283
|
+
await copyDirRecursive(sourceFolder, destinationFolder);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return skillFolders;
|
|
287
|
+
} finally {
|
|
288
|
+
await fsp.rm(cloneRoot, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function resolveSkillTargets() {
|
|
293
|
+
const home = os.homedir();
|
|
294
|
+
|
|
295
|
+
const targetSpecs = [
|
|
296
|
+
{
|
|
297
|
+
label: "Gemini Antigravity",
|
|
298
|
+
envVar: "YEKNAL_GEMINI_PARENT",
|
|
299
|
+
defaults: [path.join(home, ".gemini", "antigravity"), path.join(home, ".antigravity")],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
label: "Codex",
|
|
303
|
+
envVar: "YEKNAL_CODEX_PARENT",
|
|
304
|
+
defaults: [path.join(home, ".codex")],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
label: "Claude",
|
|
308
|
+
envVar: "YEKNAL_CLAUDE_PARENT",
|
|
309
|
+
defaults: [path.join(home, ".claude")],
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const seenParents = new Set();
|
|
314
|
+
const targets = [];
|
|
315
|
+
|
|
316
|
+
for (const spec of targetSpecs) {
|
|
317
|
+
const overridePath = process.env[spec.envVar];
|
|
318
|
+
const candidates = overridePath ? [path.resolve(overridePath)] : spec.defaults;
|
|
319
|
+
|
|
320
|
+
for (const candidateParent of candidates) {
|
|
321
|
+
if (seenParents.has(candidateParent)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (await isDirectory(candidateParent)) {
|
|
326
|
+
seenParents.add(candidateParent);
|
|
327
|
+
targets.push({
|
|
328
|
+
label: spec.label,
|
|
329
|
+
parentPath: candidateParent,
|
|
330
|
+
skillsPath: path.join(candidateParent, "skills"),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return targets;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function runSkillsCommand() {
|
|
340
|
+
console.log("\nFetching available skill folders from GitHub...");
|
|
341
|
+
|
|
342
|
+
const targets = await resolveSkillTargets();
|
|
343
|
+
if (targets.length === 0) {
|
|
344
|
+
console.log("No supported parent folders found. Nothing to sync.");
|
|
345
|
+
console.log("Expected one or more of:");
|
|
346
|
+
console.log(" ~/.gemini/antigravity or ~/.antigravity");
|
|
347
|
+
console.log(" ~/.codex");
|
|
348
|
+
console.log(" ~/.claude\n");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`Detected ${targets.length} target parent folder(s):`);
|
|
353
|
+
for (const target of targets) {
|
|
354
|
+
console.log(` - ${target.label}: ${target.parentPath}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "yeknal-skills-"));
|
|
358
|
+
try {
|
|
359
|
+
let skillFolders = [];
|
|
360
|
+
let sourceLabel = "GitHub API + raw file download";
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const repoTree = await fetchRepoTree();
|
|
364
|
+
skillFolders = discoverSkillFolders(repoTree);
|
|
365
|
+
if (skillFolders.length === 0) {
|
|
366
|
+
throw new Error("No skill folders were discovered from the GitHub repository.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await downloadSkillsFromGit(tempRoot, skillFolders, repoTree);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const message = error && error.message ? error.message : String(error);
|
|
372
|
+
if (!message.includes("GitHub API rate limit exceeded")) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log("\nGitHub API rate-limited. Falling back to git clone source download...");
|
|
377
|
+
skillFolders = await stageSkillsFromGitClone(tempRoot);
|
|
378
|
+
sourceLabel = "git clone fallback";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(`\nSkill folders to sync (${skillFolders.length}) via ${sourceLabel}:`);
|
|
382
|
+
console.log(` ${skillFolders.join(", ")}`);
|
|
383
|
+
|
|
384
|
+
let hadFailure = false;
|
|
385
|
+
for (const target of targets) {
|
|
386
|
+
try {
|
|
387
|
+
await fsp.mkdir(target.skillsPath, { recursive: true });
|
|
388
|
+
let copiedCount = 0;
|
|
389
|
+
|
|
390
|
+
for (const folder of skillFolders) {
|
|
391
|
+
const sourceFolder = path.join(tempRoot, folder);
|
|
392
|
+
const destinationFolder = path.join(target.skillsPath, folder);
|
|
393
|
+
await fsp.rm(destinationFolder, { recursive: true, force: true });
|
|
394
|
+
await copyDirRecursive(sourceFolder, destinationFolder);
|
|
395
|
+
copiedCount += 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log(
|
|
399
|
+
`\n[ok] ${target.label}: synced ${copiedCount} folder(s) into ${target.skillsPath}`,
|
|
400
|
+
);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
hadFailure = true;
|
|
403
|
+
console.error(`\n[error] ${target.label}: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (hadFailure) {
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
console.error("\nSync completed with errors.");
|
|
410
|
+
} else {
|
|
411
|
+
console.log("\nSkills sync completed successfully.");
|
|
412
|
+
}
|
|
413
|
+
} finally {
|
|
414
|
+
await fsp.rm(tempRoot, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function runSecurityAudit() {
|
|
419
|
+
return new Promise((resolve) => {
|
|
420
|
+
console.log("Running security audit (this may take a moment)...");
|
|
421
|
+
exec("npx secure-repo audit", (error, stdout, stderr) => {
|
|
422
|
+
const logPath = path.join(process.cwd(), "security-audit.log");
|
|
423
|
+
|
|
424
|
+
const outputLines = stdout.split("\n");
|
|
425
|
+
const finalLines = [];
|
|
426
|
+
|
|
427
|
+
let inPolicySection = false;
|
|
428
|
+
let policyPasses = 0;
|
|
429
|
+
let policyWarnings = 0;
|
|
430
|
+
let policyIssues = 0;
|
|
431
|
+
let policyPoints = 0;
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < outputLines.length; i++) {
|
|
434
|
+
const line = outputLines[i];
|
|
435
|
+
|
|
436
|
+
if (line.match(/^\s*Policy files:/)) {
|
|
437
|
+
inPolicySection = true;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (inPolicySection) {
|
|
442
|
+
if (
|
|
443
|
+
line.match(/^\s*Environment files:/) ||
|
|
444
|
+
line.match(/^\s*Secret scanning:/) ||
|
|
445
|
+
line.match(/^\s*Configuration:/)
|
|
446
|
+
) {
|
|
447
|
+
inPolicySection = false;
|
|
448
|
+
} else {
|
|
449
|
+
if (line.includes("[FAIL]")) policyIssues += 1;
|
|
450
|
+
if (line.includes("[warn]")) policyWarnings += 1;
|
|
451
|
+
if (line.includes("[pass]")) {
|
|
452
|
+
policyPasses += 1;
|
|
453
|
+
if (
|
|
454
|
+
line.includes("SECURITY.md") ||
|
|
455
|
+
line.includes("AUTH.md") ||
|
|
456
|
+
line.includes("API.md") ||
|
|
457
|
+
line.includes("ENV_VARIABLES.md")
|
|
458
|
+
) {
|
|
459
|
+
policyPoints += 10;
|
|
460
|
+
} else {
|
|
461
|
+
policyPoints += 5;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (line.match(/^\s*Security Score:\s*\d+\s*\/\s*\d+/)) {
|
|
469
|
+
const match = line.match(/^\s*Security Score:\s*(\d+)/);
|
|
470
|
+
if (match) {
|
|
471
|
+
const oldScore = parseInt(match[1], 10);
|
|
472
|
+
const newScore = oldScore - policyPoints;
|
|
473
|
+
finalLines.push(` Security Score: ${newScore} / 45`);
|
|
474
|
+
} else {
|
|
475
|
+
finalLines.push(line);
|
|
476
|
+
}
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (line.match(/^\s*Results:\s*\d+\s*passed,\s*\d+\s*warnings,\s*\d+\s*issues/)) {
|
|
481
|
+
const match = line.match(
|
|
482
|
+
/Results:\s*(\d+)\s*passed,\s*(\d+)\s*warnings,\s*(\d+)\s*issues/,
|
|
483
|
+
);
|
|
484
|
+
if (match) {
|
|
485
|
+
const newPassed = parseInt(match[1], 10) - policyPasses;
|
|
486
|
+
const newWarnings = parseInt(match[2], 10) - policyWarnings;
|
|
487
|
+
const newIssues = parseInt(match[3], 10) - policyIssues;
|
|
488
|
+
finalLines.push(
|
|
489
|
+
` Results: ${newPassed} passed, ${newWarnings} warnings, ${newIssues} issues`,
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
finalLines.push(line);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (line.match(/^\s*\d+\s*issue\(s\)\s*found/)) {
|
|
498
|
+
const match = line.match(/^\s*(\d+)\s*issue\(s\)/);
|
|
499
|
+
if (match) {
|
|
500
|
+
const newIssues = parseInt(match[1], 10) - policyIssues;
|
|
501
|
+
finalLines.push(` ${newIssues} issue(s) found. Fix these before shipping.`);
|
|
502
|
+
} else {
|
|
503
|
+
finalLines.push(line);
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (line.includes("Run: npx secure-repo init") && line.includes("adds missing policy files")) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (line.includes("Want deeper coverage? The pro pack adds")) {
|
|
513
|
+
i += 2;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
finalLines.push(line);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const modifiedStdout = finalLines.join("\n");
|
|
521
|
+
const output = `--- Security Audit Log ---\nDate: ${new Date().toISOString()}\n\n${modifiedStdout}\n${
|
|
522
|
+
stderr ? `Errors/Warnings:\n${stderr}` : ""
|
|
523
|
+
}`;
|
|
524
|
+
|
|
525
|
+
fs.writeFileSync(logPath, output);
|
|
526
|
+
|
|
527
|
+
const totalNewIssuesRegex = /^\s*(\d+)\s*issue\(s\)\s*found/m;
|
|
528
|
+
const matchNewIssues = modifiedStdout.match(totalNewIssuesRegex);
|
|
529
|
+
let hasIssues = error !== null;
|
|
530
|
+
if (matchNewIssues) {
|
|
531
|
+
hasIssues = parseInt(matchNewIssues[1], 10) > 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (hasIssues) {
|
|
535
|
+
console.log("Security audit found issues (or returned an error code).");
|
|
536
|
+
console.log(`See ${logPath} for details.\n`);
|
|
537
|
+
} else {
|
|
538
|
+
console.log(`Security audit clean. Results saved to: ${logPath}\n`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
resolve();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function runSingleFileTemplateCommand(category) {
|
|
547
|
+
const config = singleFileConfigs[category];
|
|
548
|
+
const fileUrl = `${RAW_BASE_URL}/${config.remotePath}`;
|
|
549
|
+
const localDest = path.join(process.cwd(), config.localName);
|
|
550
|
+
|
|
551
|
+
console.log(`\nFetching ${category} guidelines...`);
|
|
552
|
+
await downloadUrlToFile(fileUrl, localDest);
|
|
553
|
+
console.log(`Saved to: ${localDest}\n`);
|
|
554
|
+
|
|
555
|
+
if (category === "security") {
|
|
556
|
+
await runSecurityAudit();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function main() {
|
|
561
|
+
const args = process.argv.slice(2);
|
|
562
|
+
const command = args[0] ? args[0].toLowerCase() : null;
|
|
563
|
+
|
|
564
|
+
if (!command) {
|
|
565
|
+
console.error("Error: Missing command.");
|
|
566
|
+
usage();
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (command === "skills") {
|
|
571
|
+
await runSkillsCommand();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (singleFileConfigs[command]) {
|
|
576
|
+
await runSingleFileTemplateCommand(command);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.error(`Error: Invalid command "${command}".`);
|
|
581
|
+
usage();
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
main().catch((error) => {
|
|
586
|
+
console.error(`\nError: ${error.message}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yeknal",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI to fetch
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "CLI to fetch markdown templates and sync AI agent skills",
|
|
5
5
|
"main": "bin/yeknal.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"yeknal": "bin/yeknal.js"
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"keywords": [
|
|
10
10
|
"cli",
|
|
11
11
|
"markdown",
|
|
12
|
-
"templates"
|
|
12
|
+
"templates",
|
|
13
|
+
"skills",
|
|
14
|
+
"ai-agents"
|
|
13
15
|
],
|
|
14
16
|
"author": "nubiaville",
|
|
15
17
|
"license": "ISC",
|