@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.
Files changed (2) hide show
  1. package/bin/cli.js +380 -285
  2. 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 # First-time setup detects your AI tools and installs skills
8
- * npx ai-skills update # Pull latest skills into your project
9
- * npx ai-skills status # Show which skills are installed and which tools are configured
10
- * npx ai-skills list # List all available skills
11
- * npx ai-skills add <skill> # Add a specific skill
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", // copies full SKILL.md folder structure
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", // merges into a single 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 COLORS = {
67
- green: "\x1b[32m",
68
- yellow: "\x1b[33m",
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
- require("child_process").execSync(`which ${cmd} 2>/dev/null`, { stdio: "ignore" });
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 getAllSkills() {
92
- const skills = [];
93
- if (!fs.existsSync(SKILLS_SOURCE)) return skills;
79
+ function mkdirp(dirPath) {
80
+ if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
81
+ }
94
82
 
95
- for (const category of ["global", "stack", "onboarding"]) {
96
- const catDir = path.join(SKILLS_SOURCE, category);
97
- if (!fs.existsSync(catDir)) continue;
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
- if (category === "onboarding") {
100
- if (fs.existsSync(path.join(catDir, "SKILL.md"))) {
101
- skills.push({ name: "onboarding", category, path: catDir });
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
- continue;
104
- }
123
+ );
124
+ req.on("error", reject);
125
+ req.write(postData);
126
+ req.end();
127
+ });
128
+ }
105
129
 
