@valentia-ai-skills/framework 1.0.3 → 1.0.4
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 +310 -287
- 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,376 @@ 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
|
-
|
|
94
|
-
|
|
95
|
-
for (const category of ["global", "stack", "onboarding"]) {
|
|
96
|
-
const catDir = path.join(SKILLS_SOURCE, category);
|
|
97
|
-
if (!fs.existsSync(catDir)) continue;
|
|
79
|
+
function mkdirp(dirPath) {
|
|
80
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
81
|
+
}
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
});
|
|
110
122
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
);
|
|
124
|
+
req.on("error", reject);
|
|
125
|
+
req.write(postData);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return fs.readFileSync(skillMd, "utf-8");
|
|
130
|
+
function loadConfig() {
|
|
131
|
+
if (fs.existsSync(CONFIG_PATH)) return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
132
|
+
return null;
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n*/m, "");
|
|
125
|
-
return withoutFrontmatter.trim();
|
|
135
|
+
function saveConfig(config) {
|
|
136
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
function detectTools() {
|
|
140
|
+
const detected = [];
|
|
141
|
+
for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
|
|
142
|
+
if (config.detect()) detected.push(key);
|
|
131
143
|
}
|
|
144
|
+
return detected;
|
|
132
145
|
}
|
|
133
146
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
fs.
|
|
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);
|
|
144
157
|
}
|
|
158
|
+
return skills.length;
|
|
145
159
|
}
|
|
146
|
-
}
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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";
|
|
154
177
|
}
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(rulesPath, content);
|
|
180
|
+
return skills.length;
|
|
155
181
|
}
|
|
156
|
-
|
|
182
|
+
|
|
183
|
+
return 0;
|
|
157
184
|
}
|
|
158
185
|
|
|
159
|
-
function
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
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, "");
|
|
194
|
+
}
|
|
163
195
|
}
|
|
164
|
-
return
|
|
196
|
+
return fm;
|
|
165
197
|
}
|
|
166
198
|
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
fs.
|
|
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
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return skills;
|
|
170
215
|
}
|
|
171
216
|
|
|
172
217
|
// ── Commands ──
|
|
173
218
|
|
|
174
|
-
function cmdSetup() {
|
|
219
|
+
async function cmdSetup() {
|
|
175
220
|
console.log(c("blue", "\n━━━ AI Skills Framework — Setup ━━━\n"));
|
|
176
221
|
|
|
177
222
|
// 1. Detect AI tools
|
|
178
223
|
const tools = detectTools();
|
|
179
224
|
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");
|
|
225
|
+
console.log(c("yellow", "No AI coding tools detected. Will create a generic .ai-rules file.\n"));
|
|
182
226
|
tools.push("generic");
|
|
183
227
|
} else {
|
|
184
|
-
console.log(`Detected
|
|
228
|
+
console.log(`Detected: ${tools.map((t) => c("green", TOOL_CONFIGS[t]?.name || t)).join(", ")}\n`);
|
|
185
229
|
}
|
|
186
230
|
|
|
187
|
-
// 2.
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
// 2. Ask for email
|
|
232
|
+
const email = await ask(`${c("bold", "Enter your work email:")} `);
|
|
233
|
+
if (!email || !email.includes("@")) {
|
|
234
|
+
console.log(c("red", "Invalid email. Please try again."));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
191
237
|
|
|
192
|
-
|
|
193
|
-
console.log(
|
|
194
|
-
console.log(` Stack: ${stackSkills.map((s) => s.name).join(", ")}\n`);
|
|
238
|
+
// 3. Lookup team from Supabase
|
|
239
|
+
console.log(c("dim", "\nLooking up your team..."));
|
|
195
240
|
|
|
196
|
-
|
|
197
|
-
let
|
|
241
|
+
let response;
|
|
242
|
+
let skills;
|
|
243
|
+
let teamName = null;
|
|
244
|
+
let useRemote = true;
|
|
198
245
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.log(c("yellow", `Setting up for ${tool.name}...`));
|
|
246
|
+
try {
|
|
247
|
+
response = await fetchJSON(SUPABASE_FUNCTION_URL, { email });
|
|
202
248
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
copyDir(skill.path, dest);
|
|
209
|
-
console.log(` ${c("green", "✓")} ${skill.name}`);
|
|
210
|
-
installedCount++;
|
|
249
|
+
if (response.team) {
|
|
250
|
+
teamName = response.team.name;
|
|
251
|
+
console.log(c("green", `\n✓ Team: ${teamName}`));
|
|
252
|
+
if (response.user) {
|
|
253
|
+
console.log(c("dim", ` Welcome, ${response.user.name} (${response.user.role})`));
|
|
211
254
|
}
|
|
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
|
-
}
|
|
255
|
+
if (response.team.stack_tags?.length) {
|
|
256
|
+
console.log(c("dim", ` Stack: ${response.team.stack_tags.join(", ")}`));
|
|
231
257
|
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(c("yellow", `\n⚠ ${response.message || "No team found for this email."}`));
|
|
260
|
+
}
|
|
232
261
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
skills = response.skills || [];
|
|
263
|
+
console.log(` ${c("bold", skills.length)} skill(s) to install\n`);
|
|
264
|
+
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.log(c("yellow", `\n⚠ Could not reach skills server: ${err.message}`));
|
|
267
|
+
console.log(c("dim", " Falling back to local skills from npm package...\n"));
|
|
268
|
+
|
|
269
|
+
skills = getLocalSkills();
|
|
270
|
+
useRemote = false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (skills.length === 0) {
|
|
274
|
+
console.log(c("red", "No skills available. Contact your Framework Admin."));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 4. Install for each tool
|
|
279
|
+
for (const toolKey of tools) {
|
|
280
|
+
const tool = TOOL_CONFIGS[toolKey];
|
|
281
|
+
if (!tool) continue;
|
|
282
|
+
console.log(c("yellow", `Installing for ${tool.name}...`));
|
|
283
|
+
const count = installSkillsForTool(toolKey, skills);
|
|
284
|
+
|
|
285
|
+
if (tool.format === "skill-folder") {
|
|
286
|
+
for (const s of skills) console.log(` ${c("green", "✓")} ${s.name}`);
|
|
287
|
+
} else {
|
|
288
|
+
console.log(` ${c("green", "✓")} ${tool.rulesFile} (${count} skills merged)`);
|
|
236
289
|
}
|
|
237
290
|
console.log("");
|
|
238
291
|
}
|
|
239
292
|
|
|
240
|
-
//
|
|
293
|
+
// 5. Save config
|
|
241
294
|
const config = {
|
|
242
|
-
version: "
|
|
243
|
-
|
|
295
|
+
version: require(path.join(__dirname, "..", "package.json")).version,
|
|
296
|
+
email,
|
|
297
|
+
team: teamName,
|
|
298
|
+
source: useRemote ? "supabase" : "local",
|
|
244
299
|
tools,
|
|
245
|
-
skills:
|
|
300
|
+
skills: skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version })),
|
|
301
|
+
installedAt: new Date().toISOString(),
|
|
246
302
|
};
|
|
247
|
-
|
|
303
|
+
saveConfig(config);
|
|
248
304
|
|
|
249
|
-
//
|
|
305
|
+
// 6. Summary
|
|
250
306
|
console.log(c("blue", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
251
|
-
console.log(c("green",
|
|
252
|
-
console.log(` ${
|
|
253
|
-
console.log(`
|
|
254
|
-
console.log(
|
|
255
|
-
console.log(`To
|
|
307
|
+
console.log(c("green", "✅ Setup complete!"));
|
|
308
|
+
console.log(` ${skills.length} skills installed for ${tools.length} tool(s)`);
|
|
309
|
+
if (teamName) console.log(` Team: ${teamName}`);
|
|
310
|
+
console.log(`\n Config saved to ${c("dim", ".ai-skills.json")}`);
|
|
311
|
+
console.log(` To update: ${c("bold", "npx ai-skills update")}\n`);
|
|
256
312
|
}
|
|
257
313
|
|
|
258
|
-
function cmdUpdate() {
|
|
314
|
+
async function cmdUpdate() {
|
|
259
315
|
console.log(c("blue", "\n━━━ AI Skills Framework — Update ━━━\n"));
|
|
260
316
|
|
|
261
|
-
const config =
|
|
262
|
-
if (!config) {
|
|
317
|
+
const config = loadConfig();
|
|
318
|
+
if (!config || !config.email) {
|
|
263
319
|
console.log(c("yellow", "No .ai-skills.json found. Run 'npx ai-skills setup' first."));
|
|
264
320
|
process.exit(1);
|
|
265
321
|
}
|
|
266
322
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
323
|
+
console.log(c("dim", `Updating for ${config.email}...`));
|
|
324
|
+
|
|
325
|
+
let skills;
|
|
326
|
+
let teamName = config.team;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetchJSON(SUPABASE_FUNCTION_URL, { email: config.email });
|
|
330
|
+
skills = response.skills || [];
|
|
331
|
+
teamName = response.team?.name || config.team;
|
|
332
|
+
|
|
333
|
+
if (response.team) {
|
|
334
|
+
console.log(c("green", `✓ Team: ${teamName} (${skills.length} skills)`));
|
|
335
|
+
} else {
|
|
336
|
+
console.log(c("yellow", `⚠ ${response.message || "No team found."}`));
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.log(c("yellow", `⚠ Server unreachable: ${err.message}`));
|
|
340
|
+
console.log(c("dim", " Using local fallback...\n"));
|
|
341
|
+
skills = getLocalSkills();
|
|
342
|
+
}
|
|
270
343
|
|
|
344
|
+
const tools = config.tools || detectTools();
|
|
271
345
|
for (const toolKey of tools) {
|
|
272
346
|
const tool = TOOL_CONFIGS[toolKey];
|
|
273
347
|
if (!tool) continue;
|
|
274
|
-
|
|
275
348
|
console.log(c("yellow", `Updating ${tool.name}...`));
|
|
349
|
+
installSkillsForTool(toolKey, skills);
|
|
276
350
|
|
|
277
351
|
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);
|
|
352
|
+
for (const s of skills) console.log(` ${c("green", "✓")} ${s.name}`);
|
|
353
|
+
} else {
|
|
305
354
|
console.log(` ${c("green", "✓")} ${tool.rulesFile} updated`);
|
|
306
355
|
}
|
|
307
356
|
}
|
|
308
357
|
|
|
309
|
-
// Update config
|
|
310
358
|
config.updatedAt = new Date().toISOString();
|
|
311
|
-
config.
|
|
312
|
-
|
|
359
|
+
config.team = teamName;
|
|
360
|
+
config.skills = skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version }));
|
|
361
|
+
saveConfig(config);
|
|
313
362
|
|
|
314
|
-
console.log(c("green", `\n✅ Updated ${
|
|
363
|
+
console.log(c("green", `\n✅ Updated ${skills.length} skills!\n`));
|
|
315
364
|
}
|
|
316
365
|
|
|
317
366
|
function cmdStatus() {
|
|
318
367
|
console.log(c("blue", "\n━━━ AI Skills Framework — Status ━━━\n"));
|
|
319
368
|
|
|
320
|
-
const config = loadProjectConfig();
|
|
321
|
-
const allSkills = getAllSkills();
|
|
322
|
-
const tools = detectTools();
|
|
323
|
-
|
|
324
|
-
// Package version
|
|
325
369
|
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
326
|
-
|
|
327
|
-
|
|
370
|
+
const config = loadConfig();
|
|
371
|
+
|
|
372
|
+
console.log(`Package: ${c("bold", pkg.name)} v${pkg.version}`);
|
|
328
373
|
|
|
329
|
-
// Installed status
|
|
330
374
|
if (config) {
|
|
331
|
-
console.log(`
|
|
332
|
-
|
|
333
|
-
|
|
375
|
+
console.log(`Email: ${c("bold", config.email)}`);
|
|
376
|
+
console.log(`Team: ${config.team ? c("green", config.team) : c("yellow", "none")}`);
|
|
377
|
+
console.log(`Source: ${config.source === "supabase" ? c("green", "Supabase (live)") : c("yellow", "local fallback")}`);
|
|
378
|
+
console.log(`Installed: ${config.installedAt?.split("T")[0]}`);
|
|
379
|
+
if (config.updatedAt) console.log(`Updated: ${config.updatedAt.split("T")[0]}`);
|
|
380
|
+
console.log(`Tools: ${config.tools?.map((t) => TOOL_CONFIGS[t]?.name || t).join(", ")}`);
|
|
381
|
+
console.log(`\nSkills (${config.skills?.length || 0}):`);
|
|
382
|
+
for (const s of config.skills || []) {
|
|
383
|
+
console.log(` ${c("green", "✓")} ${s.name} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
|
|
334
384
|
}
|
|
335
|
-
console.log(`Tools configured: ${config.tools.map((t) => TOOL_CONFIGS[t]?.name || t).join(", ")}`);
|
|
336
385
|
} else {
|
|
337
386
|
console.log(`Installed: ${c("red", "No")} — run 'npx ai-skills setup'`);
|
|
338
387
|
}
|
|
339
388
|
|
|
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})`)}`);
|
|
389
|
+
console.log(`\nDetected AI tools:`);
|
|
390
|
+
for (const [key, tool] of Object.entries(TOOL_CONFIGS)) {
|
|
391
|
+
console.log(` ${tool.detect() ? c("green", "●") : c("dim", "○")} ${tool.name}`);
|
|
355
392
|
}
|
|
356
393
|
console.log("");
|
|
357
394
|
}
|
|
358
395
|
|
|
359
396
|
function cmdList() {
|
|
360
|
-
const
|
|
361
|
-
console.log(c("blue", "\n━━━ Available Skills ━━━\n"));
|
|
397
|
+
const skills = getLocalSkills();
|
|
398
|
+
console.log(c("blue", "\n━━━ Available Skills (local) ━━━\n"));
|
|
362
399
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
categories[skill.category].push(skill);
|
|
400
|
+
if (skills.length === 0) {
|
|
401
|
+
console.log("No local skills found in package.");
|
|
402
|
+
return;
|
|
367
403
|
}
|
|
368
404
|
|
|
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("");
|
|
405
|
+
for (const s of skills) {
|
|
406
|
+
console.log(` ${s.name.padEnd(22)} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
|
|
378
407
|
}
|
|
408
|
+
console.log(c("dim", `\n ${skills.length} skills available`));
|
|
409
|
+
console.log(c("dim", " Note: 'npx ai-skills setup' installs team-specific skills from Supabase\n"));
|
|
379
410
|
}
|
|
380
411
|
|
|
381
412
|
function cmdDoctor() {
|
|
382
413
|
console.log(c("blue", "\n━━━ AI Skills — Health Check ━━━\n"));
|
|
383
|
-
|
|
384
414
|
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
415
|
|
|
395
|
-
// Check
|
|
396
|
-
const config =
|
|
416
|
+
// Check config
|
|
417
|
+
const config = loadConfig();
|
|
397
418
|
if (!config) {
|
|
398
|
-
console.log(c("
|
|
419
|
+
console.log(c("red", "✗ No .ai-skills.json — run 'npx ai-skills setup'"));
|
|
399
420
|
issues++;
|
|
400
421
|
} else {
|
|
401
|
-
console.log(c("green", `✓
|
|
422
|
+
console.log(c("green", `✓ Config found (${config.email}, team: ${config.team || "none"})`));
|
|
402
423
|
|
|
403
|
-
// Check
|
|
404
|
-
for (const toolKey of config.tools) {
|
|
424
|
+
// Check tool files
|
|
425
|
+
for (const toolKey of config.tools || []) {
|
|
405
426
|
const tool = TOOL_CONFIGS[toolKey];
|
|
406
427
|
if (!tool) continue;
|
|
407
|
-
|
|
408
428
|
if (tool.format === "skill-folder") {
|
|
409
429
|
const dir = path.join(PROJECT_ROOT, tool.skillsDir);
|
|
410
430
|
if (fs.existsSync(dir)) {
|
|
411
|
-
const count = fs.readdirSync(dir).filter(
|
|
412
|
-
|
|
431
|
+
const count = fs.readdirSync(dir).filter((f) =>
|
|
432
|
+
fs.existsSync(path.join(dir, f, "SKILL.md"))
|
|
413
433
|
).length;
|
|
414
434
|
console.log(c("green", `✓ ${tool.name}: ${count} skills in ${tool.skillsDir}/`));
|
|
415
435
|
} else {
|
|
@@ -428,10 +448,30 @@ function cmdDoctor() {
|
|
|
428
448
|
}
|
|
429
449
|
}
|
|
430
450
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
451
|
+
// Check API connectivity
|
|
452
|
+
console.log(c("dim", "\nChecking API connectivity..."));
|
|
453
|
+
fetchJSON(SUPABASE_FUNCTION_URL, { email: "healthcheck@test.com" })
|
|
454
|
+
.then(() => {
|
|
455
|
+
console.log(c("green", "✓ Supabase Edge Function reachable"));
|
|
456
|
+
finish();
|
|
457
|
+
})
|
|
458
|
+
.catch((err) => {
|
|
459
|
+
if (SUPABASE_FUNCTION_URL.includes("YOUR_PROJECT")) {
|
|
460
|
+
console.log(c("yellow", "⚠ API URL not configured — update SUPABASE_FUNCTION_URL in bin/cli.js"));
|
|
461
|
+
issues++;
|
|
462
|
+
} else {
|
|
463
|
+
console.log(c("yellow", `⚠ API unreachable: ${err.message} (will use local fallback)`));
|
|
464
|
+
}
|
|
465
|
+
finish();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function finish() {
|
|
469
|
+
console.log(
|
|
470
|
+
issues === 0
|
|
471
|
+
? c("green", "\n✅ Everything looks good!\n")
|
|
472
|
+
: c("yellow", `\n⚠ ${issues} issue(s) found.\n`)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
435
475
|
}
|
|
436
476
|
|
|
437
477
|
// ── Main ──
|
|
@@ -439,44 +479,27 @@ function cmdDoctor() {
|
|
|
439
479
|
const command = process.argv[2] || "setup";
|
|
440
480
|
|
|
441
481
|
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":
|
|
482
|
+
case "setup": cmdSetup(); break;
|
|
483
|
+
case "update": cmdUpdate(); break;
|
|
484
|
+
case "status": cmdStatus(); break;
|
|
485
|
+
case "list": cmdList(); break;
|
|
486
|
+
case "doctor": cmdDoctor(); break;
|
|
487
|
+
case "help": case "--help": case "-h":
|
|
460
488
|
console.log(`
|
|
461
|
-
${c("blue", "AI Skills Framework")}
|
|
489
|
+
${c("blue", "AI Skills Framework")} — @valentia-ai-skills/framework
|
|
462
490
|
|
|
463
491
|
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;
|
|
492
|
+
npx ai-skills setup Enter email → fetch team's skills → install
|
|
493
|
+
npx ai-skills update Re-fetch and update skills for your team
|
|
494
|
+
npx ai-skills status Show installed skills, team, and tools
|
|
495
|
+
npx ai-skills list List locally bundled skills
|
|
496
|
+
npx ai-skills doctor Health check (config + API + tools)
|
|
497
|
+
|
|
498
|
+
Environment:
|
|
499
|
+
AI_SKILLS_API_URL Override the Supabase Edge Function URL
|
|
500
|
+
`); break;
|
|
478
501
|
default:
|
|
479
502
|
console.log(c("red", `Unknown command: ${command}`));
|
|
480
|
-
console.log("Run 'npx ai-skills help' for usage
|
|
503
|
+
console.log("Run 'npx ai-skills help' for usage.");
|
|
481
504
|
process.exit(1);
|
|
482
505
|
}
|
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.4",
|
|
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",
|