@valentia-ai-skills/framework 1.0.7 → 1.0.9

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 CHANGED
@@ -26,6 +26,8 @@ const CONFIG_PATH = path.join(PROJECT_ROOT, ".ai-skills.json");
26
26
  // UPDATE THIS with your actual Supabase project URL
27
27
  const SUPABASE_FUNCTION_URL =
28
28
  process.env.AI_SKILLS_API_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/lookup-team";
29
+ const ANALYZE_FUNCTION_URL =
30
+ process.env.AI_SKILLS_ANALYZE_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/analyze-commit";
29
31
 
30
32
  const TOOL_CONFIGS = {
31
33
  "claude-code": {
@@ -248,9 +250,15 @@ async function requestOtpForEmail(emailInput) {
248
250
 
249
251
  // OTP sent successfully
250
252
  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
 
253
- return email;
254
+ if (response.fallback_code) {
255
+ // Email delivery not configured — show code in terminal
256
+ console.log(c("yellow", `\n Your verification code: ${c("bold", response.fallback_code)}\n`));
257
+ } else {
258
+ console.log(c("dim", ` A verification code has been sent to ${email}\n`));
259
+ }
260
+
261
+ return { email, fallbackCode: response.fallback_code || null };
254
262
  }
255
263
  }
256
264
 
@@ -301,7 +309,8 @@ async function cmdSetup() {
301
309
 
302
310
  try {
303
311
  // 3. Request OTP (with 1 retry for wrong email)
304
- email = await requestOtpForEmail(email);
312
+ const otpResult = await requestOtpForEmail(email);
313
+ email = otpResult.email;
305
314
 
306
315
  // 4. Verify OTP and get skills
307
316
  const response = await verifyOtp(email);
@@ -360,14 +369,26 @@ async function cmdSetup() {
360
369
  email,
361
370
  team: teamName,
362
371
  source: "supabase",
372
+ analyzeUrl: ANALYZE_FUNCTION_URL,
363
373
  tools,
364
374
  skills: skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version })),
365
375
  installedAt: new Date().toISOString(),
366
376
  };
367
377
  saveConfig(config);
368
378
 