106
- for (const item of fs.readdirSync(catDir)) {
107
- const itemPath = path.join(catDir, item);
108
- if (fs.statSync(itemPath).isDirectory() && fs.existsSync(path.join(itemPath, "SKILL.md"))) {
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 readSkillContent(skillPath) {
117
- const skillMd = path.join(skillPath, "SKILL.md");
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 extractSkillRules(content) {
123
- // Remove YAML frontmatter
124
- const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n*/m, "");
125
- return withoutFrontmatter.trim();
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 mkdirp(dirPath) {
129
- if (!fs.existsSync(dirPath)) {
130
- fs.mkdirSync(dirPath, { recursive: true });
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 copyDir(src, dest) {
135
- mkdirp(dest);
136
- for (const item of fs.readdirSync(src)) {
137
- const srcItem = path.join(src, item);
138
- const destItem = path.join(dest, item);
139
- if (item === "tests") continue; // skip test prompts in installed version
140
- if (fs.statSync(srcItem).isDirectory()) {
141
- copyDir(srcItem, destItem);
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 detectTools() {
149
- const detected = [];
150
- for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
151
- if (key === "generic") continue;
152
- if (config.detect()) {
153
- detected.push(key);
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 detected;
214
+ return skills;
157
215
  }
158
216
 
159
- function loadProjectConfig() {
160
- const configPath = path.join(PROJECT_ROOT, ".ai-skills.json");
161
- if (fs.existsSync(configPath)) {
162
- return JSON.parse(fs.readFileSync(configPath, "utf-8"));
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 saveProjectConfig(config) {
168
- const configPath = path.join(PROJECT_ROOT, ".ai-skills.json");
169
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
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 in this project."));
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 AI tools: ${tools.map((t) => c("green", TOOL_CONFIGS[t].name)).join(", ")}\n`);
293
+ console.log(`Detected: ${tools.map((t) => c("green", TOOL_CONFIGS[t]?.name || t)).join(", ")}\n`);
185
294
  }
186
295
 
187
- // 2. Get all available skills
188
- const allSkills = getAllSkills();
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
- console.log(`Available skills: ${c("bold", allSkills.length)}`);
193
- console.log(` Global: ${globalSkills.map((s) => s.name).join(", ")}`);
194
- console.log(` Stack: ${stackSkills.map((s) => s.name).join(", ")}\n`);
299
+ let skills;
300
+ let teamName = null;
301
+ let useRemote = true;
195
302
 
196
- // 3. Install for each detected tool
197
- let installedCount = 0;
303
+ try {
304
+ // 3. Request OTP (with 1 retry for wrong email)
305
+ email = await requestOtpForEmail(email);
198
306
 
199
- for (const toolKey of tools) {
200
- const tool = TOOL_CONFIGS[toolKey];
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 (tool.format === "skill-folder") {
204
- // Claude Code: copy individual SKILL.md folders
205
- const skillsDir = path.join(PROJECT_ROOT, tool.skillsDir);
206
- for (const skill of allSkills) {
207
- const dest = path.join(skillsDir, skill.name);
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
- } else if (tool.format === "rules-file") {
213
- // Cursor / Copilot / Windsurf / Generic: merge into single rules file
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
- fs.writeFileSync(rulesPath, rulesContent);
234
- console.log(` ${c("green", "✓")} Written to ${tool.rulesFile} (${allSkills.length} skills merged)`);
235
- installedCount += allSkills.length;
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
- // 4. Save project config
360
+ // 6. Save config
241
361
  const config = {
242
- version: "1.0.0",
243
- installedAt: new Date().toISOString(),
362
+ version: require(path.join(__dirname, "..", "package.json")).version,
363
+ email,
364
+ team: teamName,
365
+ source: useRemote ? "supabase" : "local",
244
366
  tools,
245
- skills: allSkills.map((s) => ({ name: s.name, category: s.category })),
367
+ skills: skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version })),
368
+ installedAt: new Date().toISOString(),
246
369
  };
247
- saveProjectConfig(config);
370
+ saveConfig(config);
248
371
 
249
- // 5. Summary
372
+ // 7. Summary
250
373
  console.log(c("blue", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
251
- console.log(c("green", `✅ Setup complete!`));
252
- console.log(` ${allSkills.length} skills installed for ${tools.length} tool(s)\n`);
253
- console.log(`Config saved to ${c("dim", ".ai-skills.json")}`);
254
- console.log(`To update later: ${c("bold", "npx ai-skills update")}`);
255
- console.log(`To check status: ${c("bold", "npx ai-skills status")}\n`);
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 = loadProjectConfig();
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
- // Re-run setup with saved tool preferences
268
- const tools = config.tools || detectTools();
269
- const allSkills = getAllSkills();
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 skillsDir = path.join(PROJECT_ROOT, tool.skillsDir);
279
- for (const skill of allSkills) {
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.skills = allSkills.map((s) => ({ name: s.name, category: s.category }));
312
- saveProjectConfig(config);
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 ${allSkills.length} skills!\n`));
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
- console.log(`Package version: ${c("bold", pkg.version)}`);
327
- console.log(`Skills available: ${c("bold", allSkills.length)}`);
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(`Installed: ${c("green", "Yes")} (${config.installedAt.split("T")[0]})`);
332
- if (config.updatedAt) {
333
- console.log(`Last updated: ${config.updatedAt.split("T")[0]}`);
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
- // Detected tools
341
- console.log(`\nDetected AI tools in this project:`);
342
- for (const [key, toolConfig] of Object.entries(TOOL_CONFIGS)) {
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 allSkills = getAllSkills();
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
- const categories = {};
364
- for (const skill of allSkills) {
365
- if (!categories[skill.category]) categories[skill.category] = [];
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 [cat, skills] of Object.entries(categories)) {
370
- console.log(c("yellow", `${cat.toUpperCase()}`));
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 project config
396
- const config = loadProjectConfig();
488
+ // Check config
489
+ const config = loadConfig();
397
490
  if (!config) {
398
- console.log(c("yellow", " No .ai-skills.json — run 'npx ai-skills setup'"));
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", `✓ Project configured (${config.tools.length} tool(s))`));
494
+ console.log(c("green", `✓ Config found (${config.email}, team: ${config.team || "none"})`));
402
495
 
403
- // Check each tool's files exist
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
- (f) => fs.existsSync(path.join(dir, f, "SKILL.md"))
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
- console.log(issues === 0
432
- ? c("green", "\n✅ Everything looks good!\n")
433
- : c("yellow", `\n⚠ ${issues} issue(s) found. Run 'npx ai-skills setup' to fix.\n`)
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
- cmdSetup();
444
- break;
445
- case "update":
446
- cmdUpdate();
447
- break;
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 Setup skills for detected AI tools
465
- npx ai-skills update Update skills to latest version
466
- npx ai-skills status Show installed skills and tools
467
- npx ai-skills list List all available skills
468
- npx ai-skills doctor Check setup health
469
- npx ai-skills help Show this help
470
-
471
- How it works:
472
- 1. Install: npm install --save-dev @valentia-ai-skills/framework
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 info.");
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.3",
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.",
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",