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.
Files changed (3) hide show
  1. package/README.md +93 -79
  2. package/bin/yeknal.js +484 -236
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -1,79 +1,93 @@
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-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 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 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.5",
4
- "description": "CLI to fetch MD templates for security, design, and seo",
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",