369
- // 7. Summary
370
- console.log(c("blue", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
379
+ // 7. Install post-commit analysis hook
380
+ try {
381
+ const { installHook } = require(path.join(__dirname, "..", "hooks", "install-hook.js"));
382
+ const hookResult = installHook(PROJECT_ROOT);
383
+ if (hookResult.installed) {
384
+ console.log(`${c("green", "✓")} Post-commit analysis hook installed`);
385
+ }
386
+ } catch {
387
+ // Hook installation is optional — don't fail setup
388
+ }
389
+
390
+ // 8. Summary
391
+ console.log(c("blue", "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
371
392
  console.log(c("green", "✅ Setup complete!"));
372
393
  console.log(` ${skills.length} skills installed for ${tools.length} tool(s)`);
373
394
  if (teamName) console.log(` Team: ${teamName}`);
@@ -392,7 +413,8 @@ async function cmdUpdate() {
392
413
 
393
414
  try {
394
415
  // Request OTP for the saved email
395
- await requestOtpForEmail(email);
416
+ const otpResult = await requestOtpForEmail(email);
417
+ email = otpResult.email;
396
418
 
397
419
  // Verify OTP
398
420
  const response = await verifyOtp(email);
@@ -543,6 +565,142 @@ function cmdDoctor() {
543
565
  }
544
566
  }
545
567
 
568
+ // ── Analyze Command ──
569
+
570
+ async function cmdAnalyze() {
571
+ console.log(c("blue", "\n━━━ AI Skills Framework — Analyze ━━━\n"));
572
+
573
+ const config = loadConfig();
574
+ if (!config || !config.email) {
575
+ console.log(c("yellow", "No .ai-skills.json found. Run 'npx ai-skills setup' first."));
576
+ process.exit(1);
577
+ }
578
+
579
+ const skillNames = (config.skills || []).map((s) => s.name);
580
+ if (skillNames.length === 0) {
581
+ console.log(c("yellow", "No skills configured. Run 'npx ai-skills setup' first."));
582
+ process.exit(1);
583
+ }
584
+
585
+ const analyzeUrl = config.analyzeUrl || ANALYZE_FUNCTION_URL;
586
+
587
+ // Check if we're in a git repo
588
+ try {
589
+ require("child_process").execSync("git rev-parse --git-dir", { stdio: "ignore", cwd: PROJECT_ROOT });
590
+ } catch {
591
+ console.log(c("red", "Not a git repository."));
592
+ process.exit(1);
593
+ }
594
+
595
+ const execOpt = { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 10000 };
596
+
597
+ // Collect git data
598
+ const useLastCommit = process.argv.includes("--last");
599
+ let diff, commitHash, commitMessage, branch, filesChanged;
600
+
601
+ try {
602
+ if (useLastCommit) {
603
+ diff = require("child_process").execSync("git diff HEAD~1 HEAD", execOpt).trim();
604
+ commitHash = require("child_process").execSync("git rev-parse HEAD", execOpt).trim();
605
+ commitMessage = require("child_process").execSync('git log -1 --format="%s"', execOpt).trim();
606
+ filesChanged = require("child_process").execSync("git diff --name-only HEAD~1 HEAD", execOpt).trim().split("\n").filter(Boolean);
607
+ } else {
608
+ // Analyze staged changes or last commit
609
+ diff = require("child_process").execSync("git diff --cached", execOpt).trim();
610
+ if (!diff) {
611
+ diff = require("child_process").execSync("git diff HEAD~1 HEAD", execOpt).trim();
612
+ commitHash = require("child_process").execSync("git rev-parse HEAD", execOpt).trim();
613
+ commitMessage = require("child_process").execSync('git log -1 --format="%s"', execOpt).trim();
614
+ filesChanged = require("child_process").execSync("git diff --name-only HEAD~1 HEAD", execOpt).trim().split("\n").filter(Boolean);
615
+ } else {
616
+ commitHash = "(staged)";
617
+ commitMessage = "(staged changes)";
618
+ filesChanged = require("child_process").execSync("git diff --cached --name-only", execOpt).trim().split("\n").filter(Boolean);
619
+ }
620
+ }
621
+ branch = require("child_process").execSync("git branch --show-current", execOpt).trim();
622
+ } catch (err) {
623
+ console.log(c("red", `Failed to collect git data: ${err.message}`));
624
+ process.exit(1);
625
+ }
626
+
627
+ if (!diff) {
628
+ console.log(c("yellow", "No changes to analyze."));
629
+ process.exit(0);
630
+ }
631
+
632
+ // Truncate diff if needed
633
+ if (diff.length > 10000) {
634
+ diff = diff.slice(0, 4000) + "\n\n[...truncated...]\n\n" + diff.slice(-4000);
635
+ }
636
+
637
+ console.log(`Analyzing ${c("bold", filesChanged.length)} file(s) against ${c("bold", skillNames.length)} skill(s)...`);
638
+ console.log(c("dim", `Commit: ${commitHash?.slice(0, 8)} — ${commitMessage}`));
639
+ console.log(c("dim", `Branch: ${branch}\n`));
640
+
641
+ // Get project name
642
+ let projectName = null;
643
+ try {
644
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
645
+ if (fs.existsSync(pkgPath)) {
646
+ projectName = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name || null;
647
+ }
648
+ } catch { /* ignore */ }
649
+
650
+ try {
651
+ console.log(c("dim", "Sending to AI for analysis (this may take 10-30 seconds)...\n"));
652
+
653
+ const result = await fetchJSON(analyzeUrl, {
654
+ email: config.email,
655
+ commit_hash: commitHash,
656
+ commit_message: commitMessage,
657
+ branch,
658
+ project_name: projectName,
659
+ files_changed: filesChanged,
660
+ diff,
661
+ skill_names: skillNames,
662
+ });
663
+
664
+ if (result.error) {
665
+ throw new Error(result.error);
666
+ }
667
+
668
+ // Display results
669
+ console.log(c("blue", "━━━ Analysis Results ━━━\n"));
670
+ const score = result.overall_score || 0;
671
+ const scoreColor = score >= 80 ? "green" : score >= 50 ? "yellow" : "red";
672
+ console.log(`Overall Score: ${c(scoreColor, `${score}/100`)}\n`);
673
+
674
+ if (result.skill_scores) {
675
+ for (const [skillName, data] of Object.entries(result.skill_scores)) {
676
+ const s = data;
677
+ const sColor = s.score >= 80 ? "green" : s.score >= 50 ? "yellow" : "red";
678
+ console.log(`${c("bold", skillName)}: ${c(sColor, `${s.score}/100`)}`);
679
+ if (s.passed?.length) {
680
+ for (const p of s.passed) console.log(` ${c("green", "✓")} ${p}`);
681
+ }
682
+ if (s.failed?.length) {
683
+ for (const f of s.failed) console.log(` ${c("red", "✗")} ${f}`);
684
+ }
685
+ if (s.suggestions?.length) {
686
+ for (const sg of s.suggestions) console.log(` ${c("yellow", "→")} ${sg}`);
687
+ }
688
+ console.log("");
689
+ }
690
+ }
691
+
692
+ if (result.summary) {
693
+ console.log(c("dim", `Summary: ${result.summary}`));
694
+ }
695
+
696
+ console.log(c("dim", `\nTokens used: ${result.tokens_used || "?"} | Duration: ${result.duration_ms || "?"}ms\n`));
697
+
698
+ } catch (err) {
699
+ console.log(c("red", `\n✗ Analysis failed: ${err.message}`));
700
+ process.exit(1);
701
+ }
702
+ }
703
+
546
704
  // ── Main ──
547
705
 
548
706
  const command = process.argv[2] || "setup";
@@ -553,6 +711,7 @@ switch (command) {
553
711
  case "status": cmdStatus(); break;
554
712
  case "list": cmdList(); break;
555
713
  case "doctor": cmdDoctor(); break;
714
+ case "analyze": cmdAnalyze(); break;
556
715
  case "help": case "--help": case "-h":
557
716
  console.log(`
558
717
  ${c("blue", "AI Skills Framework")} — @valentia-ai-skills/framework
@@ -562,8 +721,12 @@ Usage:
562
721
  npx ai-skills update Re-fetch and update skills for your team
563
722
  npx ai-skills status Show installed skills, team, and tools
564
723
  npx ai-skills list List locally bundled skills
724
+ npx ai-skills analyze Analyze last commit against active skills
565
725
  npx ai-skills doctor Health check (config + API + tools)
566
726
 
727
+ Flags:
728
+ analyze --last Analyze the most recent commit
729
+
567
730
  Environment:
568
731
  AI_SKILLS_API_URL Override the Supabase Edge Function URL
569
732
  `); break;
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Background commit analyzer.
5
+ * Collects git diff + metadata, sends to the analyze-commit Edge Function.
6
+ * Runs as a detached process — never shows output to the developer.
7
+ * Everything is wrapped in try/catch — fails silently on any error.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { execSync } = require("child_process");
13
+ const https = require("https");
14
+ const http = require("http");
15
+
16
+ const PROJECT_ROOT = process.env.AI_SKILLS_CWD || process.cwd();
17
+ const CONFIG_PATH = path.join(PROJECT_ROOT, ".ai-skills.json");
18
+
19
+ // Default Edge Function URL (can be overridden in .ai-skills.json)
20
+ const DEFAULT_API_URL =
21
+ "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/analyze-commit";
22
+
23
+ function loadConfig() {
24
+ try {
25
+ if (fs.existsSync(CONFIG_PATH)) {
26
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
27
+ }
28
+ } catch {
29
+ // ignore
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function git(cmd) {
35
+ try {
36
+ return execSync(cmd, { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 10000 }).trim();
37
+ } catch {
38
+ return "";
39
+ }
40
+ }
41
+
42
+ function truncateDiff(diff, maxLen = 10000) {
43
+ if (diff.length <= maxLen) return diff;
44
+ const half = Math.floor(maxLen / 2);
45
+ return diff.slice(0, half) + "\n\n[...truncated...]\n\n" + diff.slice(-half);
46
+ }
47
+
48
+ function postJSON(url, body) {
49
+ return new Promise((resolve, reject) => {
50
+ const parsed = new URL(url);
51
+ const mod = parsed.protocol === "https:" ? https : http;
52
+ const postData = JSON.stringify(body);
53
+
54
+ const req = mod.request(
55
+ {
56
+ hostname: parsed.hostname,
57
+ port: parsed.port,
58
+ path: parsed.pathname + parsed.search,
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ "Content-Length": Buffer.byteLength(postData),
63
+ },
64
+ timeout: 60000, // 60s timeout for AI analysis
65
+ },
66
+ (res) => {
67
+ let data = "";
68
+ res.on("data", (chunk) => (data += chunk));
69
+ res.on("end", () => resolve(data));
70
+ }
71
+ );
72
+ req.on("error", reject);
73
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
74
+ req.write(postData);
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ async function main() {
80
+ // 1. Read config
81
+ const config = loadConfig();
82
+ if (!config || !config.email) return; // No config = not set up, silently exit
83
+
84
+ const email = config.email;
85
+ const skillNames = (config.skills || []).map((s) => s.name);
86
+ if (skillNames.length === 0) return; // No skills to check against
87
+
88
+ const apiUrl = config.analyzeUrl || process.env.AI_SKILLS_ANALYZE_URL || DEFAULT_API_URL;
89
+
90
+ // 2. Collect git data
91
+ const diff = git("git diff HEAD~1 HEAD");
92
+ if (!diff) return; // No diff = nothing to analyze
93
+
94
+ const commitHash = git("git rev-parse HEAD");
95
+ const commitMessage = git('git log -1 --format="%s"');
96
+ const branch = git("git branch --show-current");
97
+ const filesChangedRaw = git("git diff --name-only HEAD~1 HEAD");
98
+ const filesChanged = filesChangedRaw ? filesChangedRaw.split("\n").filter(Boolean) : [];
99
+
100
+ // Try to get project name from package.json
101
+ let projectName = null;
102
+ try {
103
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
104
+ if (fs.existsSync(pkgPath)) {
105
+ projectName = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name || null;
106
+ }
107
+ } catch {
108
+ // ignore
109
+ }
110
+
111
+ // 3. Send to Edge Function
112
+ await postJSON(apiUrl, {
113
+ email,
114
+ commit_hash: commitHash,
115
+ commit_message: commitMessage,
116
+ branch,
117
+ project_name: projectName,
118
+ files_changed: filesChanged,
119
+ diff: truncateDiff(diff),
120
+ skill_names: skillNames,
121
+ });
122
+ }
123
+
124
+ // Run and exit silently regardless of outcome
125
+ main().catch(() => {}).finally(() => process.exit(0));
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Installs the post-commit analysis hook into .git/hooks/post-commit.
5
+ * Called during `npx ai-skills setup`.
6
+ *
7
+ * The hook script delegates to the npm package's hooks/post-commit-analyze.js,
8
+ * so updates to the analysis logic are picked up automatically on npm update.
9
+ */
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ function installHook(projectRoot) {
15
+ const gitHooksDir = path.join(projectRoot, ".git", "hooks");
16
+
17
+ // Check if this is a git repo
18
+ if (!fs.existsSync(path.join(projectRoot, ".git"))) {
19
+ return { installed: false, reason: "Not a git repository" };
20
+ }
21
+
22
+ // Ensure hooks directory exists
23
+ if (!fs.existsSync(gitHooksDir)) {
24
+ fs.mkdirSync(gitHooksDir, { recursive: true });
25
+ }
26
+
27
+ const hookPath = path.join(gitHooksDir, "post-commit");
28
+
29
+ // Find the path to the hook script in the npm package
30
+ // Works whether running from node_modules or directly
31
+ const hookScript = path.join(__dirname, "post-commit-analyze.js");
32
+
33
+ // Generate the hook content — delegates to the npm package script
34
+ const hookContent = `#!/bin/sh
35
+ # AI Skills Framework — post-commit analysis hook
36
+ # Auto-installed by npx ai-skills setup
37
+ # Runs code analysis in the background after each commit (non-blocking)
38
+
39
+ node "${hookScript}" &
40
+ `;
41
+
42
+ // Check if an existing post-commit hook exists
43
+ if (fs.existsSync(hookPath)) {
44
+ const existing = fs.readFileSync(hookPath, "utf-8");
45
+ if (existing.includes("ai-skills")) {
46
+ // Already installed — update it
47
+ fs.writeFileSync(hookPath, hookContent);
48
+ fs.chmodSync(hookPath, "755");
49
+ return { installed: true, reason: "Updated existing hook" };
50
+ }
51
+
52
+ // There's a different post-commit hook — append our line
53
+ const appendLine = `\n# AI Skills Framework — post-commit analysis\nnode "${hookScript}" &\n`;
54
+ fs.appendFileSync(hookPath, appendLine);
55
+ fs.chmodSync(hookPath, "755");
56
+ return { installed: true, reason: "Appended to existing hook" };
57
+ }
58
+
59
+ // No existing hook — create new one
60
+ fs.writeFileSync(hookPath, hookContent);
61
+ fs.chmodSync(hookPath, "755");
62
+ return { installed: true, reason: "Hook installed" };
63
+ }
64
+
65
+ // If run directly (not required as module)
66
+ if (require.main === module) {
67
+ const result = installHook(process.cwd());
68
+ if (result.installed) {
69
+ console.log(`✓ Post-commit hook: ${result.reason}`);
70
+ } else {
71
+ console.log(`⚠ Post-commit hook skipped: ${result.reason}`);
72
+ }
73
+ }
74
+
75
+ module.exports = { installHook };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-commit hook entry point.
5
+ * Spawns the actual analysis as a detached background process so git returns immediately.
6
+ * This file is copied into .git/hooks/post-commit during `npx ai-skills setup`.
7
+ */
8
+
9
+ const { spawn } = require("child_process");
10
+ const path = require("path");
11
+
12
+ try {
13
+ // Find do-analysis.js relative to this hook's location in the npm package
14
+ // When installed via npm, hooks/ is inside node_modules/@valentia-ai-skills/framework/hooks/
15
+ const analysisScript = path.join(__dirname, "do-analysis.js");
16
+
17
+ const child = spawn("node", [analysisScript], {
18
+ detached: true,
19
+ stdio: "ignore",
20
+ cwd: process.cwd(),
21
+ env: { ...process.env, AI_SKILLS_CWD: process.cwd() },
22
+ });
23
+
24
+ child.unref();
25
+ } catch {
26
+ // Never block the developer — silently exit
27
+ }
28
+
29
+ process.exit(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valentia-ai-skills/framework",
3
- "version": "1.0.7",
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.9",
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",
@@ -34,6 +34,7 @@
34
34
  "src/",
35
35
  "skills/",
36
36
  "scripts/",
37
+ "hooks/",
37
38
  "README.md"
38
39
  ],
39
40
  "engines": {