From f411f52515b8728f4937c7bb79741b33a1d3ae73 Mon Sep 17 00:00:00 2001 From: To Nguyen Date: Thu, 19 Mar 2026 08:07:08 +0700 Subject: [PATCH] Add add-azure-skills command to integrate 192 Microsoft Azure skills - New command: add-azure-skills with bundle and individual skill install - 5 curated bundles: quick-start (8), popular (21), ai-ml (8), devops (8), security (8) - Auto-clone Microsoft Agent-Skills repo with cache and --refresh support - Skip existing skills, report install/skip/missing counts - Requires .agent init first, fully compatible with validate (78 checks pass) - Update README with Azure skills usage guide and CLI reference Co-Authored-By: Claude Opus 4.6 --- README.md | 48 ++++- src/cli.js | 27 ++- src/commands/add-azure-skills.js | 299 +++++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 src/commands/add-azure-skills.js diff --git a/README.md b/README.md index 370a32b..dcfe2ec 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,40 @@ antigravity-superpowers init --force --backup With `--backup`, the existing `.agent` is renamed to `.agent-backup-2026-03-19T10-30-00` before the new profile is copied. +### 6. Add Microsoft Azure skills + +Tích hợp 192 Azure skills từ [Microsoft Agent-Skills](https://github.com/MicrosoftDocs/Agent-Skills) vào project. Các skills này cung cấp tra cứu tài liệu Azure chính thức (troubleshooting, best practices, architecture, security...). + +```bash +# Xem các bundle có sẵn +antigravity-superpowers add-azure-skills --list-bundles + +# Cài bundle quick-start (8 skills cơ bản) +antigravity-superpowers add-azure-skills --bundle quick-start + +# Cài bundle popular (21 skills phổ biến) +antigravity-superpowers add-azure-skills --bundle popular + +# Cài từng skill riêng lẻ +antigravity-superpowers add-azure-skills --skill azure-functions --skill azure-cosmos-db + +# Kết hợp bundle + skill riêng +antigravity-superpowers add-azure-skills --bundle ai-ml --skill azure-container-apps + +# Xem toàn bộ 192 skills có sẵn +antigravity-superpowers add-azure-skills --list-skills +``` + +**Bundles có sẵn:** + +| Bundle | Skills | Mô tả | +| --- | --- | --- | +| `quick-start` | 8 | App Service, Functions, Blob Storage, SQL, Key Vault, Monitor... | +| `popular` | 21 | Các dịch vụ phổ biến nhất: Cosmos DB, AKS, Container Apps, Logic Apps... | +| `ai-ml` | 8 | AI Services, AI Vision, Bot Service, Machine Learning, Foundry... | +| `devops` | 8 | DevOps, Pipelines, Repos, Boards, Artifacts, Automation... | +| `security` | 8 | Key Vault, RBAC, Policy, Sentinel, Defender, Firewall... | + --- ## CLI Reference @@ -196,15 +230,23 @@ With `--backup`, the existing `.agent` is renamed to `.agent-backup-2026-03-19T1 antigravity-superpowers [options] Commands: - init [dir] Initialize .agent profile in a project - validate [dir] Validate .agent profile in a project - list List available skills + init [dir] Initialize .agent profile in a project + validate [dir] Validate .agent profile in a project + list List bundled skills + add-azure-skills [opts] Add Microsoft Azure skills to a project Init options: -f, --force Overwrite existing .agent directory -b, --backup Backup existing .agent before overwrite (use with --force) -n, --dry-run Preview files that would be copied +Azure skills options: + -B, --bundle Install a bundle (quick-start, popular, ai-ml, devops, security) + -s, --skill Install individual skill (repeatable) + --list-bundles List available bundles + --list-skills List all 192 Azure skills + --refresh Re-download skill repository + Global options: -v, --version Show version -h, --help Show help diff --git a/src/cli.js b/src/cli.js index 3569eab..51053fe 100644 --- a/src/cli.js +++ b/src/cli.js @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; import { initCommand } from "./commands/init.js"; import { validateCommand } from "./commands/validate.js"; import { listCommand } from "./commands/list.js"; +import { addAzureSkillsCommand } from "./commands/add-azure-skills.js"; async function getVersion() { const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url)); @@ -18,14 +19,24 @@ function helpText() { " antigravity-superpowers [options]", "", "Commands:", - " init [dir] [--force] [--backup] [--dry-run] Initialize .agent profile in a project", - " validate [dir] Validate .agent profile in a project", - " list List available skills", + " init [dir] [--force] [--backup] [--dry-run] Initialize .agent profile in a project", + " validate [dir] Validate .agent profile in a project", + " list List bundled skills", + " add-azure-skills [options] Add Microsoft Azure skills to a project", "", - "Options:", + "Init options:", " -f, --force Overwrite existing .agent directory", " -b, --backup Backup existing .agent before overwrite (use with --force)", " -n, --dry-run Preview files that would be copied", + "", + "Azure skills options:", + " -B, --bundle Install a skill bundle (quick-start, popular, ai-ml, devops, security)", + " -s, --skill Install individual skill (repeatable)", + " --list-bundles List available bundles", + " --list-skills List all available Azure skills", + " --refresh Re-download skill repository", + "", + "Global options:", " -v, --version Show version", " -h, --help Show help", ].join("\n"); @@ -68,6 +79,14 @@ export async function runCli(args, io = process) { }); } + if (command === "add-azure-skills") { + return addAzureSkillsCommand(rest, { + cwd: io.cwd?.() ?? process.cwd(), + stdout: io.stdout, + stderr: io.stderr, + }); + } + io.stderr.write(`Unknown command: ${command}\n\n${helpText()}\n`); return 1; } diff --git a/src/commands/add-azure-skills.js b/src/commands/add-azure-skills.js new file mode 100644 index 0000000..fb6245e --- /dev/null +++ b/src/commands/add-azure-skills.js @@ -0,0 +1,299 @@ +import { access, cp, mkdir, readdir, readFile, rm } from "node:fs/promises"; +import { constants as fsConstants } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { join, resolve } from "node:path"; + +const REPO_URL = "https://github.com/MicrosoftDocs/Agent-Skills.git"; +const CLONE_DIR = join(process.env.TMPDIR || "/tmp", "antigravity-azure-skills-cache"); + +const BUNDLES = { + "quick-start": [ + "azure-app-service", + "azure-blob-storage", + "azure-functions", + "azure-sql-database", + "azure-key-vault", + "azure-app-configuration", + "azure-microsoft-foundry", + "azure-monitor", + ], + popular: [ + "azure-app-service", + "azure-blob-storage", + "azure-cosmos-db", + "azure-container-registry", + "azure-functions", + "azure-sql-database", + "azure-key-vault", + "azure-virtual-machines", + "azure-kubernetes-service", + "azure-event-grid", + "azure-service-bus", + "azure-monitor", + "azure-app-configuration", + "azure-resource-manager", + "azure-cost-management", + "azure-logic-apps", + "azure-microsoft-foundry", + "azure-data-factory", + "azure-api-management", + "azure-container-apps", + "azure-defender-for-cloud", + ], + "ai-ml": [ + "azure-ai-services", + "azure-ai-vision", + "azure-bot-service", + "azure-cognitive-search", + "azure-machine-learning", + "azure-ai-foundry-local", + "azure-microsoft-foundry", + "azure-anomaly-detector", + ], + devops: [ + "azure-devops", + "azure-pipelines", + "azure-repos", + "azure-boards", + "azure-artifacts", + "azure-test-plans", + "azure-automation", + "azure-monitor", + ], + security: [ + "azure-key-vault", + "azure-rbac", + "azure-policy", + "azure-sentinel", + "azure-defender-for-cloud", + "azure-attestation", + "azure-confidential-computing", + "azure-firewall", + ], +}; + +async function exists(path) { + try { + await access(path, fsConstants.F_OK); + return true; + } catch { + return false; + } +} + +function parseArgs(args) { + const parsed = { + target: ".", + bundle: null, + skills: [], + listBundles: false, + listSkills: false, + refresh: false, + }; + let targetSet = false; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === "--bundle" || arg === "-B") { + parsed.bundle = args[++i]; + if (!parsed.bundle) throw new Error("--bundle requires a bundle name"); + continue; + } + if (arg === "--list-bundles") { + parsed.listBundles = true; + continue; + } + if (arg === "--list-skills") { + parsed.listSkills = true; + continue; + } + if (arg === "--refresh") { + parsed.refresh = true; + continue; + } + if (arg === "--skill" || arg === "-s") { + const skill = args[++i]; + if (!skill) throw new Error("--skill requires a skill name"); + parsed.skills.push(skill); + continue; + } + if (arg.startsWith("-")) { + throw new Error(`Unknown option: ${arg}`); + } + if (targetSet) { + throw new Error("Too many positional arguments."); + } + parsed.target = arg; + targetSet = true; + } + + return parsed; +} + +function cloneOrUpdate(stdout, refresh) { + if (refresh && exists(CLONE_DIR)) { + try { + execFileSync("rm", ["-rf", CLONE_DIR]); + } catch { + // ignore + } + } + + const cloneExists = + (() => { + try { + return require("node:fs").existsSync(join(CLONE_DIR, "skills")); + } catch { + return false; + } + })() || + (() => { + try { + execFileSync("test", ["-d", join(CLONE_DIR, "skills")]); + return true; + } catch { + return false; + } + })(); + + if (!cloneExists) { + stdout.write(`Cloning Microsoft Agent-Skills repository...\n`); + execFileSync("git", ["clone", "--depth", "1", REPO_URL, CLONE_DIR], { + stdio: ["ignore", "pipe", "pipe"], + }); + stdout.write(`Cached at ${CLONE_DIR}\n`); + } else { + stdout.write(`Using cached repository at ${CLONE_DIR}\n`); + stdout.write(`(Use --refresh to re-download)\n`); + } + + return join(CLONE_DIR, "skills"); +} + +export async function addAzureSkillsCommand(args, { cwd, stdout, stderr }) { + let parsed; + try { + parsed = parseArgs(args); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + stderr.write(`${message}\n`); + return 1; + } + + // List bundles + if (parsed.listBundles) { + stdout.write("Available bundles:\n\n"); + for (const [name, skills] of Object.entries(BUNDLES)) { + stdout.write(` ${name.padEnd(15)} (${skills.length} skills)\n`); + } + stdout.write(`\nUsage: antigravity-superpowers add-azure-skills --bundle \n`); + return 0; + } + + // List all available skills + if (parsed.listSkills) { + let skillsDir; + try { + skillsDir = cloneOrUpdate(stdout, parsed.refresh); + } catch (error) { + stderr.write(`Failed to clone repository: ${error.message}\n`); + return 1; + } + + const entries = await readdir(skillsDir, { withFileTypes: true }); + const skillNames = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + + stdout.write(`\n${skillNames.length} Azure skills available:\n\n`); + for (const name of skillNames) { + stdout.write(` ${name}\n`); + } + stdout.write( + `\nUsage: antigravity-superpowers add-azure-skills --skill [--skill ...]\n`, + ); + return 0; + } + + // Determine which skills to install + let skillsToInstall = []; + + if (parsed.bundle) { + const bundleSkills = BUNDLES[parsed.bundle]; + if (!bundleSkills) { + stderr.write( + `Unknown bundle: ${parsed.bundle}\nAvailable: ${Object.keys(BUNDLES).join(", ")}\n`, + ); + return 1; + } + skillsToInstall = bundleSkills; + } + + if (parsed.skills.length > 0) { + skillsToInstall = [...new Set([...skillsToInstall, ...parsed.skills])]; + } + + if (skillsToInstall.length === 0) { + stderr.write( + "No skills specified. Use --bundle or --skill .\n" + + "Run with --list-bundles or --list-skills to see available options.\n", + ); + return 1; + } + + // Clone/update repo + let skillsDir; + try { + skillsDir = cloneOrUpdate(stdout, parsed.refresh); + } catch (error) { + stderr.write(`Failed to clone repository: ${error.message}\n`); + return 1; + } + + // Resolve target + const targetDir = resolve(cwd, parsed.target); + const agentSkillsDir = join(targetDir, ".agent", "skills"); + + if (!(await exists(join(targetDir, ".agent")))) { + stderr.write( + `No .agent directory found at ${targetDir}\nRun 'antigravity-superpowers init' first.\n`, + ); + return 1; + } + + // Copy skills + let installed = 0; + let skipped = 0; + const missing = []; + + for (const skill of skillsToInstall) { + const src = join(skillsDir, skill); + const dest = join(agentSkillsDir, skill); + + if (!(await exists(src))) { + missing.push(skill); + continue; + } + + if (await exists(dest)) { + stdout.write(` [SKIP] ${skill} (already exists)\n`); + skipped++; + continue; + } + + await cp(src, dest, { recursive: true }); + stdout.write(` [ADD] ${skill}\n`); + installed++; + } + + stdout.write(`\nInstalled: ${installed}, Skipped: ${skipped}`); + if (missing.length > 0) { + stdout.write(`, Not found: ${missing.length}`); + stderr.write(`\nMissing skills: ${missing.join(", ")}\n`); + } + stdout.write(`\n`); + + return missing.length > 0 ? 1 : 0; +}