yeknal 1.0.5 → 1.0.6
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 +93 -79
- package/bin/yeknal.js +484 -236
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,79 +1,93 @@
|
|
|
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-only.
|
|
61
|
+
- Top-level folders are included only if they contain `SKILL.md`.
|
|
62
|
+
- Excludes `Design`, `Security`, `Security_Raw`, and `SEO`.
|
|
63
|
+
- Sync targets (if parent folder exists):
|
|
64
|
+
- Gemini: `~/.gemini/antigravity` or `~/.antigravity`
|
|
65
|
+
- Codex: `~/.codex`
|
|
66
|
+
- Claude: `~/.claude`
|
|
67
|
+
- For each detected target, creates `<parent>/skills` if missing.
|
|
68
|
+
- Overwrites destination skill folders to keep targets in sync.
|
|
69
|
+
|
|
70
|
+
Optional environment variable overrides:
|
|
71
|
+
- `YEKNAL_GEMINI_PARENT`
|
|
72
|
+
- `YEKNAL_CODEX_PARENT`
|
|
73
|
+
- `YEKNAL_CLAUDE_PARENT`
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Pull security guidelines + run audit
|
|
79
|
+
npx yeknal security
|
|
80
|
+
|
|
81
|
+
# Pull design guidelines
|
|
82
|
+
npx yeknal design
|
|
83
|
+
|
|
84
|
+
# Pull SEO guidelines
|
|
85
|
+
npx yeknal seo
|
|
86
|
+
|
|
87
|
+
# Sync skill folders for Gemini/Codex/Claude
|
|
88
|
+
npx yeknal skills
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
ISC
|
package/bin/yeknal.js
CHANGED
|
@@ -1,236 +1,484 @@
|
|
|
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
|
-
console.log("
|
|
47
|
-
console.log(" npx yeknal
|
|
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 CONTENTS_API_BASE = `https://api.github.com/repos/${GITHUB_USERNAME}/${GITHUB_REPO}/contents`;
|
|
27
|
+
|
|
28
|
+
const EXCLUDED_SKILL_FOLDERS = new Set(["Design", "Security", "Security_Raw", "SEO"]);
|
|
29
|
+
|
|
30
|
+
const singleFileConfigs = {
|
|
31
|
+
security: {
|
|
32
|
+
remotePath: "Security/Security-Master.md",
|
|
33
|
+
localName: "Security-Master.md",
|
|
34
|
+
},
|
|
35
|
+
design: {
|
|
36
|
+
remotePath: "Design/SKILL.md",
|
|
37
|
+
localName: "Design.md",
|
|
38
|
+
},
|
|
39
|
+
seo: {
|
|
40
|
+
remotePath: "SEO/seo-improvement-prompt.md",
|
|
41
|
+
localName: "SEO-Prompt.md",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function usage() {
|
|
46
|
+
console.log("\nUsage:");
|
|
47
|
+
console.log(" npx yeknal security");
|
|
48
|
+
console.log(" npx yeknal design");
|
|
49
|
+
console.log(" npx yeknal seo");
|
|
50
|
+
console.log(" npx yeknal skills\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isHttpSuccess(statusCode) {
|
|
54
|
+
return typeof statusCode === "number" && statusCode >= 200 && statusCode < 300;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requestBuffer(url, redirectsRemaining = 5) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const req = https.get(
|
|
60
|
+
url,
|
|
61
|
+
{
|
|
62
|
+
headers: {
|
|
63
|
+
"User-Agent": "yeknal-cli",
|
|
64
|
+
Accept: "application/vnd.github+json",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
(res) => {
|
|
68
|
+
const statusCode = res.statusCode || 0;
|
|
69
|
+
|
|
70
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && res.headers.location) {
|
|
71
|
+
if (redirectsRemaining <= 0) {
|
|
72
|
+
reject(new Error(`Too many redirects requesting ${url}`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
res.resume();
|
|
76
|
+
requestBuffer(res.headers.location, redirectsRemaining - 1).then(resolve).catch(reject);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const chunks = [];
|
|
81
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
82
|
+
res.on("end", () => {
|
|
83
|
+
resolve({
|
|
84
|
+
statusCode,
|
|
85
|
+
body: Buffer.concat(chunks),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
req.on("error", reject);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function fetchJson(url) {
|
|
96
|
+
const response = await requestBuffer(url);
|
|
97
|
+
if (!isHttpSuccess(response.statusCode)) {
|
|
98
|
+
const bodyText = response.body.toString("utf8");
|
|
99
|
+
throw new Error(`GitHub API request failed (${response.statusCode}): ${url}\n${bodyText}`);
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(response.body.toString("utf8"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function downloadUrlToFile(url, localPath) {
|
|
105
|
+
const response = await requestBuffer(url);
|
|
106
|
+
if (!isHttpSuccess(response.statusCode)) {
|
|
107
|
+
throw new Error(`Failed to download file (${response.statusCode}): ${url}`);
|
|
108
|
+
}
|
|
109
|
+
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
110
|
+
await fsp.writeFile(localPath, response.body);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildContentsApiUrl(repoPath) {
|
|
114
|
+
const encoded = repoPath
|
|
115
|
+
.split("/")
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map((segment) => encodeURIComponent(segment))
|
|
118
|
+
.join("/");
|
|
119
|
+
const endpoint = encoded ? `/${encoded}` : "";
|
|
120
|
+
return `${CONTENTS_API_BASE}${endpoint}?ref=${encodeURIComponent(BRANCH)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function listRepoPath(repoPath) {
|
|
124
|
+
const data = await fetchJson(buildContentsApiUrl(repoPath));
|
|
125
|
+
return Array.isArray(data) ? data : [data];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function folderHasTopLevelSkillFile(folderName) {
|
|
129
|
+
const entries = await listRepoPath(folderName);
|
|
130
|
+
return entries.some((entry) => entry.type === "file" && entry.name === "SKILL.md");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function discoverSkillFolders() {
|
|
134
|
+
const rootEntries = await listRepoPath("");
|
|
135
|
+
const candidateDirs = rootEntries.filter(
|
|
136
|
+
(entry) => entry.type === "dir" && !EXCLUDED_SKILL_FOLDERS.has(entry.name),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const checks = await Promise.all(
|
|
140
|
+
candidateDirs.map(async (dir) => ({
|
|
141
|
+
name: dir.name,
|
|
142
|
+
hasSkillFile: await folderHasTopLevelSkillFile(dir.name),
|
|
143
|
+
})),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return checks
|
|
147
|
+
.filter((entry) => entry.hasSkillFile)
|
|
148
|
+
.map((entry) => entry.name)
|
|
149
|
+
.sort((a, b) => a.localeCompare(b));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function downloadRepoFolderRecursive(repoPath, localFolderPath) {
|
|
153
|
+
const entries = await listRepoPath(repoPath);
|
|
154
|
+
await fsp.mkdir(localFolderPath, { recursive: true });
|
|
155
|
+
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const nextLocalPath = path.join(localFolderPath, entry.name);
|
|
158
|
+
if (entry.type === "dir") {
|
|
159
|
+
await downloadRepoFolderRecursive(entry.path, nextLocalPath);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (entry.type === "file") {
|
|
164
|
+
if (!entry.download_url) {
|
|
165
|
+
throw new Error(`Missing download URL for repo file: ${entry.path}`);
|
|
166
|
+
}
|
|
167
|
+
await downloadUrlToFile(entry.download_url, nextLocalPath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function copyDirRecursive(sourceDir, targetDir) {
|
|
173
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
174
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
175
|
+
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
178
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
179
|
+
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
await copyDirRecursive(sourcePath, targetPath);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (entry.isFile()) {
|
|
186
|
+
await fsp.copyFile(sourcePath, targetPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function isDirectory(filePath) {
|
|
192
|
+
try {
|
|
193
|
+
const stats = await fsp.stat(filePath);
|
|
194
|
+
return stats.isDirectory();
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function resolveSkillTargets() {
|
|
201
|
+
const home = os.homedir();
|
|
202
|
+
|
|
203
|
+
const targetSpecs = [
|
|
204
|
+
{
|
|
205
|
+
label: "Gemini Antigravity",
|
|
206
|
+
envVar: "YEKNAL_GEMINI_PARENT",
|
|
207
|
+
defaults: [path.join(home, ".gemini", "antigravity"), path.join(home, ".antigravity")],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: "Codex",
|
|
211
|
+
envVar: "YEKNAL_CODEX_PARENT",
|
|
212
|
+
defaults: [path.join(home, ".codex")],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: "Claude",
|
|
216
|
+
envVar: "YEKNAL_CLAUDE_PARENT",
|
|
217
|
+
defaults: [path.join(home, ".claude")],
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const seenParents = new Set();
|
|
222
|
+
const targets = [];
|
|
223
|
+
|
|
224
|
+
for (const spec of targetSpecs) {
|
|
225
|
+
const overridePath = process.env[spec.envVar];
|
|
226
|
+
const candidates = overridePath ? [path.resolve(overridePath)] : spec.defaults;
|
|
227
|
+
|
|
228
|
+
for (const candidateParent of candidates) {
|
|
229
|
+
if (seenParents.has(candidateParent)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (await isDirectory(candidateParent)) {
|
|
234
|
+
seenParents.add(candidateParent);
|
|
235
|
+
targets.push({
|
|
236
|
+
label: spec.label,
|
|
237
|
+
parentPath: candidateParent,
|
|
238
|
+
skillsPath: path.join(candidateParent, "skills"),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return targets;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function runSkillsCommand() {
|
|
248
|
+
console.log("\nFetching available skill folders from GitHub...");
|
|
249
|
+
|
|
250
|
+
const targets = await resolveSkillTargets();
|
|
251
|
+
if (targets.length === 0) {
|
|
252
|
+
console.log("No supported parent folders found. Nothing to sync.");
|
|
253
|
+
console.log("Expected one or more of:");
|
|
254
|
+
console.log(" ~/.gemini/antigravity or ~/.antigravity");
|
|
255
|
+
console.log(" ~/.codex");
|
|
256
|
+
console.log(" ~/.claude\n");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(`Detected ${targets.length} target parent folder(s):`);
|
|
261
|
+
for (const target of targets) {
|
|
262
|
+
console.log(` - ${target.label}: ${target.parentPath}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const skillFolders = await discoverSkillFolders();
|
|
266
|
+
if (skillFolders.length === 0) {
|
|
267
|
+
throw new Error("No skill folders were discovered from the GitHub repository.");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`\nSkill folders to sync (${skillFolders.length}):`);
|
|
271
|
+
console.log(` ${skillFolders.join(", ")}`);
|
|
272
|
+
|
|
273
|
+
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "yeknal-skills-"));
|
|
274
|
+
try {
|
|
275
|
+
for (const folder of skillFolders) {
|
|
276
|
+
const localFolder = path.join(tempRoot, folder);
|
|
277
|
+
await downloadRepoFolderRecursive(folder, localFolder);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let hadFailure = false;
|
|
281
|
+
for (const target of targets) {
|
|
282
|
+
try {
|
|
283
|
+
await fsp.mkdir(target.skillsPath, { recursive: true });
|
|
284
|
+
let copiedCount = 0;
|
|
285
|
+
|
|
286
|
+
for (const folder of skillFolders) {
|
|
287
|
+
const sourceFolder = path.join(tempRoot, folder);
|
|
288
|
+
const destinationFolder = path.join(target.skillsPath, folder);
|
|
289
|
+
await fsp.rm(destinationFolder, { recursive: true, force: true });
|
|
290
|
+
await copyDirRecursive(sourceFolder, destinationFolder);
|
|
291
|
+
copiedCount += 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(
|
|
295
|
+
`\n[ok] ${target.label}: synced ${copiedCount} folder(s) into ${target.skillsPath}`,
|
|
296
|
+
);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
hadFailure = true;
|
|
299
|
+
console.error(`\n[error] ${target.label}: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (hadFailure) {
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
console.error("\nSync completed with errors.");
|
|
306
|
+
} else {
|
|
307
|
+
console.log("\nSkills sync completed successfully.");
|
|
308
|
+
}
|
|
309
|
+
} finally {
|
|
310
|
+
await fsp.rm(tempRoot, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runSecurityAudit() {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
console.log("Running security audit (this may take a moment)...");
|
|
317
|
+
exec("npx secure-repo audit", (error, stdout, stderr) => {
|
|
318
|
+
const logPath = path.join(process.cwd(), "security-audit.log");
|
|
319
|
+
|
|
320
|
+
const outputLines = stdout.split("\n");
|
|
321
|
+
const finalLines = [];
|
|
322
|
+
|
|
323
|
+
let inPolicySection = false;
|
|
324
|
+
let policyPasses = 0;
|
|
325
|
+
let policyWarnings = 0;
|
|
326
|
+
let policyIssues = 0;
|
|
327
|
+
let policyPoints = 0;
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < outputLines.length; i++) {
|
|
330
|
+
const line = outputLines[i];
|
|
331
|
+
|
|
332
|
+
if (line.match(/^\s*Policy files:/)) {
|
|
333
|
+
inPolicySection = true;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (inPolicySection) {
|
|
338
|
+
if (
|
|
339
|
+
line.match(/^\s*Environment files:/) ||
|
|
340
|
+
line.match(/^\s*Secret scanning:/) ||
|
|
341
|
+
line.match(/^\s*Configuration:/)
|
|
342
|
+
) {
|
|
343
|
+
inPolicySection = false;
|
|
344
|
+
} else {
|
|
345
|
+
if (line.includes("[FAIL]")) policyIssues += 1;
|
|
346
|
+
if (line.includes("[warn]")) policyWarnings += 1;
|
|
347
|
+
if (line.includes("[pass]")) {
|
|
348
|
+
policyPasses += 1;
|
|
349
|
+
if (
|
|
350
|
+
line.includes("SECURITY.md") ||
|
|
351
|
+
line.includes("AUTH.md") ||
|
|
352
|
+
line.includes("API.md") ||
|
|
353
|
+
line.includes("ENV_VARIABLES.md")
|
|
354
|
+
) {
|
|
355
|
+
policyPoints += 10;
|
|
356
|
+
} else {
|
|
357
|
+
policyPoints += 5;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (line.match(/^\s*Security Score:\s*\d+\s*\/\s*\d+/)) {
|
|
365
|
+
const match = line.match(/^\s*Security Score:\s*(\d+)/);
|
|
366
|
+
if (match) {
|
|
367
|
+
const oldScore = parseInt(match[1], 10);
|
|
368
|
+
const newScore = oldScore - policyPoints;
|
|
369
|
+
finalLines.push(` Security Score: ${newScore} / 45`);
|
|
370
|
+
} else {
|
|
371
|
+
finalLines.push(line);
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (line.match(/^\s*Results:\s*\d+\s*passed,\s*\d+\s*warnings,\s*\d+\s*issues/)) {
|
|
377
|
+
const match = line.match(
|
|
378
|
+
/Results:\s*(\d+)\s*passed,\s*(\d+)\s*warnings,\s*(\d+)\s*issues/,
|
|
379
|
+
);
|
|
380
|
+
if (match) {
|
|
381
|
+
const newPassed = parseInt(match[1], 10) - policyPasses;
|
|
382
|
+
const newWarnings = parseInt(match[2], 10) - policyWarnings;
|
|
383
|
+
const newIssues = parseInt(match[3], 10) - policyIssues;
|
|
384
|
+
finalLines.push(
|
|
385
|
+
` Results: ${newPassed} passed, ${newWarnings} warnings, ${newIssues} issues`,
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
finalLines.push(line);
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (line.match(/^\s*\d+\s*issue\(s\)\s*found/)) {
|
|
394
|
+
const match = line.match(/^\s*(\d+)\s*issue\(s\)/);
|
|
395
|
+
if (match) {
|
|
396
|
+
const newIssues = parseInt(match[1], 10) - policyIssues;
|
|
397
|
+
finalLines.push(` ${newIssues} issue(s) found. Fix these before shipping.`);
|
|
398
|
+
} else {
|
|
399
|
+
finalLines.push(line);
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (line.includes("Run: npx secure-repo init") && line.includes("adds missing policy files")) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (line.includes("Want deeper coverage? The pro pack adds")) {
|
|
409
|
+
i += 2;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
finalLines.push(line);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const modifiedStdout = finalLines.join("\n");
|
|
417
|
+
const output = `--- Security Audit Log ---\nDate: ${new Date().toISOString()}\n\n${modifiedStdout}\n${
|
|
418
|
+
stderr ? `Errors/Warnings:\n${stderr}` : ""
|
|
419
|
+
}`;
|
|
420
|
+
|
|
421
|
+
fs.writeFileSync(logPath, output);
|
|
422
|
+
|
|
423
|
+
const totalNewIssuesRegex = /^\s*(\d+)\s*issue\(s\)\s*found/m;
|
|
424
|
+
const matchNewIssues = modifiedStdout.match(totalNewIssuesRegex);
|
|
425
|
+
let hasIssues = error !== null;
|
|
426
|
+
if (matchNewIssues) {
|
|
427
|
+
hasIssues = parseInt(matchNewIssues[1], 10) > 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (hasIssues) {
|
|
431
|
+
console.log("Security audit found issues (or returned an error code).");
|
|
432
|
+
console.log(`See ${logPath} for details.\n`);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`Security audit clean. Results saved to: ${logPath}\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function runSingleFileTemplateCommand(category) {
|
|
443
|
+
const config = singleFileConfigs[category];
|
|
444
|
+
const fileUrl = `${RAW_BASE_URL}/${config.remotePath}`;
|
|
445
|
+
const localDest = path.join(process.cwd(), config.localName);
|
|
446
|
+
|
|
447
|
+
console.log(`\nFetching ${category} guidelines...`);
|
|
448
|
+
await downloadUrlToFile(fileUrl, localDest);
|
|
449
|
+
console.log(`Saved to: ${localDest}\n`);
|
|
450
|
+
|
|
451
|
+
if (category === "security") {
|
|
452
|
+
await runSecurityAudit();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function main() {
|
|
457
|
+
const args = process.argv.slice(2);
|
|
458
|
+
const command = args[0] ? args[0].toLowerCase() : null;
|
|
459
|
+
|
|
460
|
+
if (!command) {
|
|
461
|
+
console.error("Error: Missing command.");
|
|
462
|
+
usage();
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (command === "skills") {
|
|
467
|
+
await runSkillsCommand();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (singleFileConfigs[command]) {
|
|
472
|
+
await runSingleFileTemplateCommand(command);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.error(`Error: Invalid command "${command}".`);
|
|
477
|
+
usage();
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
main().catch((error) => {
|
|
482
|
+
console.error(`\nError: ${error.message}`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
});
|
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.6",
|
|
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",
|