@valentia-ai-skills/framework 1.0.3 → 1.0.5
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/bin/cli.js +380 -285
- package/package.json +2 -2
package/bin/cli.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* ai-skills CLI
|
|
5
|
-
*
|
|
4
|
+
* ai-skills CLI — @valentia-ai-skills/framework
|
|
5
|
+
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx ai-skills setup
|
|
8
|
-
* npx ai-skills update
|
|
9
|
-
* npx ai-skills status
|
|
10
|
-
* npx ai-skills list
|
|
11
|
-
* npx ai-skills
|
|
12
|
-
* npx ai-skills remove <skill> # Remove a specific skill
|
|
13
|
-
* npx ai-skills doctor # Check setup health
|
|
7
|
+
* npx ai-skills setup # Enter email → lookup team → install team's skills
|
|
8
|
+
* npx ai-skills update # Re-fetch skills from Supabase for your team
|
|
9
|
+
* npx ai-skills status # Show installed skills and team info
|
|
10
|
+
* npx ai-skills list # List all locally available skills
|
|
11
|
+
* npx ai-skills doctor # Health check
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
const fs = require("fs");
|
|
17
15
|
const path = require("path");
|
|
16
|
+
const readline = require("readline");
|
|
17
|
+
const https = require("https");
|
|
18
|
+
const http = require("http");
|
|
18
19
|
|
|
19
20
|
// ── Constants ──
|
|
20
21
|
|
|
21
|
-
const SKILLS_SOURCE = path.join(__dirname, "..", "skills");
|
|
22
|
+
const SKILLS_SOURCE = path.join(__dirname, "..", "skills"); // fallback local skills
|
|
22
23
|
const PROJECT_ROOT = process.cwd();
|
|
24
|
+
const CONFIG_PATH = path.join(PROJECT_ROOT, ".ai-skills.json");
|
|
25
|
+
|
|
26
|
+
// UPDATE THIS with your actual Supabase project URL
|
|
27
|
+
const SUPABASE_FUNCTION_URL =
|
|
28
|
+
process.env.AI_SKILLS_API_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/lookup-team";
|
|
23
29
|
|
|
24
30
|
const TOOL_CONFIGS = {
|
|
25
31
|
"claude-code": {
|
|
@@ -27,7 +33,7 @@ const TOOL_CONFIGS = {
|
|
|
27
33
|
skillsDir: ".claude/skills",
|
|
28
34
|
rulesFile: null,
|
|
29
35
|
detect: () => fs.existsSync(path.join(PROJECT_ROOT, ".claude")) || shellHas("claude"),
|
|
30
|
-
format: "skill-folder",
|
|
36
|
+
format: "skill-folder",
|
|
31
37
|
},
|
|
32
38
|
cursor: {
|
|
33
39
|
name: "Cursor",
|
|
@@ -36,7 +42,7 @@ const TOOL_CONFIGS = {
|
|
|
36
42
|
detect: () =>
|
|
37
43
|
fs.existsSync(path.join(PROJECT_ROOT, ".cursor")) ||
|
|
38
44
|
fs.existsSync(path.join(PROJECT_ROOT, ".cursorules")),
|
|
39
|
-
format: "rules-file",
|
|
45
|
+
format: "rules-file",
|
|
40
46
|
},
|
|
41
47
|
copilot: {
|
|
42
48
|
name: "GitHub Copilot",
|
|
@@ -54,362 +60,448 @@ const TOOL_CONFIGS = {
|
|
|
54
60
|
detect: () => fs.existsSync(path.join(PROJECT_ROOT, ".windsurfrules")),
|
|
55
61
|
format: "rules-file",
|
|
56
62
|
},
|
|
57
|
-
generic: {
|
|
58
|
-
name: "Generic (.ai-rules)",
|
|
59
|
-
skillsDir: null,
|
|
60
|
-
rulesFile: ".ai-rules",
|
|
61
|
-
detect: () => true, // always available as fallback
|
|
62
|
-
format: "rules-file",
|
|
63
|
-
},
|
|
64
63
|
};
|
|
65
64
|
|
|
66
|
-
const
|
|
67
|
-
green: "\x1b[32m",
|
|
68
|
-
|
|
69
|
-
blue: "\x1b[34m",
|
|
70
|
-
red: "\x1b[31m",
|
|
71
|
-
dim: "\x1b[2m",
|
|
72
|
-
bold: "\x1b[1m",
|
|
73
|
-
reset: "\x1b[0m",
|
|
65
|
+
const C = {
|
|
66
|
+
green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m",
|
|
67
|
+
red: "\x1b[31m", dim: "\x1b[2m", bold: "\x1b[1m", reset: "\x1b[0m",
|
|
74
68
|
};
|
|
75
69
|
|
|
76
70
|
// ── Helpers ──
|
|
77
71
|
|
|
78
|
-
function c(color, text) {
|
|
79
|
-
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
80
|
-
}
|
|
72
|
+
function c(color, text) { return `${C[color]}${text}${C.reset}`; }
|
|
81
73
|
|
|
82
74
|
function shellHas(cmd) {
|
|
83
|
-
try {
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
} catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
75
|
+
try { require("child_process").execSync(`which ${cmd} 2>/dev/null`, { stdio: "ignore" }); return true; }
|
|
76
|
+
catch { return false; }
|
|
89
77
|
}
|
|
90
78
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
function mkdirp(dirPath) {
|
|
80
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
81
|
+
}
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
83
|
+
function ask(question) {
|
|
84
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
function fetchJSON(url, body) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
const mod = parsed.protocol === "https:" ? https : http;
|
|
94
|
+
const postData = JSON.stringify(body);
|
|
95
|
+
|
|
96
|
+
const req = mod.request(
|
|
97
|
+
{
|
|
98
|
+
hostname: parsed.hostname,
|
|
99
|
+
port: parsed.port,
|
|
100
|
+
path: parsed.pathname + parsed.search,
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
(res) => {
|
|
108
|
+
let data = "";
|
|
109
|
+
res.on("data", (chunk) => (data += chunk));
|
|
110
|
+
res.on("end", () => {
|
|
111
|
+
try {
|
|
112
|
+
const json = JSON.parse(data);
|
|
113
|
+
if (res.statusCode >= 400) {
|
|
114
|
+
reject(new Error(json.error || `HTTP ${res.statusCode}`));
|
|
115
|
+
} else {
|
|
116
|
+
resolve(json);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
102
122
|
}
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
);
|
|
124
|
+
req.on("error", reject);
|
|
125
|
+
req.write(postData);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
skills.push({ name: item, category, path: itemPath });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return skills;
|
|
130
|
+
function loadConfig() {
|
|
131
|
+
if (fs.existsSync(CONFIG_PATH)) return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
132
|
+
return null;
|
|
114
133
|
}
|
|
115
134
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
if (!fs.existsSync(skillMd)) return null;
|
|
119
|
-
return fs.readFileSync(skillMd, "utf-8");
|
|
135
|
+
function saveConfig(config) {
|
|
136
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
139
|
+
function detectTools() {
|
|
140
|
+
const detected = [];
|
|
141
|
+
for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
|
|
142
|
+
if (config.detect()) detected.push(key);
|
|
143
|
+
}
|
|
144
|
+
return detected;
|
|
126
145
|
}
|
|
127
146
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
function installSkillsForTool(toolKey, skills) {
|
|
148
|
+
const tool = TOOL_CONFIGS[toolKey];
|
|
149
|
+
if (!tool) return 0;
|
|
150
|
+
|
|
151
|
+
if (tool.format === "skill-folder") {
|
|
152
|
+
const skillsDir = path.join(PROJECT_ROOT, tool.skillsDir);
|
|
153
|
+
for (const skill of skills) {
|
|
154
|
+
const dest = path.join(skillsDir, skill.name);
|
|
155
|
+
mkdirp(dest);
|
|
156
|
+
fs.writeFileSync(path.join(dest, "SKILL.md"), skill.content);
|
|
157
|
+
}
|
|
158
|
+
return skills.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (tool.format === "rules-file") {
|
|
162
|
+
const rulesPath = path.join(PROJECT_ROOT, tool.rulesFile);
|
|
163
|
+
mkdirp(path.dirname(rulesPath));
|
|
164
|
+
|
|
165
|
+
let content = `# AI Development Skills Framework\n`;
|
|
166
|
+
content += `# Auto-generated by @valentia-ai-skills/framework\n`;
|
|
167
|
+
content += `# Do not edit manually — run 'npx ai-skills update' to refresh\n`;
|
|
168
|
+
content += `# Last updated: ${new Date().toISOString().split("T")[0]}\n\n`;
|
|
169
|
+
|
|
170
|
+
for (const skill of skills) {
|
|
171
|
+
// Strip YAML frontmatter for rules files
|
|
172
|
+
const body = skill.content.replace(/^---[\s\S]*?---\n*/m, "").trim();
|
|
173
|
+
content += `\n${"=".repeat(60)}\n`;
|
|
174
|
+
content += `# SKILL: ${skill.name} (${skill.scope})\n`;
|
|
175
|
+
content += `${"=".repeat(60)}\n\n`;
|
|
176
|
+
content += body + "\n";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(rulesPath, content);
|
|
180
|
+
return skills.length;
|
|
131
181
|
}
|
|
182
|
+
|
|
183
|
+
return 0;
|
|
132
184
|
}
|
|
133
185
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
} else {
|
|
143
|
-
fs.copyFileSync(srcItem, destItem);
|
|
186
|
+
function extractFrontmatter(content) {
|
|
187
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
188
|
+
if (!match) return {};
|
|
189
|
+
const fm = {};
|
|
190
|
+
for (const line of match[1].split("\n")) {
|
|
191
|
+
if (!line.startsWith(" ") && line.includes(":")) {
|
|
192
|
+
const [key, ...rest] = line.split(":");
|
|
193
|
+
fm[key.trim()] = rest.join(":").trim().replace(/^["']|["']$/g, "");
|
|
144
194
|
}
|
|
145
195
|
}
|
|
196
|
+
return fm;
|
|
146
197
|
}
|
|
147
198
|
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
function getLocalSkills() {
|
|
200
|
+
const skills = [];
|
|
201
|
+
if (!fs.existsSync(SKILLS_SOURCE)) return skills;
|
|
202
|
+
for (const cat of ["global", "stack"]) {
|
|
203
|
+
const catDir = path.join(SKILLS_SOURCE, cat);
|
|
204
|
+
if (!fs.existsSync(catDir)) continue;
|
|
205
|
+
for (const item of fs.readdirSync(catDir)) {
|
|
206
|
+
const skillMd = path.join(catDir, item, "SKILL.md");
|
|
207
|
+
if (fs.existsSync(skillMd)) {
|
|
208
|
+
const content = fs.readFileSync(skillMd, "utf-8");
|
|
209
|
+
const fm = extractFrontmatter(content);
|
|
210
|
+
skills.push({ name: fm.name || item, scope: cat, stack: fm.stack || null, version: fm.version || "?", content });
|
|
211
|
+
}
|
|
154
212
|
}
|
|
155
213
|
}
|
|
156
|
-
return
|
|
214
|
+
return skills;
|
|
157
215
|
}
|
|
158
216
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
217
|
+
// ── Email + OTP Verification ──
|
|
218
|
+
|
|
219
|
+
async function requestOtpForEmail(emailInput) {
|
|
220
|
+
let email = emailInput;
|
|
221
|
+
let attempts = 0;
|
|
222
|
+
|
|
223
|
+
while (attempts < 2) {
|
|
224
|
+
if (!email || !email.includes("@")) {
|
|
225
|
+
console.log(c("red", "Invalid email."));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(c("dim", "\nVerifying your email..."));
|
|
230
|
+
|
|
231
|
+
const response = await fetchJSON(SUPABASE_FUNCTION_URL, { email, action: "request_otp" });
|
|
232
|
+
|
|
233
|
+
if (response.error === "not_found") {
|
|
234
|
+
attempts++;
|
|
235
|
+
if (attempts >= 2) {
|
|
236
|
+
console.log(c("red", "\n✗ Email not recognized. Access denied."));
|
|
237
|
+
console.log(c("dim", " Contact your Framework Admin to be added to the system.\n"));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log(c("yellow", "\n⚠ Email not found. Please check and try again.\n"));
|
|
241
|
+
email = await ask(`${c("bold", "Enter your work email:")} `);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (response.error) {
|
|
246
|
+
throw new Error(response.error);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// OTP sent successfully
|
|
250
|
+
console.log(c("green", `\n✓ Found: ${response.user_name}`));
|
|
251
|
+
console.log(c("dim", ` A verification code has been sent to ${email}\n`));
|
|
252
|
+
|
|
253
|
+
return email;
|
|
163
254
|
}
|
|
164
|
-
return null;
|
|
165
255
|
}
|
|
166
256
|
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
|
|
257
|
+
async function verifyOtp(email) {
|
|
258
|
+
const otp = await ask(`${c("bold", "Enter the 6-digit code:")} `);
|
|
259
|
+
|
|
260
|
+
if (!otp || otp.length < 4) {
|
|
261
|
+
console.log(c("red", "Invalid code."));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(c("dim", "\nVerifying code..."));
|
|
266
|
+
|
|
267
|
+
const response = await fetchJSON(SUPABASE_FUNCTION_URL, { email, otp, action: "verify_otp" });
|
|
268
|
+
|
|
269
|
+
if (response.error === "invalid_otp") {
|
|
270
|
+
console.log(c("red", "\n✗ Invalid verification code. Please run setup again."));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (response.error) {
|
|
275
|
+
throw new Error(response.error);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(c("green", "✓ Verified!\n"));
|
|
279
|
+
return response;
|
|
170
280
|
}
|
|
171
281
|
|
|
172
282
|
// ── Commands ──
|
|
173
283
|
|
|
174
|
-
function cmdSetup() {
|
|
284
|
+
async function cmdSetup() {
|
|
175
285
|
console.log(c("blue", "\n━━━ AI Skills Framework — Setup ━━━\n"));
|
|
176
286
|
|
|
177
287
|
// 1. Detect AI tools
|
|
178
288
|
const tools = detectTools();
|
|
179
289
|
if (tools.length === 0) {
|
|
180
|
-
console.log(c("yellow", "No AI coding tools detected
|
|
181
|
-
console.log("Will create a generic .ai-rules file that works with any tool.\n");
|
|
290
|
+
console.log(c("yellow", "No AI coding tools detected. Will create a generic .ai-rules file.\n"));
|
|
182
291
|
tools.push("generic");
|
|
183
292
|
} else {
|
|
184
|
-
console.log(`Detected
|
|
293
|
+
console.log(`Detected: ${tools.map((t) => c("green", TOOL_CONFIGS[t]?.name || t)).join(", ")}\n`);
|
|
185
294
|
}
|
|
186
295
|
|
|
187
|
-
// 2.
|
|
188
|
-
|
|
189
|
-
const globalSkills = allSkills.filter((s) => s.category === "global");
|
|
190
|
-
const stackSkills = allSkills.filter((s) => s.category === "stack");
|
|
296
|
+
// 2. Ask for email
|
|
297
|
+
let email = await ask(`${c("bold", "Enter your work email:")} `);
|
|
191
298
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
299
|
+
let skills;
|
|
300
|
+
let teamName = null;
|
|
301
|
+
let useRemote = true;
|
|
195
302
|
|
|
196
|
-
|
|
197
|
-
|
|
303
|
+
try {
|
|
304
|
+
// 3. Request OTP (with 1 retry for wrong email)
|
|
305
|
+
email = await requestOtpForEmail(email);
|
|
198
306
|
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
console.log(c("yellow", `Setting up for ${tool.name}...`));
|
|
307
|
+
// 4. Verify OTP and get skills
|
|
308
|
+
const response = await verifyOtp(email);
|
|
202
309
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
copyDir(skill.path, dest);
|
|
209
|
-
console.log(` ${c("green", "✓")} ${skill.name}`);
|
|
210
|
-
installedCount++;
|
|
310
|
+
if (response.team) {
|
|
311
|
+
teamName = response.team.name;
|
|
312
|
+
console.log(c("green", `✓ Team: ${teamName}`));
|
|
313
|
+
if (response.user) {
|
|
314
|
+
console.log(c("dim", ` Welcome, ${response.user.name} (${response.user.role})`));
|
|
211
315
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const rulesPath = path.join(PROJECT_ROOT, tool.rulesFile);
|
|
215
|
-
mkdirp(path.dirname(rulesPath));
|
|
216
|
-
|
|
217
|
-
let rulesContent = `# AI Development Skills Framework\n`;
|
|
218
|
-
rulesContent += `# Auto-generated by @valentia-ai-skills/framework\n`;
|
|
219
|
-
rulesContent += `# Do not edit manually — run 'npx ai-skills update' to refresh\n`;
|
|
220
|
-
rulesContent += `# Last updated: ${new Date().toISOString().split("T")[0]}\n\n`;
|
|
221
|
-
|
|
222
|
-
for (const skill of allSkills) {
|
|
223
|
-
const content = readSkillContent(skill.path);
|
|
224
|
-
if (content) {
|
|
225
|
-
const rules = extractSkillRules(content);
|
|
226
|
-
rulesContent += `\n${"=".repeat(60)}\n`;
|
|
227
|
-
rulesContent += `# SKILL: ${skill.name} (${skill.category})\n`;
|
|
228
|
-
rulesContent += `${"=".repeat(60)}\n\n`;
|
|
229
|
-
rulesContent += rules + "\n";
|
|
230
|
-
}
|
|
316
|
+
if (response.team.stack_tags?.length) {
|
|
317
|
+
console.log(c("dim", ` Stack: ${response.team.stack_tags.join(", ")}`));
|
|
231
318
|
}
|
|
319
|
+
} else if (response.message) {
|
|
320
|
+
console.log(c("yellow", `⚠ ${response.message}`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
skills = response.skills || [];
|
|
324
|
+
|
|
325
|
+
if (skills.length === 0) {
|
|
326
|
+
console.log(c("yellow", "\n⚠ No skills are enabled for your team. Contact your Team Lead."));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(` ${c("bold", skills.length)} skill(s) to install\n`);
|
|
331
|
+
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.log(c("yellow", `\n⚠ Could not reach skills server: ${err.message}`));
|
|
334
|
+
console.log(c("dim", " Falling back to local skills from npm package...\n"));
|
|
335
|
+
|
|
336
|
+
skills = getLocalSkills();
|
|
337
|
+
useRemote = false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (skills.length === 0) {
|
|
341
|
+
console.log(c("red", "No skills available. Contact your Framework Admin."));
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
232
344
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
345
|
+
// 5. Install for each tool
|
|
346
|
+
for (const toolKey of tools) {
|
|
347
|
+
const tool = TOOL_CONFIGS[toolKey];
|
|
348
|
+
if (!tool) continue;
|
|
349
|
+
console.log(c("yellow", `Installing for ${tool.name}...`));
|
|
350
|
+
const count = installSkillsForTool(toolKey, skills);
|
|
351
|
+
|
|
352
|
+
if (tool.format === "skill-folder") {
|
|
353
|
+
for (const s of skills) console.log(` ${c("green", "✓")} ${s.name}`);
|
|
354
|
+
} else {
|
|
355
|
+
console.log(` ${c("green", "✓")} ${tool.rulesFile} (${count} skills merged)`);
|
|
236
356
|
}
|
|
237
357
|
console.log("");
|
|
238
358
|
}
|
|
239
359
|
|
|
240
|
-
//
|
|
360
|
+
// 6. Save config
|
|
241
361
|
const config = {
|
|
242
|
-
version: "
|
|
243
|
-
|
|
362
|
+
version: require(path.join(__dirname, "..", "package.json")).version,
|
|
363
|
+
email,
|
|
364
|
+
team: teamName,
|
|
365
|
+
source: useRemote ? "supabase" : "local",
|
|
244
366
|
tools,
|
|
245
|
-
skills:
|
|
367
|
+
skills: skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version })),
|
|
368
|
+
installedAt: new Date().toISOString(),
|
|
246
369
|
};
|
|
247
|
-
|
|
370
|
+
saveConfig(config);
|
|
248
371
|
|
|
249
|
-
//
|
|
372
|
+
// 7. Summary
|
|
250
373
|
console.log(c("blue", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
251
|
-
console.log(c("green",
|
|
252
|
-
console.log(` ${
|
|
253
|
-
console.log(`
|
|
254
|
-
console.log(
|
|
255
|
-
console.log(`To
|
|
374
|
+
console.log(c("green", "✅ Setup complete!"));
|
|
375
|
+
console.log(` ${skills.length} skills installed for ${tools.length} tool(s)`);
|
|
376
|
+
if (teamName) console.log(` Team: ${teamName}`);
|
|
377
|
+
console.log(`\n Config saved to ${c("dim", ".ai-skills.json")}`);
|
|
378
|
+
console.log(` To update: ${c("bold", "npx ai-skills update")}\n`);
|
|
256
379
|
}
|
|
257
380
|
|
|
258
|
-
function cmdUpdate() {
|
|
381
|
+
async function cmdUpdate() {
|
|
259
382
|
console.log(c("blue", "\n━━━ AI Skills Framework — Update ━━━\n"));
|
|
260
383
|
|
|
261
|
-
const config =
|
|
262
|
-
if (!config) {
|
|
384
|
+
const config = loadConfig();
|
|
385
|
+
if (!config || !config.email) {
|
|
263
386
|
console.log(c("yellow", "No .ai-skills.json found. Run 'npx ai-skills setup' first."));
|
|
264
387
|
process.exit(1);
|
|
265
388
|
}
|
|
266
389
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
390
|
+
const email = config.email;
|
|
391
|
+
console.log(c("dim", `Updating for ${email}...`));
|
|
392
|
+
|
|
393
|
+
let skills;
|
|
394
|
+
let teamName = config.team;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Request OTP for the saved email
|
|
398
|
+
await requestOtpForEmail(email);
|
|
399
|
+
|
|
400
|
+
// Verify OTP
|
|
401
|
+
const response = await verifyOtp(email);
|
|
402
|
+
skills = response.skills || [];
|
|
403
|
+
teamName = response.team?.name || config.team;
|
|
404
|
+
|
|
405
|
+
if (response.team) {
|
|
406
|
+
console.log(c("green", `✓ Team: ${teamName} (${skills.length} skills)`));
|
|
407
|
+
} else {
|
|
408
|
+
console.log(c("yellow", `⚠ ${response.message || "No team found."}`));
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.log(c("yellow", `⚠ Server unreachable: ${err.message}`));
|
|
412
|
+
console.log(c("dim", " Using local fallback...\n"));
|
|
413
|
+
skills = getLocalSkills();
|
|
414
|
+
}
|
|
270
415
|
|
|
416
|
+
const tools = config.tools || detectTools();
|
|
271
417
|
for (const toolKey of tools) {
|
|
272
418
|
const tool = TOOL_CONFIGS[toolKey];
|
|
273
419
|
if (!tool) continue;
|
|
274
|
-
|
|
275
420
|
console.log(c("yellow", `Updating ${tool.name}...`));
|
|
421
|
+
installSkillsForTool(toolKey, skills);
|
|
276
422
|
|
|
277
423
|
if (tool.format === "skill-folder") {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const dest = path.join(skillsDir, skill.name);
|
|
281
|
-
copyDir(skill.path, dest);
|
|
282
|
-
console.log(` ${c("green", "✓")} ${skill.name}`);
|
|
283
|
-
}
|
|
284
|
-
} else if (tool.format === "rules-file") {
|
|
285
|
-
const rulesPath = path.join(PROJECT_ROOT, tool.rulesFile);
|
|
286
|
-
mkdirp(path.dirname(rulesPath));
|
|
287
|
-
|
|
288
|
-
let rulesContent = `# AI Development Skills Framework\n`;
|
|
289
|
-
rulesContent += `# Auto-generated by @valentia-ai-skills/framework\n`;
|
|
290
|
-
rulesContent += `# Do not edit manually — run 'npx ai-skills update' to refresh\n`;
|
|
291
|
-
rulesContent += `# Last updated: ${new Date().toISOString().split("T")[0]}\n\n`;
|
|
292
|
-
|
|
293
|
-
for (const skill of allSkills) {
|
|
294
|
-
const content = readSkillContent(skill.path);
|
|
295
|
-
if (content) {
|
|
296
|
-
const rules = extractSkillRules(content);
|
|
297
|
-
rulesContent += `\n${"=".repeat(60)}\n`;
|
|
298
|
-
rulesContent += `# SKILL: ${skill.name} (${skill.category})\n`;
|
|
299
|
-
rulesContent += `${"=".repeat(60)}\n\n`;
|
|
300
|
-
rulesContent += rules + "\n";
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
fs.writeFileSync(rulesPath, rulesContent);
|
|
424
|
+
for (const s of skills) console.log(` ${c("green", "✓")} ${s.name}`);
|
|
425
|
+
} else {
|
|
305
426
|
console.log(` ${c("green", "✓")} ${tool.rulesFile} updated`);
|
|
306
427
|
}
|
|
307
428
|
}
|
|
308
429
|
|
|
309
|
-
// Update config
|
|
310
430
|
config.updatedAt = new Date().toISOString();
|
|
311
|
-
config.
|
|
312
|
-
|
|
431
|
+
config.team = teamName;
|
|
432
|
+
config.skills = skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version }));
|
|
433
|
+
saveConfig(config);
|
|
313
434
|
|
|
314
|
-
console.log(c("green", `\n✅ Updated ${
|
|
435
|
+
console.log(c("green", `\n✅ Updated ${skills.length} skills!\n`));
|
|
315
436
|
}
|
|
316
437
|
|
|
317
438
|
function cmdStatus() {
|
|
318
439
|
console.log(c("blue", "\n━━━ AI Skills Framework — Status ━━━\n"));
|
|
319
440
|
|
|
320
|
-
const config = loadProjectConfig();
|
|
321
|
-
const allSkills = getAllSkills();
|
|
322
|
-
const tools = detectTools();
|
|
323
|
-
|
|
324
|
-
// Package version
|
|
325
441
|
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
326
|
-
|
|
327
|
-
|
|
442
|
+
const config = loadConfig();
|
|
443
|
+
|
|
444
|
+
console.log(`Package: ${c("bold", pkg.name)} v${pkg.version}`);
|
|
328
445
|
|
|
329
|
-
// Installed status
|
|
330
446
|
if (config) {
|
|
331
|
-
console.log(`
|
|
332
|
-
|
|
333
|
-
|
|
447
|
+
console.log(`Email: ${c("bold", config.email)}`);
|
|
448
|
+
console.log(`Team: ${config.team ? c("green", config.team) : c("yellow", "none")}`);
|
|
449
|
+
console.log(`Source: ${config.source === "supabase" ? c("green", "Supabase (live)") : c("yellow", "local fallback")}`);
|
|
450
|
+
console.log(`Installed: ${config.installedAt?.split("T")[0]}`);
|
|
451
|
+
if (config.updatedAt) console.log(`Updated: ${config.updatedAt.split("T")[0]}`);
|
|
452
|
+
console.log(`Tools: ${config.tools?.map((t) => TOOL_CONFIGS[t]?.name || t).join(", ")}`);
|
|
453
|
+
console.log(`\nSkills (${config.skills?.length || 0}):`);
|
|
454
|
+
for (const s of config.skills || []) {
|
|
455
|
+
console.log(` ${c("green", "✓")} ${s.name} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
|
|
334
456
|
}
|
|
335
|
-
console.log(`Tools configured: ${config.tools.map((t) => TOOL_CONFIGS[t]?.name || t).join(", ")}`);
|
|
336
457
|
} else {
|
|
337
458
|
console.log(`Installed: ${c("red", "No")} — run 'npx ai-skills setup'`);
|
|
338
459
|
}
|
|
339
460
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (key === "generic") continue;
|
|
344
|
-
const detected = toolConfig.detect();
|
|
345
|
-
console.log(` ${detected ? c("green", "●") : c("dim", "○")} ${toolConfig.name}`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Skill inventory
|
|
349
|
-
console.log(`\nSkill inventory:`);
|
|
350
|
-
for (const skill of allSkills) {
|
|
351
|
-
const content = readSkillContent(skill.path);
|
|
352
|
-
const versionMatch = content?.match(/version:\s*["']?([^"'\n]+)/);
|
|
353
|
-
const version = versionMatch ? versionMatch[1].trim() : "?";
|
|
354
|
-
console.log(` ${c("green", "✓")} ${skill.name} ${c("dim", `v${version}`)} ${c("dim", `(${skill.category})`)}`);
|
|
461
|
+
console.log(`\nDetected AI tools:`);
|
|
462
|
+
for (const [key, tool] of Object.entries(TOOL_CONFIGS)) {
|
|
463
|
+
console.log(` ${tool.detect() ? c("green", "●") : c("dim", "○")} ${tool.name}`);
|
|
355
464
|
}
|
|
356
465
|
console.log("");
|
|
357
466
|
}
|
|
358
467
|
|
|
359
468
|
function cmdList() {
|
|
360
|
-
const
|
|
361
|
-
console.log(c("blue", "\n━━━ Available Skills ━━━\n"));
|
|
469
|
+
const skills = getLocalSkills();
|
|
470
|
+
console.log(c("blue", "\n━━━ Available Skills (local) ━━━\n"));
|
|
362
471
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
categories[skill.category].push(skill);
|
|
472
|
+
if (skills.length === 0) {
|
|
473
|
+
console.log("No local skills found in package.");
|
|
474
|
+
return;
|
|
367
475
|
}
|
|
368
476
|
|
|
369
|
-
for (const
|
|
370
|
-
console.log(c("
|
|
371
|
-
for (const s of skills) {
|
|
372
|
-
const content = readSkillContent(s.path);
|
|
373
|
-
const descMatch = content?.match(/description:\s*>?\s*\n?\s*(.+)/);
|
|
374
|
-
const desc = descMatch ? descMatch[1].trim().slice(0, 70) + "..." : "";
|
|
375
|
-
console.log(` ${s.name.padEnd(22)} ${c("dim", desc)}`);
|
|
376
|
-
}
|
|
377
|
-
console.log("");
|
|
477
|
+
for (const s of skills) {
|
|
478
|
+
console.log(` ${s.name.padEnd(22)} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
|
|
378
479
|
}
|
|
480
|
+
console.log(c("dim", `\n ${skills.length} skills available`));
|
|
481
|
+
console.log(c("dim", " Note: 'npx ai-skills setup' installs team-specific skills from Supabase\n"));
|
|
379
482
|
}
|
|
380
483
|
|
|
381
484
|
function cmdDoctor() {
|
|
382
485
|
console.log(c("blue", "\n━━━ AI Skills — Health Check ━━━\n"));
|
|
383
|
-
|
|
384
486
|
let issues = 0;
|
|
385
|
-
const allSkills = getAllSkills();
|
|
386
|
-
|
|
387
|
-
// Check skills source exists
|
|
388
|
-
if (allSkills.length === 0) {
|
|
389
|
-
console.log(c("red", "✗ No skills found in package. Package may be corrupted."));
|
|
390
|
-
issues++;
|
|
391
|
-
} else {
|
|
392
|
-
console.log(c("green", `✓ ${allSkills.length} skills found in package`));
|
|
393
|
-
}
|
|
394
487
|
|
|
395
|
-
// Check
|
|
396
|
-
const config =
|
|
488
|
+
// Check config
|
|
489
|
+
const config = loadConfig();
|
|
397
490
|
if (!config) {
|
|
398
|
-
console.log(c("
|
|
491
|
+
console.log(c("red", "✗ No .ai-skills.json — run 'npx ai-skills setup'"));
|
|
399
492
|
issues++;
|
|
400
493
|
} else {
|
|
401
|
-
console.log(c("green", `✓
|
|
494
|
+
console.log(c("green", `✓ Config found (${config.email}, team: ${config.team || "none"})`));
|
|
402
495
|
|
|
403
|
-
// Check
|
|
404
|
-
for (const toolKey of config.tools) {
|
|
496
|
+
// Check tool files
|
|
497
|
+
for (const toolKey of config.tools || []) {
|
|
405
498
|
const tool = TOOL_CONFIGS[toolKey];
|
|
406
499
|
if (!tool) continue;
|
|
407
|
-
|
|
408
500
|
if (tool.format === "skill-folder") {
|
|
409
501
|
const dir = path.join(PROJECT_ROOT, tool.skillsDir);
|
|
410
502
|
if (fs.existsSync(dir)) {
|
|
411
|
-
const count = fs.readdirSync(dir).filter(
|
|
412
|
-
|
|
503
|
+
const count = fs.readdirSync(dir).filter((f) =>
|
|
504
|
+
fs.existsSync(path.join(dir, f, "SKILL.md"))
|
|
413
505
|
).length;
|
|
414
506
|
console.log(c("green", `✓ ${tool.name}: ${count} skills in ${tool.skillsDir}/`));
|
|
415
507
|
} else {
|
|
@@ -428,10 +520,30 @@ function cmdDoctor() {
|
|
|
428
520
|
}
|
|
429
521
|
}
|
|
430
522
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
523
|
+
// Check API connectivity
|
|
524
|
+
console.log(c("dim", "\nChecking API connectivity..."));
|
|
525
|
+
fetchJSON(SUPABASE_FUNCTION_URL, { email: "healthcheck@test.com" })
|
|
526
|
+
.then(() => {
|
|
527
|
+
console.log(c("green", "✓ Supabase Edge Function reachable"));
|
|
528
|
+
finish();
|
|
529
|
+
})
|
|
530
|
+
.catch((err) => {
|
|
531
|
+
if (SUPABASE_FUNCTION_URL.includes("YOUR_PROJECT")) {
|
|
532
|
+
console.log(c("yellow", "⚠ API URL not configured — update SUPABASE_FUNCTION_URL in bin/cli.js"));
|
|
533
|
+
issues++;
|
|
534
|
+
} else {
|
|
535
|
+
console.log(c("yellow", `⚠ API unreachable: ${err.message} (will use local fallback)`));
|
|
536
|
+
}
|
|
537
|
+
finish();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
function finish() {
|
|
541
|
+
console.log(
|
|
542
|
+
issues === 0
|
|
543
|
+
? c("green", "\n✅ Everything looks good!\n")
|
|
544
|
+
: c("yellow", `\n⚠ ${issues} issue(s) found.\n`)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
435
547
|
}
|
|
436
548
|
|
|
437
549
|
// ── Main ──
|
|
@@ -439,44 +551,27 @@ function cmdDoctor() {
|
|
|
439
551
|
const command = process.argv[2] || "setup";
|
|
440
552
|
|
|
441
553
|
switch (command) {
|
|
442
|
-
case "setup":
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
case "
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
case "status":
|
|
449
|
-
cmdStatus();
|
|
450
|
-
break;
|
|
451
|
-
case "list":
|
|
452
|
-
cmdList();
|
|
453
|
-
break;
|
|
454
|
-
case "doctor":
|
|
455
|
-
cmdDoctor();
|
|
456
|
-
break;
|
|
457
|
-
case "help":
|
|
458
|
-
case "--help":
|
|
459
|
-
case "-h":
|
|
554
|
+
case "setup": cmdSetup(); break;
|
|
555
|
+
case "update": cmdUpdate(); break;
|
|
556
|
+
case "status": cmdStatus(); break;
|
|
557
|
+
case "list": cmdList(); break;
|
|
558
|
+
case "doctor": cmdDoctor(); break;
|
|
559
|
+
case "help": case "--help": case "-h":
|
|
460
560
|
console.log(`
|
|
461
|
-
${c("blue", "AI Skills Framework")}
|
|
561
|
+
${c("blue", "AI Skills Framework")} — @valentia-ai-skills/framework
|
|
462
562
|
|
|
463
563
|
Usage:
|
|
464
|
-
npx ai-skills setup
|
|
465
|
-
npx ai-skills update
|
|
466
|
-
npx ai-skills status
|
|
467
|
-
npx ai-skills list
|
|
468
|
-
npx ai-skills doctor
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
2. Setup: npx ai-skills setup
|
|
474
|
-
3. Code: Your AI tools now follow your org's standards
|
|
475
|
-
4. Update: npm update @valentia-ai-skills/framework && npx ai-skills update
|
|
476
|
-
`);
|
|
477
|
-
break;
|
|
564
|
+
npx ai-skills setup Enter email → fetch team's skills → install
|
|
565
|
+
npx ai-skills update Re-fetch and update skills for your team
|
|
566
|
+
npx ai-skills status Show installed skills, team, and tools
|
|
567
|
+
npx ai-skills list List locally bundled skills
|
|
568
|
+
npx ai-skills doctor Health check (config + API + tools)
|
|
569
|
+
|
|
570
|
+
Environment:
|
|
571
|
+
AI_SKILLS_API_URL Override the Supabase Edge Function URL
|
|
572
|
+
`); break;
|
|
478
573
|
default:
|
|
479
574
|
console.log(c("red", `Unknown command: ${command}`));
|
|
480
|
-
console.log("Run 'npx ai-skills help' for usage
|
|
575
|
+
console.log("Run 'npx ai-skills help' for usage.");
|
|
481
576
|
process.exit(1);
|
|
482
577
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentia-ai-skills/framework",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "AI development skills framework
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "AI development skills framework ÃÂÃÂÃÂâÃÂÃÂÃÂÃÂÃÂÃÂÃÂàcentralized coding standards, security patterns, and SOPs for AI-assisted development. Works with Claude Code, Cursor, Copilot, Windsurf, and any AI coding tool.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-skills",
|
|
7
7
|
"claude-code",
|