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.
Files changed (3) hide show
  1. package/README.md +96 -79
  2. package/bin/yeknal.js +588 -236
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -1,79 +1,96 @@
1
- # yeknal
2
-
3
- A CLI tool to instantly fetch project guideline templates (Security, Design, SEO) into any repository.
4
-
5
- ## Installation
6
-
7
- No installation needed — just use `npx`:
8
-
9
- ```bash
10
- npx yeknal <category>
11
- ```
12
-
13
- ## Usage
14
-
15
- Run any of the following commands from the root of your project:
16
-
17
- ### Security
18
-
19
- Fetches the **Security-Master.md** guideline and automatically runs a security audit on your project, saving the results to `security-audit.log`.
20
-
21
- ```bash
22
- npx yeknal security
23
- ```
24
-
25
- **Output files:**
26
- - `Security-Master.md` — Security best practices and policies
27
- - `security-audit.log` — Automated audit results for your project
28
-
29
- ---
30
-
31
- ### Design
32
-
33
- Fetches the **Design.md** guideline with UI/UX design principles and standards.
34
-
35
- ```bash
36
- npx yeknal design
37
- ```
38
-
39
- **Output files:**
40
- - `Design.md` — Design system guidelines
41
-
42
- ---
43
-
44
- ### SEO
45
-
46
- Fetches the **SEO-Prompt.md** guideline with SEO improvement strategies.
47
-
48
- ```bash
49
- npx yeknal seo
50
- ```
51
-
52
- **Output files:**
53
- - `SEO-Prompt.md` SEO checklist and improvement prompts
54
-
55
- ## Examples
56
-
57
- ```bash
58
- # Navigate to any project
59
- cd my-project
60
-
61
- # Pull security guidelines + run audit
62
- npx yeknal security
63
-
64
- # Pull design guidelines
65
- npx yeknal design
66
-
67
- # Pull SEO guidelines
68
- npx yeknal seo
69
- ```
70
-
71
- ## How It Works
72
-
73
- 1. The CLI fetches the requested markdown file from a remote GitHub repository.
74
- 2. The file is saved directly into your current working directory.
75
- 3. For `security`, it additionally runs `npx secure-repo audit` and logs the results.
76
-
77
- ## License
78
-
79
- ISC
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 Markdown files from a GitHub repository to the current working directory.
6
- */
7
-
8
- const fs = require("fs");
9
- const path = require("path");
10
- const https = require("https");
11
- const { exec } = require("child_process");
12
-
13
- // ==========================================
14
- // USER CONFIGURATION (UPDATE THESE VALUES)
15
- // ==========================================
16
- const GITHUB_USERNAME = "tryraisins"; // e.g., 'your-github-username'
17
- const GITHUB_REPO = "MD_Files"; // e.g., 'your-repo-name'
18
- const BRANCH = "main"; // e.g., 'main' or 'master'
19
- // ==========================================
20
-
21
- // Base URL for GitHub raw content
22
- const BASE_URL = `https://raw.githubusercontent.com/${GITHUB_USERNAME}/${GITHUB_REPO}/${BRANCH}`;
23
-
24
- // Map commands to specific file paths in the GitHub repository
25
- const configs = {
26
- security: {
27
- remotePath: "Security/Security-Master.md", // Path in your repo
28
- localName: "Security-Master.md", // Name saved locally
29
- },
30
- design: {
31
- remotePath: "Design/SKILL.md",
32
- localName: "Design.md",
33
- },
34
- seo: {
35
- remotePath: "SEO/seo-improvement-prompt.md",
36
- localName: "SEO-Prompt.md",
37
- },
38
- };
39
-
40
- const args = process.argv.slice(2);
41
- const category = args[0] ? args[0].toLowerCase() : null;
42
-
43
- if (!category || !configs[category]) {
44
- console.error("❌ Error: Invalid or missing category.");
45
- console.log("\nUsage:");
46
- console.log(" npx yeknal security");
47
- console.log(" npx yeknal design");
48
- console.log(" npx yeknal seo\n");
49
- process.exit(1);
50
- }
51
-
52
- const config = configs[category];
53
- const fileUrl = `${BASE_URL}/${config.remotePath}`;
54
- const localDest = path.join(process.cwd(), config.localName);
55
-
56
- console.log(`\n⏳ Fetching ${category} guidelines...`);
57
-
58
- https
59
- .get(fileUrl, (res) => {
60
- if (res.statusCode !== 200) {
61
- console.error(
62
- `\n❌ Failed to download file. (HTTP Status: ${res.statusCode})`,
63
- );
64
- console.error(`Please verify that the file exists at:\n👉 ${fileUrl}\n`);
65
- console.error(
66
- `If the repository is private, this script needs a Personal Access Token.`,
67
- );
68
- process.exit(1);
69
- }
70
-
71
- const fileStream = fs.createWriteStream(localDest);
72
- res.pipe(fileStream);
73
-
74
- fileStream.on("finish", () => {
75
- fileStream.close();
76
- console.log(`✅ Successfully saved to: ${localDest}\n`);
77
-
78
- if (category === "security") {
79
- console.log(`🔍 Running security audit (this may take a moment)...`);
80
- exec("npx secure-repo audit", (error, stdout, stderr) => {
81
- const logPath = path.join(process.cwd(), "security-audit.log");
82
-
83
- // Parse stdout to remove Policy files and recalculate scores
84
- const outputLines = stdout.split("\n");
85
- const finalLines = [];
86
-
87
- let inPolicySection = false;
88
- let policyPasses = 0;
89
- let policyWarnings = 0;
90
- let policyIssues = 0;
91
- let policyPoints = 0;
92
-
93
- for (let i = 0; i < outputLines.length; i++) {
94
- const line = outputLines[i];
95
-
96
- if (line.match(/^\s*Policy files:/)) {
97
- inPolicySection = true;
98
- continue;
99
- }
100
-
101
- if (inPolicySection) {
102
- if (
103
- line.match(/^\s*Environment files:/) ||
104
- line.match(/^\s*Secret scanning:/) ||
105
- line.match(/^\s*Configuration:/)
106
- ) {
107
- inPolicySection = false;
108
- } else {
109
- if (line.includes("[FAIL]")) policyIssues++;
110
- if (line.includes("[warn]")) policyWarnings++;
111
- if (line.includes("[pass]")) {
112
- policyPasses++;
113
- if (
114
- line.includes("SECURITY.md") ||
115
- line.includes("AUTH.md") ||
116
- line.includes("API.md") ||
117
- line.includes("ENV_VARIABLES.md")
118
- ) {
119
- policyPoints += 10;
120
- } else {
121
- policyPoints += 5;
122
- }
123
- }
124
- continue;
125
- }
126
- }
127
-
128
- if (line.match(/^\s*Security Score:\s*\d+\s*\/\s*\d+/)) {
129
- const match = line.match(/^\s*Security Score:\s*(\d+)/);
130
- if (match) {
131
- const oldScore = parseInt(match[1], 10);
132
- const newScore = oldScore - policyPoints;
133
- finalLines.push(` Security Score: ${newScore} / 45`);
134
- } else {
135
- finalLines.push(line);
136
- }
137
- continue;
138
- }
139
-
140
- if (
141
- line.match(
142
- /^\s*Results:\s*\d+\s*passed,\s*\d+\s*warnings,\s*\d+\s*issues/,
143
- )
144
- ) {
145
- const match = line.match(
146
- /Results:\s*(\d+)\s*passed,\s*(\d+)\s*warnings,\s*(\d+)\s*issues/,
147
- );
148
- if (match) {
149
- const newPassed = parseInt(match[1], 10) - policyPasses;
150
- const newWarnings = parseInt(match[2], 10) - policyWarnings;
151
- const newIssues = parseInt(match[3], 10) - policyIssues;
152
- finalLines.push(
153
- ` Results: ${newPassed} passed, ${newWarnings} warnings, ${newIssues} issues`,
154
- );
155
- } else {
156
- finalLines.push(line);
157
- }
158
- continue;
159
- }
160
-
161
- if (line.match(/^\s*\d+\s*issue\(s\)\s*found/)) {
162
- const match = line.match(/^\s*(\d+)\s*issue\(s\)/);
163
- if (match) {
164
- const newIssues = parseInt(match[1], 10) - policyIssues;
165
- finalLines.push(
166
- ` ${newIssues} issue(s) found. Fix these before shipping.`,
167
- );
168
- } else {
169
- finalLines.push(line);
170
- }
171
- continue;
172
- }
173
-
174
- if (
175
- line.includes("Run: npx secure-repo init") &&
176
- line.includes("adds missing policy files")
177
- ) {
178
- continue;
179
- }
180
-
181
- if (line.includes("Want deeper coverage? The pro pack adds")) {
182
- i += 2; // skip this and next 2 lines
183
- continue;
184
- }
185
-
186
- if (line.includes("────────────────────────────────────")) {
187
- // there are multiple of these, we drop the one used for the pro upsell if the previous lines were the pro upsell
188
- // To be safe we'll leave it, the upsell message is dropped.
189
- // Actually, the previous line is "Run: npx secure-repo init", then "────────────────────────────────────", then "Want deeper coverage?"
190
- // So if we see "────────────────────────────────────" and the NEXT line is "Want deeper coverage?", we skip both.
191
- if (
192
- i + 1 < outputLines.length &&
193
- outputLines[i + 1].includes(
194
- "Want deeper coverage? The pro pack adds",
195
- )
196
- ) {
197
- i += 3;
198
- continue;
199
- }
200
- }
201
-
202
- finalLines.push(line);
203
- }
204
-
205
- const modifiedStdout = finalLines.join("\n");
206
-
207
- const output = `--- Security Audit Log ---\nDate: ${new Date().toISOString()}\n\n${modifiedStdout}\n${stderr ? "Errors/Warnings:\n" + stderr : ""}`;
208
-
209
- fs.writeFileSync(logPath, output);
210
-
211
- // We adjust the error logging checking new issues count
212
- const totalNewIssuesRegex = /^\s*(\d+)\s*issue\(s\)\s*found/m;
213
- const matchNewIssues = modifiedStdout.match(totalNewIssuesRegex);
214
- let hasIssues = error !== null;
215
- if (matchNewIssues) {
216
- hasIssues = parseInt(matchNewIssues[1], 10) > 0;
217
- }
218
-
219
- if (hasIssues) {
220
- console.log(
221
- `⚠️ Security audit found issues (or returned an error code).`,
222
- );
223
- console.log(`👉 See ${logPath} for details.\n`);
224
- } else {
225
- console.log(
226
- `✅ Security audit clean. Results saved to: ${logPath}\n`,
227
- );
228
- }
229
- });
230
- }
231
- });
232
- })
233
- .on("error", (err) => {
234
- console.error("\n❌ Network error fetching file:", err.message);
235
- process.exit(1);
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.5",
4
- "description": "CLI to fetch MD templates for security, design, and seo",
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",