From f2338a754c04a9b4005b1867e704e4f5d4afdbd1 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 20 Jan 2026 11:52:43 -0500 Subject: [PATCH 1/3] initial skills --- .github/workflows/publish.yml | 19 + packages/b2c-cli/package.json | 3 + packages/b2c-cli/src/commands/setup/skills.ts | 378 ++++++++++++++++++ packages/b2c-cli/src/i18n/locales/en.ts | 23 ++ packages/b2c-tooling-sdk/package.json | 11 + packages/b2c-tooling-sdk/src/skills/agents.ts | 169 ++++++++ packages/b2c-tooling-sdk/src/skills/github.ts | 262 ++++++++++++ packages/b2c-tooling-sdk/src/skills/index.ts | 79 ++++ .../b2c-tooling-sdk/src/skills/installer.ts | 225 +++++++++++ packages/b2c-tooling-sdk/src/skills/parser.ts | 154 +++++++ packages/b2c-tooling-sdk/src/skills/types.ts | 150 +++++++ 11 files changed, 1473 insertions(+) create mode 100644 packages/b2c-cli/src/commands/setup/skills.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/agents.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/github.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/index.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/installer.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/parser.ts create mode 100644 packages/b2c-tooling-sdk/src/skills/types.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e068eacb..86ba4983 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -138,3 +138,22 @@ jobs: gh release create "$GITHUB_REF_NAME" --notes-file /tmp/release-notes.md $PRERELEASE_FLAG env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Package skills artifacts + if: steps.release-type.outputs.type == 'stable' + run: | + # Create b2c-skills.zip containing plugins/b2c/skills/ + cd plugins/b2c && zip -r ../../b2c-skills.zip skills/ + cd ../.. + # Create b2c-cli-skills.zip containing plugins/b2c-cli/skills/ + cd plugins/b2c-cli && zip -r ../../b2c-cli-skills.zip skills/ + cd ../.. + echo "Created skills artifacts:" + ls -la *.zip + + - name: Upload skills to release + if: steps.release-type.outputs.type == 'stable' + run: | + gh release upload "$GITHUB_REF_NAME" b2c-skills.zip b2c-cli-skills.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index ee918258..5fd61e2f 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -131,6 +131,9 @@ "description": "Browse and retrieve SCAPI schema specifications" } } + }, + "setup": { + "description": "Setup commands for development environment" } } }, diff --git a/packages/b2c-cli/src/commands/setup/skills.ts b/packages/b2c-cli/src/commands/setup/skills.ts new file mode 100644 index 00000000..385b59d7 --- /dev/null +++ b/packages/b2c-cli/src/commands/setup/skills.ts @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Args, Flags, ux} from '@oclif/core'; +import {BaseCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + type IdeType, + type SkillSet, + type SkillMetadata, + type InstallSkillsResult, + ALL_IDE_TYPES, + detectInstalledIdes, + downloadSkillsArtifact, + scanSkills, + installSkills, + getIdeDisplayName, + findSkillsByName, +} from '@salesforce/b2c-tooling-sdk/skills'; +import {t} from '../../i18n/index.js'; + +/** + * Simple confirmation prompt. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + + return new Promise((resolve) => { + rl.question(`${message} `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Simple selection prompt (returns selected indices). + */ +async function selectMultiple(message: string, options: string[]): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + + // Display options + process.stderr.write(`${message}\n`); + for (const [i, opt] of options.entries()) { + process.stderr.write(` ${i + 1}. ${opt}\n`); + } + + return new Promise((resolve) => { + rl.question('Enter numbers separated by commas (or "all" for all): ', (answer) => { + rl.close(); + if (answer.toLowerCase() === 'all') { + resolve(options.map((_, i) => i)); + return; + } + const indices = answer + .split(',') + .map((s) => Number.parseInt(s.trim(), 10) - 1) + .filter((n) => !Number.isNaN(n) && n >= 0 && n < options.length); + resolve(indices); + }); + }); +} + +/** + * Table columns for skill listing. + */ +const SKILL_COLUMNS: Record> = { + name: { + header: 'Name', + get: (s) => s.name, + }, + description: { + header: 'Description', + get: (s) => s.description, + }, + skillSet: { + header: 'Set', + get: (s) => s.skillSet, + }, + hasReferences: { + header: 'Refs', + get: (s) => (s.hasReferences ? 'Yes' : '-'), + }, +}; + +const DEFAULT_SKILL_COLUMNS = ['name', 'description', 'skillSet']; + +/** + * Response type for JSON output. + */ +interface SetupSkillsResponse { + skills?: SkillMetadata[]; + installed?: InstallSkillsResult['installed']; + skipped?: InstallSkillsResult['skipped']; + errors?: InstallSkillsResult['errors']; +} + +export default class SetupSkills extends BaseCommand { + static args = { + skillset: Args.string({ + description: 'Skill set to install: b2c, b2c-cli, or all', + options: ['b2c', 'b2c-cli', 'all'], + default: 'all', + }), + }; + + static description = t('commands.setup.skills.description', 'Install agent skills for AI-powered IDEs'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> b2c-cli --ide cursor --global', + '<%= config.bin %> <%= command.id %> --list', + '<%= config.bin %> <%= command.id %> --skill b2c-code --skill b2c-webdav --ide cursor', + '<%= config.bin %> <%= command.id %> --global --update --force', + '<%= config.bin %> <%= command.id %> --version v0.1.0', + ]; + + static flags = { + ...BaseCommand.baseFlags, + list: Flags.boolean({ + char: 'l', + description: 'List available skills without installing', + default: false, + }), + skill: Flags.string({ + description: 'Install specific skill(s) (can be specified multiple times)', + multiple: true, + }), + ide: Flags.string({ + description: 'Target IDE(s): claude-code, cursor, windsurf, github-copilot, codex, opencode, manual', + options: ALL_IDE_TYPES, + multiple: true, + }), + global: Flags.boolean({ + char: 'g', + description: 'Install to user home directory (global)', + default: false, + }), + update: Flags.boolean({ + char: 'u', + description: 'Update existing skills (overwrite)', + default: false, + }), + version: Flags.string({ + description: 'Specific release version (default: latest)', + }), + force: Flags.boolean({ + description: 'Skip confirmation prompts (non-interactive)', + default: false, + }), + }; + + async run(): Promise { + const skillsets: SkillSet[] = this.args.skillset === 'all' ? ['b2c', 'b2c-cli'] : [this.args.skillset as SkillSet]; + + // Download and scan skills + this.log( + t('commands.setup.skills.downloading', 'Downloading skills from release {{version}}...', { + version: this.flags.version || 'latest', + }), + ); + + // Download skills for all skillsets in parallel + const downloadResults = await Promise.all( + skillsets.map(async (skillset) => { + const skillsDir = await downloadSkillsArtifact(skillset, { + version: this.flags.version, + }); + const skills = await scanSkills(skillsDir, skillset); + return {skillset, skillsDir, skills}; + }), + ); + + const allSkills: SkillMetadata[] = []; + const skillsDirs: Record = {} as Record; + + for (const {skillset, skillsDir, skills} of downloadResults) { + skillsDirs[skillset] = skillsDir; + allSkills.push(...skills); + } + + // List mode + if (this.flags.list) { + if (this.jsonEnabled()) { + return {skills: allSkills}; + } + + if (allSkills.length === 0) { + ux.stdout(t('commands.setup.skills.noSkills', 'No skills found.')); + return {skills: []}; + } + + createTable(SKILL_COLUMNS).render(allSkills, DEFAULT_SKILL_COLUMNS); + return {skills: allSkills}; + } + + // Filter skills if --skill specified + let skillsToInstall = allSkills; + if (this.flags.skill && this.flags.skill.length > 0) { + const {found, notFound} = findSkillsByName(allSkills, this.flags.skill); + if (notFound.length > 0) { + this.warn( + t('commands.setup.skills.notFound', 'Skills not found: {{skills}}', { + skills: notFound.join(', '), + }), + ); + } + skillsToInstall = found; + } + + if (skillsToInstall.length === 0) { + this.log(t('commands.setup.skills.noSkillsToInstall', 'No skills to install.')); + return {}; + } + + // Determine target IDEs + let targetIdes: IdeType[] = (this.flags.ide as IdeType[]) || []; + + if (targetIdes.length === 0) { + // Auto-detect installed IDEs + this.log(t('commands.setup.skills.detecting', 'Detecting installed IDEs...')); + const detectedIdes = await detectInstalledIdes(); + + if (detectedIdes.length === 0) { + this.log( + t( + 'commands.setup.skills.noIdesDetected', + 'No IDEs detected. Use --ide to specify target (e.g., --ide cursor --ide manual).', + ), + ); + return {}; + } + + if (this.flags.force) { + // Non-interactive: use all detected IDEs + targetIdes = detectedIdes; + } else { + // Interactive: let user select + const ideNames = detectedIdes.map((ide) => getIdeDisplayName(ide)); + const selected = await selectMultiple('Select target IDEs:', ideNames); + targetIdes = selected.map((i) => detectedIdes[i]); + } + } + + if (targetIdes.length === 0) { + this.log(t('commands.setup.skills.noIdesSelected', 'No IDEs selected.')); + return {}; + } + + // Claude Code marketplace recommendation + if (targetIdes.includes('claude-code')) { + this.log(''); + this.log( + t( + 'commands.setup.skills.claudeCodeRecommendation', + 'Note: For Claude Code, we recommend using the plugin marketplace for automatic updates:\n' + + ' claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling\n' + + ' claude plugin install b2c-cli\n' + + ' claude plugin install b2c\n\n' + + 'Use --ide manual for manual installation to the same paths.', + ), + ); + + if (!this.flags.force) { + const proceed = await confirm('Continue with Claude Code installation? (y/n)'); + if (!proceed) { + targetIdes = targetIdes.filter((ide) => ide !== 'claude-code'); + if (targetIdes.length === 0) { + this.log(t('commands.setup.skills.cancelled', 'Installation cancelled.')); + return {}; + } + } + } + } + + // Show installation preview + const scope = this.flags.global ? 'global (user home)' : 'project'; + this.log(''); + this.log( + t('commands.setup.skills.preview', 'Installing {{count}} skills to {{ides}} ({{scope}})', { + count: skillsToInstall.length, + ides: targetIdes.map((ide) => getIdeDisplayName(ide)).join(', '), + scope, + }), + ); + + // Confirm installation + if (!this.flags.force) { + const proceed = await confirm('Proceed with installation? (y/n)'); + if (!proceed) { + this.log(t('commands.setup.skills.cancelled', 'Installation cancelled.')); + return {}; + } + } + + // Install skills for each skillset + // Install skills for all skillsets in parallel + const installPromises = skillsets + .map((skillset) => { + const skillsForSet = skillsToInstall.filter((s) => s.skillSet === skillset); + if (skillsForSet.length === 0) return null; + return installSkills(skillsForSet, skillsDirs[skillset], { + ides: targetIdes, + global: this.flags.global, + update: this.flags.update, + projectRoot: process.cwd(), + }); + }) + .filter((p): p is Promise => p !== null); + + const installResults = await Promise.all(installPromises); + + const combinedResult: InstallSkillsResult = { + installed: [], + skipped: [], + errors: [], + }; + + for (const result of installResults) { + combinedResult.installed.push(...result.installed); + combinedResult.skipped.push(...result.skipped); + combinedResult.errors.push(...result.errors); + } + + // Report results + if (combinedResult.installed.length > 0) { + this.log(''); + this.log( + t('commands.setup.skills.installed', 'Successfully installed {{count}} skill(s):', { + count: combinedResult.installed.length, + }), + ); + for (const item of combinedResult.installed) { + this.log(` - ${item.skill} → ${item.path}`); + } + } + + if (combinedResult.skipped.length > 0) { + this.log(''); + this.log( + t('commands.setup.skills.skippedCount', 'Skipped {{count}} skill(s):', { + count: combinedResult.skipped.length, + }), + ); + for (const item of combinedResult.skipped) { + this.log(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.reason}`); + } + } + + if (combinedResult.errors.length > 0) { + this.log(''); + this.warn( + t('commands.setup.skills.errorsCount', 'Failed to install {{count}} skill(s):', { + count: combinedResult.errors.length, + }), + ); + for (const item of combinedResult.errors) { + this.log(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.error}`); + } + } + + return { + installed: combinedResult.installed, + skipped: combinedResult.skipped, + errors: combinedResult.errors, + }; + } +} diff --git a/packages/b2c-cli/src/i18n/locales/en.ts b/packages/b2c-cli/src/i18n/locales/en.ts index abcca1f4..2fa216ec 100644 --- a/packages/b2c-cli/src/i18n/locales/en.ts +++ b/packages/b2c-cli/src/i18n/locales/en.ts @@ -119,5 +119,28 @@ export const en = { }, }, }, + setup: { + skills: { + description: 'Install agent skills for AI-powered IDEs', + downloading: 'Downloading skills from release {{version}}...', + detecting: 'Detecting installed IDEs...', + noSkills: 'No skills found.', + noSkillsToInstall: 'No skills to install.', + notFound: 'Skills not found: {{skills}}', + noIdesDetected: 'No IDEs detected. Use --ide to specify target (e.g., --ide cursor --ide manual).', + noIdesSelected: 'No IDEs selected.', + claudeCodeRecommendation: + 'Note: For Claude Code, we recommend using the plugin marketplace for automatic updates:\n' + + ' claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling\n' + + ' claude plugin install b2c-cli\n' + + ' claude plugin install b2c\n\n' + + 'Use --ide manual for manual installation to the same paths.', + preview: 'Installing {{count}} skills to {{ides}} ({{scope}})', + cancelled: 'Installation cancelled.', + installed: 'Successfully installed {{count}} skill(s):', + skippedCount: 'Skipped {{count}} skill(s):', + errorsCount: 'Failed to install {{count}} skill(s):', + }, + }, }, }; diff --git a/packages/b2c-tooling-sdk/package.json b/packages/b2c-tooling-sdk/package.json index 11b8d844..31330611 100644 --- a/packages/b2c-tooling-sdk/package.json +++ b/packages/b2c-tooling-sdk/package.json @@ -199,6 +199,17 @@ "types": "./dist/cjs/discovery/index.d.ts", "default": "./dist/cjs/discovery/index.js" } + }, + "./skills": { + "development": "./src/skills/index.ts", + "import": { + "types": "./dist/esm/skills/index.d.ts", + "default": "./dist/esm/skills/index.js" + }, + "require": { + "types": "./dist/cjs/skills/index.d.ts", + "default": "./dist/cjs/skills/index.js" + } } }, "main": "./dist/cjs/index.js", diff --git a/packages/b2c-tooling-sdk/src/skills/agents.ts b/packages/b2c-tooling-sdk/src/skills/agents.ts new file mode 100644 index 00000000..d8de4049 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/agents.ts @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import type {IdeConfig, IdeType} from './types.js'; + +const home = os.homedir(); + +/** + * IDE configurations with paths and detection logic. + * Based on patterns from the add-skill reference implementation. + */ +export const IDE_CONFIGS: Record = { + 'claude-code': { + id: 'claude-code', + displayName: 'Claude Code', + paths: { + projectDir: '.claude/skills', + globalDir: path.join(home, '.claude/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.claude')); + }, + }, + cursor: { + id: 'cursor', + displayName: 'Cursor', + paths: { + projectDir: '.cursor/skills', + globalDir: path.join(home, '.cursor/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.cursor')); + }, + }, + windsurf: { + id: 'windsurf', + displayName: 'Windsurf', + paths: { + projectDir: '.windsurf/skills', + globalDir: path.join(home, '.codeium/windsurf/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.codeium/windsurf')); + }, + }, + 'github-copilot': { + id: 'github-copilot', + displayName: 'GitHub Copilot', + paths: { + projectDir: '.github/skills', + globalDir: path.join(home, '.copilot/skills'), + }, + detectInstalled: async () => { + // Check for either .github directory (project-based) or ~/.copilot + return fs.existsSync(path.join(home, '.copilot')); + }, + }, + codex: { + id: 'codex', + displayName: 'OpenAI Codex', + paths: { + projectDir: '.codex/skills', + globalDir: path.join(home, '.codex/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.codex')); + }, + }, + opencode: { + id: 'opencode', + displayName: 'OpenCode', + paths: { + projectDir: '.opencode/skills', + globalDir: path.join(home, '.config/opencode/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.config/opencode')); + }, + }, + manual: { + id: 'manual', + displayName: 'Manual Installation', + paths: { + // Manual mode uses same paths as Claude Code + projectDir: '.claude/skills', + globalDir: path.join(home, '.claude/skills'), + }, + detectInstalled: async () => { + // Manual is always "available" as a fallback + return true; + }, + }, +}; + +/** + * All supported IDE types in display order. + */ +export const ALL_IDE_TYPES: IdeType[] = [ + 'claude-code', + 'cursor', + 'windsurf', + 'github-copilot', + 'codex', + 'opencode', + 'manual', +]; + +/** + * Detect which IDEs are installed on the system. + * + * @returns Array of IDE types that appear to be installed + */ +export async function detectInstalledIdes(): Promise { + const installed: IdeType[] = []; + + for (const ideType of ALL_IDE_TYPES) { + // Skip 'manual' from auto-detection since it's always available + if (ideType === 'manual') { + continue; + } + + const config = IDE_CONFIGS[ideType]; + const isInstalled = await config.detectInstalled(); + if (isInstalled) { + installed.push(ideType); + } + } + + return installed; +} + +/** + * Get the installation path for a skill. + * + * @param ide - Target IDE + * @param skillName - Name of the skill + * @param options - Installation options + * @returns Absolute path where the skill would be installed + */ +export function getSkillInstallPath( + ide: IdeType, + skillName: string, + options: {global: boolean; projectRoot?: string}, +): string { + const config = IDE_CONFIGS[ide]; + + if (options.global) { + return path.join(config.paths.globalDir, skillName); + } + + const projectRoot = options.projectRoot || process.cwd(); + return path.join(projectRoot, config.paths.projectDir, skillName); +} + +/** + * Get the display name for an IDE. + * + * @param ide - IDE type + * @returns Human-readable display name + */ +export function getIdeDisplayName(ide: IdeType): string { + return IDE_CONFIGS[ide].displayName; +} diff --git a/packages/b2c-tooling-sdk/src/skills/github.ts b/packages/b2c-tooling-sdk/src/skills/github.ts new file mode 100644 index 00000000..f7c172b6 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/github.ts @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import JSZip from 'jszip'; +import type {CachedArtifact, DownloadSkillsOptions, ReleaseInfo, SkillSet} from './types.js'; +import {getLogger} from '../logging/logger.js'; + +const GITHUB_REPO = 'SalesforceCommerceCloud/b2c-developer-tooling'; +const GITHUB_API_BASE = 'https://api.github.com'; + +/** + * Asset filename patterns for skill archives. + */ +const ASSET_NAMES: Record = { + b2c: 'b2c-skills.zip', + 'b2c-cli': 'b2c-cli-skills.zip', +}; + +/** + * Get the cache directory for skills. + * Uses XDG_CACHE_HOME on Linux, ~/.cache otherwise. + * + * @returns Absolute path to cache directory + */ +export function getCacheDir(): string { + const xdgCache = process.env.XDG_CACHE_HOME; + const baseCache = xdgCache || path.join(os.homedir(), '.cache'); + return path.join(baseCache, 'b2c-cli', 'skills'); +} + +/** + * Parse GitHub API release response into ReleaseInfo. + */ +function parseRelease(release: { + tag_name: string; + published_at: string; + assets: Array<{name: string; browser_download_url: string}>; +}): ReleaseInfo { + const b2cAsset = release.assets.find((a) => a.name === ASSET_NAMES['b2c']); + const b2cCliAsset = release.assets.find((a) => a.name === ASSET_NAMES['b2c-cli']); + + return { + tagName: release.tag_name, + version: release.tag_name.replace(/^v/, ''), + publishedAt: release.published_at, + b2cSkillsAssetUrl: b2cAsset?.browser_download_url ?? null, + b2cCliSkillsAssetUrl: b2cCliAsset?.browser_download_url ?? null, + }; +} + +/** + * Fetch release information from GitHub API. + * + * @param version - 'latest' or specific version (e.g., 'v0.1.0') + * @returns Release information + * @throws Error if release not found or API request fails + */ +export async function getRelease(version: string = 'latest'): Promise { + const logger = getLogger(); + const endpoint = + version === 'latest' + ? `${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest` + : `${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/tags/${version.startsWith('v') ? version : `v${version}`}`; + + logger.debug({endpoint}, 'Fetching release info'); + + const response = await fetch(endpoint, { + headers: { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'b2c-cli', + }, + }); + + if (!response.ok) { + if (response.status === 404) { + throw new Error(`Release not found: ${version}`); + } + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as { + tag_name: string; + published_at: string; + assets: Array<{name: string; browser_download_url: string}>; + }; + + return parseRelease(data); +} + +/** + * List available releases with skills artifacts. + * + * @param limit - Maximum number of releases to return (default: 10) + * @returns Array of release information + */ +export async function listReleases(limit: number = 10): Promise { + const logger = getLogger(); + const endpoint = `${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases?per_page=${limit}`; + + logger.debug({endpoint}, 'Listing releases'); + + const response = await fetch(endpoint, { + headers: { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'b2c-cli', + }, + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as Array<{ + tag_name: string; + published_at: string; + assets: Array<{name: string; browser_download_url: string}>; + }>; + + // Only return releases that have at least one skills artifact + return data.map(parseRelease).filter((r) => r.b2cSkillsAssetUrl || r.b2cCliSkillsAssetUrl); +} + +/** + * Get cached artifact metadata if available. + * + * @param version - Release version + * @param skillSet - Skill set to check + * @returns Cached artifact info or null if not cached + */ +export function getCachedArtifact(version: string, skillSet: SkillSet): CachedArtifact | null { + const cacheDir = getCacheDir(); + const manifestPath = path.join(cacheDir, version, skillSet, 'manifest.json'); + + if (!fs.existsSync(manifestPath)) { + return null; + } + + try { + const content = fs.readFileSync(manifestPath, 'utf-8'); + return JSON.parse(content) as CachedArtifact; + } catch { + return null; + } +} + +/** + * Download and extract skills artifact. + * + * @param skillSet - Which skill set to download ('b2c' or 'b2c-cli') + * @param options - Download options + * @returns Path to extracted skills directory + * @throws Error if download fails or artifact not available + */ +export async function downloadSkillsArtifact(skillSet: SkillSet, options: DownloadSkillsOptions = {}): Promise { + const logger = getLogger(); + const {version = 'latest', forceDownload = false} = options; + + // Get release info to determine version and asset URL + const release = await getRelease(version); + const actualVersion = release.tagName; + + // Check cache first (unless forced) + if (!forceDownload) { + const cached = getCachedArtifact(actualVersion, skillSet); + if (cached && fs.existsSync(cached.path)) { + logger.debug({version: actualVersion, skillSet, path: cached.path}, 'Using cached skills'); + return cached.path; + } + } + + // Determine asset URL + const assetUrl = skillSet === 'b2c' ? release.b2cSkillsAssetUrl : release.b2cCliSkillsAssetUrl; + + if (!assetUrl) { + throw new Error(`Skills artifact '${ASSET_NAMES[skillSet]}' not found in release ${actualVersion}`); + } + + logger.debug({url: assetUrl, skillSet}, 'Downloading skills artifact'); + + // Download artifact + const response = await fetch(assetUrl, { + headers: { + 'User-Agent': 'b2c-cli', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to download skills: ${response.status} ${response.statusText}`); + } + + const zipBuffer = Buffer.from(await response.arrayBuffer()); + logger.debug({size: zipBuffer.length}, 'Downloaded skills archive'); + + // Extract to cache directory + const cacheDir = options.cacheDir || getCacheDir(); + const extractDir = path.join(cacheDir, actualVersion, skillSet); + + // Clean existing extraction if present + if (fs.existsSync(extractDir)) { + await fs.promises.rm(extractDir, {recursive: true}); + } + + await fs.promises.mkdir(extractDir, {recursive: true}); + + // Extract zip contents + const zip = await JSZip.loadAsync(zipBuffer); + let extractedCount = 0; + + for (const [relativePath, zipEntry] of Object.entries(zip.files)) { + if (zipEntry.dir) { + await fs.promises.mkdir(path.join(extractDir, relativePath), {recursive: true}); + continue; + } + + const targetPath = path.join(extractDir, relativePath); + const targetDir = path.dirname(targetPath); + + // Ensure parent directory exists + await fs.promises.mkdir(targetDir, {recursive: true}); + + // Write file + const content = await zipEntry.async('nodebuffer'); + await fs.promises.writeFile(targetPath, content); + extractedCount++; + } + + logger.debug({extractDir, fileCount: extractedCount}, 'Extracted skills'); + + // Write cache manifest + const manifest: CachedArtifact = { + version: actualVersion, + path: extractDir, + downloadedAt: new Date().toISOString(), + }; + await fs.promises.writeFile(path.join(extractDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); + + return extractDir; +} + +/** + * Clear the skills cache. + * + * @param version - Optional specific version to clear (default: all) + */ +export async function clearCache(version?: string): Promise { + const cacheDir = getCacheDir(); + + if (version) { + const versionDir = path.join(cacheDir, version); + if (fs.existsSync(versionDir)) { + await fs.promises.rm(versionDir, {recursive: true}); + } + } else if (fs.existsSync(cacheDir)) { + await fs.promises.rm(cacheDir, {recursive: true}); + } +} diff --git a/packages/b2c-tooling-sdk/src/skills/index.ts b/packages/b2c-tooling-sdk/src/skills/index.ts new file mode 100644 index 00000000..2259b496 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/index.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Skills management module for downloading and installing agent skills. + * + * This module provides functionality to: + * - Download skills artifacts from GitHub releases + * - Detect installed IDEs + * - Install skills to various IDE configurations + * - Manage skills cache + * + * @example + * ```typescript + * import { + * downloadSkillsArtifact, + * scanSkills, + * installSkills, + * detectInstalledIdes, + * } from '@salesforce/b2c-tooling-sdk/skills'; + * + * // Download and extract skills + * const skillsDir = await downloadSkillsArtifact('b2c'); + * + * // Scan for available skills + * const skills = await scanSkills(skillsDir, 'b2c'); + * + * // Detect installed IDEs + * const ides = await detectInstalledIdes(); + * + * // Install skills + * const result = await installSkills(skills, skillsDir, { + * ides: ['cursor'], + * global: true, + * update: false, + * }); + * ``` + * + * @module skills + */ + +// Types +export type { + IdeType, + SkillSet, + IdePaths, + IdeConfig, + SkillMetadata, + ReleaseInfo, + DownloadSkillsOptions, + InstallSkillsOptions, + SkillInstallation, + SkillSkipped, + SkillError, + InstallSkillsResult, + CachedArtifact, +} from './types.js'; + +// Agent/IDE utilities +export {IDE_CONFIGS, ALL_IDE_TYPES, detectInstalledIdes, getSkillInstallPath, getIdeDisplayName} from './agents.js'; + +// GitHub/download utilities +export { + getCacheDir, + getRelease, + listReleases, + getCachedArtifact, + downloadSkillsArtifact, + clearCache, +} from './github.js'; + +// Skill parsing +export {parseSkillFrontmatter, scanSkills, filterSkillsByName, findSkillsByName} from './parser.js'; + +// Installation +export {isSkillInstalled, installSkills, removeSkill} from './installer.js'; diff --git a/packages/b2c-tooling-sdk/src/skills/installer.ts b/packages/b2c-tooling-sdk/src/skills/installer.ts new file mode 100644 index 00000000..7fbd0aab --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/installer.ts @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type {IdeType, InstallSkillsOptions, InstallSkillsResult, SkillMetadata} from './types.js'; +import {getSkillInstallPath} from './agents.js'; +import {getLogger} from '../logging/logger.js'; + +/** + * Sanitize a skill name to prevent path traversal attacks. + * + * @param name - Skill name to sanitize + * @returns Sanitized name safe for use in file paths + */ +function sanitizeName(name: string): string { + // Remove path separators and null bytes + let sanitized = name.replace(/[/\\:\0]/g, ''); + + // Remove leading/trailing dots and spaces + sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, ''); + + // Limit length + if (sanitized.length > 255) { + sanitized = sanitized.slice(0, 255); + } + + return sanitized; +} + +/** + * Validate that a path is safely within a base directory. + * + * @param targetPath - Path to validate + * @param baseDir - Base directory that must contain targetPath + * @returns true if path is safe, false otherwise + */ +function isPathSafe(targetPath: string, baseDir: string): boolean { + const resolvedTarget = path.resolve(targetPath); + const resolvedBase = path.resolve(baseDir); + + // Ensure the target is within the base directory + return resolvedTarget.startsWith(resolvedBase + path.sep) || resolvedTarget === resolvedBase; +} + +/** + * Recursively copy a directory. + * + * @param source - Source directory + * @param target - Target directory + */ +async function copyDirectory(source: string, target: string): Promise { + const logger = getLogger(); + let fileCount = 0; + + await fs.promises.mkdir(target, {recursive: true}); + + const entries = await fs.promises.readdir(source, {withFileTypes: true}); + + for (const entry of entries) { + const sourcePath = path.join(source, entry.name); + const targetPath = path.join(target, entry.name); + + if (entry.isDirectory()) { + fileCount += await copyDirectory(sourcePath, targetPath); + } else if (entry.isFile()) { + await fs.promises.copyFile(sourcePath, targetPath); + fileCount++; + } + // Skip symlinks and other special files for security + } + + logger.debug({source, target, fileCount}, 'Copied directory'); + return fileCount; +} + +/** + * Check if a skill is already installed. + * + * @param skillName - Name of the skill + * @param ide - Target IDE + * @param options - Installation options + * @returns true if skill is installed, false otherwise + */ +export function isSkillInstalled( + skillName: string, + ide: IdeType, + options: {global: boolean; projectRoot?: string}, +): boolean { + const installPath = getSkillInstallPath(ide, skillName, options); + return fs.existsSync(installPath); +} + +/** + * Install skills to target IDE directories. + * + * @param skills - Skills to install + * @param sourceDir - Directory containing extracted skills + * @param options - Installation options + * @returns Installation results + */ +export async function installSkills( + skills: SkillMetadata[], + sourceDir: string, + options: InstallSkillsOptions, +): Promise { + const logger = getLogger(); + const result: InstallSkillsResult = { + installed: [], + skipped: [], + errors: [], + }; + + for (const skill of skills) { + const sanitizedName = sanitizeName(skill.name); + + if (sanitizedName !== skill.name) { + logger.warn({original: skill.name, sanitized: sanitizedName}, 'Skill name was sanitized'); + } + + // Source path: sourceDir/skills/skill-path/ + const sourcePath = path.join(sourceDir, 'skills', skill.path); + + if (!fs.existsSync(sourcePath)) { + for (const ide of options.ides) { + result.errors.push({ + skill: skill.name, + ide, + error: `Source directory not found: ${sourcePath}`, + }); + } + continue; + } + + for (const ide of options.ides) { + try { + const targetPath = getSkillInstallPath(ide, sanitizedName, { + global: options.global, + projectRoot: options.projectRoot, + }); + + // Get the base directory for path safety validation + const baseDir = path.dirname(targetPath); + + // Validate path safety + if (!isPathSafe(targetPath, baseDir)) { + result.errors.push({ + skill: skill.name, + ide, + error: 'Path validation failed: potential directory traversal', + }); + continue; + } + + // Check if already installed + if (fs.existsSync(targetPath)) { + if (!options.update) { + result.skipped.push({ + skill: skill.name, + ide, + reason: 'Already installed (use --update to overwrite)', + }); + continue; + } + + // Remove existing for update + await fs.promises.rm(targetPath, {recursive: true}); + } + + // Copy skill directory + await copyDirectory(sourcePath, targetPath); + + result.installed.push({ + skill: skill.name, + ide, + path: targetPath, + }); + } catch (error) { + result.errors.push({ + skill: skill.name, + ide, + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + + logger.debug( + { + installed: result.installed.length, + skipped: result.skipped.length, + errors: result.errors.length, + }, + 'Installation complete', + ); + + return result; +} + +/** + * Remove an installed skill. + * + * @param skillName - Name of the skill to remove + * @param ide - Target IDE + * @param options - Installation options + * @returns true if removed, false if not found + */ +export async function removeSkill( + skillName: string, + ide: IdeType, + options: {global: boolean; projectRoot?: string}, +): Promise { + const sanitizedName = sanitizeName(skillName); + const installPath = getSkillInstallPath(ide, sanitizedName, options); + + if (!fs.existsSync(installPath)) { + return false; + } + + await fs.promises.rm(installPath, {recursive: true}); + return true; +} diff --git a/packages/b2c-tooling-sdk/src/skills/parser.ts b/packages/b2c-tooling-sdk/src/skills/parser.ts new file mode 100644 index 00000000..9c5a244c --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/parser.ts @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type {SkillMetadata, SkillSet} from './types.js'; +import {getLogger} from '../logging/logger.js'; + +/** + * Parse simple YAML-like frontmatter from SKILL.md content. + * Only supports basic key: value pairs (name and description). + * + * @param content - File content with frontmatter + * @returns Parsed frontmatter or null if invalid + */ +export function parseSkillFrontmatter(content: string): {name: string; description: string} | null { + // Match frontmatter between --- delimiters + const match = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match) { + return null; + } + + const frontmatter = match[1]; + const result: {name?: string; description?: string} = {}; + + // Parse simple key: value lines + for (const line of frontmatter.split('\n')) { + const keyValueMatch = line.match(/^(\w+):\s*(.+)$/); + if (keyValueMatch) { + const [, key, value] = keyValueMatch; + if (key === 'name') { + result.name = value.trim(); + } else if (key === 'description') { + result.description = value.trim(); + } + } + } + + if (!result.name || !result.description) { + return null; + } + + return {name: result.name, description: result.description}; +} + +/** + * Scan a directory for skills and extract their metadata. + * + * @param skillsDir - Path to extracted skills directory (e.g., ~/.cache/b2c-cli/skills/v0.1.0/b2c/skills/) + * @param skillSet - The skill set being scanned ('b2c' or 'b2c-cli') + * @returns Array of skill metadata + */ +export async function scanSkills(skillsDir: string, skillSet: SkillSet): Promise { + const logger = getLogger(); + const skills: SkillMetadata[] = []; + + // The extracted structure should be: skillsDir/skills/skill-name/SKILL.md + // Find the skills subdirectory + const skillsSubdir = path.join(skillsDir, 'skills'); + + if (!fs.existsSync(skillsSubdir)) { + logger.debug({skillsDir, skillsSubdir}, 'Skills subdirectory not found'); + return skills; + } + + const entries = await fs.promises.readdir(skillsSubdir, {withFileTypes: true}); + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + const skillDir = path.join(skillsSubdir, entry.name); + const skillPath = path.join(skillDir, 'SKILL.md'); + + if (!fs.existsSync(skillPath)) { + logger.debug({skillDir}, 'No SKILL.md found, skipping'); + continue; + } + + try { + const content = await fs.promises.readFile(skillPath, 'utf-8'); + const frontmatter = parseSkillFrontmatter(content); + + if (!frontmatter) { + logger.warn({skillPath}, 'Invalid frontmatter in SKILL.md'); + continue; + } + + // Check for references directory + const referencesDir = path.join(skillDir, 'references'); + const hasReferences = fs.existsSync(referencesDir); + + skills.push({ + name: frontmatter.name, + description: frontmatter.description, + skillSet, + path: entry.name, // Relative path within skills directory + hasReferences, + }); + } catch (error) { + logger.warn({skillPath, error}, 'Failed to parse SKILL.md'); + } + } + + logger.debug({count: skills.length, skillSet}, 'Scanned skills'); + return skills; +} + +/** + * Filter skills by name. + * + * @param skills - All available skills + * @param names - Skill names to include (if provided) + * @returns Filtered skills + */ +export function filterSkillsByName(skills: SkillMetadata[], names?: string[]): SkillMetadata[] { + if (!names || names.length === 0) { + return skills; + } + + const nameSet = new Set(names); + return skills.filter((skill) => nameSet.has(skill.name)); +} + +/** + * Find skills that match the given names, returning any that weren't found. + * + * @param skills - Available skills + * @param names - Requested skill names + * @returns Object with matched skills and names not found + */ +export function findSkillsByName( + skills: SkillMetadata[], + names: string[], +): {found: SkillMetadata[]; notFound: string[]} { + const skillMap = new Map(skills.map((s) => [s.name, s])); + const found: SkillMetadata[] = []; + const notFound: string[] = []; + + for (const name of names) { + const skill = skillMap.get(name); + if (skill) { + found.push(skill); + } else { + notFound.push(name); + } + } + + return {found, notFound}; +} diff --git a/packages/b2c-tooling-sdk/src/skills/types.ts b/packages/b2c-tooling-sdk/src/skills/types.ts new file mode 100644 index 00000000..0daa88d4 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/skills/types.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Supported IDE types for skill installation. + */ +export type IdeType = 'claude-code' | 'cursor' | 'windsurf' | 'github-copilot' | 'codex' | 'opencode' | 'manual'; + +/** + * Skill set categories matching the plugins directory structure. + */ +export type SkillSet = 'b2c' | 'b2c-cli'; + +/** + * IDE path configuration for skill installation. + */ +export interface IdePaths { + /** Relative path for project-level installation (e.g., '.claude/skills') */ + projectDir: string; + /** Absolute path for global/user-level installation (e.g., '~/.claude/skills') */ + globalDir: string; +} + +/** + * IDE configuration including paths and display name. + */ +export interface IdeConfig { + /** IDE identifier */ + id: IdeType; + /** Human-readable display name */ + displayName: string; + /** Installation paths */ + paths: IdePaths; + /** Function to detect if IDE is installed */ + detectInstalled: () => Promise; +} + +/** + * Skill metadata extracted from SKILL.md frontmatter. + */ +export interface SkillMetadata { + /** Skill identifier from frontmatter name field */ + name: string; + /** Skill description from frontmatter */ + description: string; + /** Skill set this skill belongs to (b2c or b2c-cli) */ + skillSet: SkillSet; + /** Relative path within the skills archive */ + path: string; + /** Whether this skill has a references/ subdirectory */ + hasReferences: boolean; +} + +/** + * GitHub release information. + */ +export interface ReleaseInfo { + /** Git tag name (e.g., 'v0.1.0') */ + tagName: string; + /** Version number without 'v' prefix */ + version: string; + /** ISO date string when release was published */ + publishedAt: string; + /** Download URL for b2c-skills.zip asset, or null if not present */ + b2cSkillsAssetUrl: string | null; + /** Download URL for b2c-cli-skills.zip asset, or null if not present */ + b2cCliSkillsAssetUrl: string | null; +} + +/** + * Options for downloading skills artifacts. + */ +export interface DownloadSkillsOptions { + /** Specific release version to download (default: 'latest') */ + version?: string; + /** Custom cache directory (default: ~/.cache/b2c-cli/skills/) */ + cacheDir?: string; + /** Force re-download even if cached */ + forceDownload?: boolean; +} + +/** + * Options for installing skills. + */ +export interface InstallSkillsOptions { + /** Specific skill names to install (default: all skills in skillset) */ + skills?: string[]; + /** Target IDEs to install to */ + ides: IdeType[]; + /** Install to global/user directory instead of project */ + global: boolean; + /** Overwrite existing skills */ + update: boolean; + /** Project root for project-level installations */ + projectRoot?: string; +} + +/** + * Result of a single skill installation. + */ +export interface SkillInstallation { + skill: string; + ide: IdeType; + path: string; +} + +/** + * Reason for skipping a skill installation. + */ +export interface SkillSkipped { + skill: string; + ide: IdeType; + reason: string; +} + +/** + * Error during skill installation. + */ +export interface SkillError { + skill: string; + ide: IdeType; + error: string; +} + +/** + * Result of installing skills. + */ +export interface InstallSkillsResult { + /** Successfully installed skills */ + installed: SkillInstallation[]; + /** Skipped skills (already exist, no update flag) */ + skipped: SkillSkipped[]; + /** Failed installations */ + errors: SkillError[]; +} + +/** + * Cached artifact metadata stored in manifest.json. + */ +export interface CachedArtifact { + /** Version of the cached artifact */ + version: string; + /** Path to the extracted skills directory */ + path: string; + /** ISO date string when artifact was downloaded */ + downloadedAt: string; +} From 64574ee9ecc0fba1c63f9737d18f3e0070578c1b Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 20 Jan 2026 12:10:37 -0500 Subject: [PATCH 2/3] documentation --- docs/.vitepress/config.mts | 1 + docs/cli/setup.md | 162 +++++++++++++++++++++++++++++++++++++ docs/guide/agent-skills.md | 122 ++++++++++++++++++++++++++-- 3 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 docs/cli/setup.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 0f8f7c7c..36c06eff 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -38,6 +38,7 @@ const guideSidebar = [ { text: 'SLAS Commands', link: '/cli/slas' }, { text: 'Custom APIs', link: '/cli/custom-apis' }, { text: 'SCAPI Schemas', link: '/cli/scapi-schemas' }, + { text: 'Setup Commands', link: '/cli/setup' }, { text: 'Auth Commands', link: '/cli/auth' }, { text: 'Logging', link: '/cli/logging' }, ], diff --git a/docs/cli/setup.md b/docs/cli/setup.md new file mode 100644 index 00000000..b1df2344 --- /dev/null +++ b/docs/cli/setup.md @@ -0,0 +1,162 @@ +--- +description: Commands for installing AI agent skills for Claude Code, Cursor, Windsurf, and other agentic IDEs. +--- + +# Setup Commands + +Commands for setting up the development environment with AI agent skills. + +## b2c setup skills + +Install agent skills from the B2C Developer Tooling project to AI-powered IDEs. + +This command downloads skills from GitHub releases and installs them to the configuration directories of supported IDEs. Skills teach AI assistants about B2C Commerce development, CLI commands, and best practices. + +### Usage + +```bash +b2c setup skills [SKILLSET] +``` + +### Arguments + +| Argument | Description | Default | +|----------|-------------|---------| +| `SKILLSET` | Skill set to install: `b2c`, `b2c-cli`, or `all` | `all` | + +### Flags + +| Flag | Description | Default | +|------|-------------|---------| +| `--list`, `-l` | List available skills without installing | `false` | +| `--skill` | Install specific skill(s) (can be repeated) | | +| `--ide` | Target IDE(s) (can be repeated) | Auto-detect | +| `--global`, `-g` | Install to user home directory (global scope) | `false` | +| `--update`, `-u` | Update existing skills (overwrite) | `false` | +| `--version` | Specific release version | `latest` | +| `--force` | Skip confirmation prompts (non-interactive) | `false` | +| `--json` | Output results as JSON | `false` | + +### Supported IDEs + +| IDE Value | IDE Name | Project Path | Global Path | +|-----------|----------|--------------|-------------| +| `claude-code` | Claude Code | `.claude/skills/` | `~/.claude/skills/` | +| `cursor` | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | +| `windsurf` | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | +| `github-copilot` | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | +| `codex` | Codex CLI | `.codex/skills/` | `~/.codex/skills/` | +| `opencode` | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | +| `manual` | Manual | `.claude/skills/` | `~/.claude/skills/` | + +Use `manual` when you want to install to the Claude Code paths without marketplace recommendations. + +### Examples + +```bash +# List all available skills +b2c setup skills --list + +# Interactive installation (auto-detects IDEs) +b2c setup skills + +# Install to Cursor (project scope) +b2c setup skills --ide cursor + +# Install to Cursor (global/user scope) +b2c setup skills --ide cursor --global + +# Install to multiple IDEs +b2c setup skills --ide cursor --ide windsurf + +# Install only b2c-cli skills +b2c setup skills b2c-cli --ide cursor + +# Install specific skills only +b2c setup skills --skill b2c-code --skill b2c-webdav --ide cursor + +# Update existing skills +b2c setup skills --ide cursor --update + +# Non-interactive mode (for CI/CD) +b2c setup skills --ide cursor --global --force + +# Install a specific version +b2c setup skills --version v0.1.0 --ide cursor + +# Output as JSON +b2c setup skills --list --json +``` + +### Interactive Mode + +When run without `--force`, the command provides an interactive experience: + +1. Downloads skills from the latest release (or specified version) +2. Auto-detects installed IDEs +3. Prompts you to select target IDEs +4. Shows installation preview +5. Confirms before installing +6. Reports results + +### Claude Code Recommendation + +For Claude Code users, we recommend using the plugin marketplace for automatic updates: + +```bash +claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling +claude plugin install b2c-cli +claude plugin install b2c +``` + +The marketplace provides: +- Automatic updates when new versions are released +- Centralized plugin management +- Version tracking + +Use `--ide manual` if you prefer manual installation to the same paths. + +### Skill Sets + +| Skill Set | Description | +|-----------|-------------| +| `b2c` | B2C Commerce development patterns and practices | +| `b2c-cli` | B2C CLI commands and operations | +| `all` | Both skill sets (default) | + +### Output + +When installing, the command reports: +- Successfully installed skills with paths +- Skipped skills (already exist, use `--update` to overwrite) +- Errors encountered during installation + +Example output: + +``` +Downloading skills from release latest... +Detecting installed IDEs... +Installing 24 skills to Cursor (project) + +Successfully installed 24 skill(s): + - b2c-code → .cursor/skills/b2c-code/ + - b2c-webdav → .cursor/skills/b2c-webdav/ + ... +``` + +### Environment + +Skills are downloaded from the GitHub releases of the [b2c-developer-tooling](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling) repository: + +| Artifact | Contents | +|----------|----------| +| `b2c-cli-skills.zip` | Skills for B2C CLI commands and operations | +| `b2c-skills.zip` | Skills for B2C Commerce development patterns | + +Downloaded artifacts are cached locally at: `~/.cache/b2c-cli/skills/{version}/{skillset}/` + +### See Also + +- [Agent Skills & Plugins Guide](/guide/agent-skills) - Overview of available skills +- [Claude Code Skills Documentation](https://claude.ai/code) - Claude Code skill format +- [Cursor Skills Documentation](https://cursor.com/docs/context/skills) - Cursor skill format diff --git a/docs/guide/agent-skills.md b/docs/guide/agent-skills.md index e9f8c318..0a81de31 100644 --- a/docs/guide/agent-skills.md +++ b/docs/guide/agent-skills.md @@ -103,28 +103,140 @@ To remove the marketplace: claude plugin marketplace remove b2c-developer-tooling ``` +## Installation with B2C CLI + +The B2C CLI provides a `setup skills` command that downloads and installs agent skills to any supported IDE. + +### List Available Skills + +```bash +b2c setup skills --list +``` + +### Install to Specific IDEs + +::: code-group + +```bash [Project Scope] +# Install to Cursor (current project only) +b2c setup skills --ide cursor + +# Install to Windsurf +b2c setup skills --ide windsurf + +# Install to multiple IDEs +b2c setup skills --ide cursor --ide windsurf +``` + +```bash [User Scope] +# Install globally (available in all projects) +b2c setup skills --ide cursor --global + +# Install to GitHub Copilot globally +b2c setup skills --ide github-copilot --global +``` + +::: + +### Install Specific Skills + +```bash +# Install only certain skills +b2c setup skills --skill b2c-code --skill b2c-webdav --ide cursor + +# Install only b2c-cli skills (not b2c development skills) +b2c setup skills b2c-cli --ide cursor +``` + +### Update Existing Skills + +```bash +# Overwrite existing skills with latest versions +b2c setup skills --ide cursor --update +``` + +### Non-Interactive Mode + +For CI/CD pipelines or scripted installations: + +```bash +b2c setup skills --ide cursor --global --force +``` + +See [Setup Commands](/cli/setup) for full CLI documentation. + ## Installation with Other IDEs The B2C skills follow the [Agent Skills](https://agentskills.io/home) standard and can be used with other AI-powered development tools. +::: tip Recommended +Use the [`b2c setup skills`](/cli/setup) command for easier installation to any supported IDE. +::: + ### Cursor See the [Cursor Skills documentation](https://cursor.com/docs/context/skills) for configuration instructions. -Copy skill files from the plugin directories to your Cursor skills location: +Skills are installed to: +- **Project scope**: `.cursor/skills/` in your project +- **User scope**: `~/.cursor/skills/` -- [b2c-cli skills](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/tree/main/plugins/b2c-cli/skills) -- [b2c skills](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/tree/main/plugins/b2c/skills) +### Windsurf + +See the [Windsurf documentation](https://docs.windsurf.com/) for configuration instructions. + +Skills are installed to: +- **Project scope**: `.windsurf/skills/` in your project +- **User scope**: `~/.codeium/windsurf/skills/` ### VS Code with GitHub Copilot See the [VS Code Agent Skills documentation](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for configuration instructions. +Skills are installed to: +- **Project scope**: `.github/skills/` in your project +- **User scope**: `~/.copilot/skills/` + You can also append skill content to `.github/copilot-instructions.md` in your repository. -### Other IDEs +### Codex CLI + +See the [Codex documentation](https://github.com/openai/codex) for configuration instructions. + +Skills are installed to: +- **Project scope**: `.codex/skills/` in your project +- **User scope**: `~/.codex/skills/` + +### OpenCode + +See the [OpenCode documentation](https://opencode.ai/) for configuration instructions. + +Skills are installed to: +- **Project scope**: `.opencode/skills/` in your project +- **User scope**: `~/.config/opencode/skills/` + +### Manual Installation + +For other AI-powered IDEs, download the skills zip files from the [latest GitHub release](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/releases/latest): + +| Artifact | Contents | +|----------|----------| +| `b2c-cli-skills.zip` | Skills for B2C CLI commands and operations | +| `b2c-skills.zip` | Skills for B2C Commerce development patterns | + +Each zip contains a `skills/` folder with individual skill directories. Extract and copy to your IDE's custom instructions location: + +```bash +# Download from latest release +curl -LO https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/releases/latest/download/b2c-cli-skills.zip +curl -LO https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/releases/latest/download/b2c-skills.zip + +# Extract and copy to your IDE's skills directory +unzip b2c-cli-skills.zip -d /path/to/your/ide/skills/ +unzip b2c-skills.zip -d /path/to/your/ide/skills/ +``` -For other AI-powered IDEs, copy the `SKILL.md` files and any `references/` directories to your IDE's custom instructions location. +Each skill is a directory containing a `SKILL.md` file and optionally a `references/` folder with additional documentation. ## Usage Examples From bbcee4857038a5b006fc433cc7017047f0d91d0d Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 20 Jan 2026 12:50:24 -0500 Subject: [PATCH 3/3] improve setup skills command UX and fix rate limiting - Add @inquirer/prompts for interactive multi-select prompts - Change skillset selection to multi-select (can install both b2c and b2c-cli) - Rename github-copilot IDE to vscode for clarity - Add docsUrl to IDE configs and show documentation links after installation - Use direct GitHub download URLs instead of API to avoid rate limiting - Update documentation with new IDE names and flag options --- docs/cli/setup.md | 62 ++-- docs/guide/agent-skills.md | 40 +-- packages/b2c-cli/package.json | 1 + packages/b2c-cli/src/commands/setup/skills.ts | 212 +++++++------- packages/b2c-cli/src/i18n/locales/en.ts | 7 + packages/b2c-tooling-sdk/src/skills/agents.ts | 35 ++- packages/b2c-tooling-sdk/src/skills/github.ts | 79 ++++-- packages/b2c-tooling-sdk/src/skills/index.ts | 9 +- packages/b2c-tooling-sdk/src/skills/types.ts | 4 +- pnpm-lock.yaml | 264 ++++++++++++++++++ 10 files changed, 526 insertions(+), 187 deletions(-) diff --git a/docs/cli/setup.md b/docs/cli/setup.md index b1df2344..ae3483f5 100644 --- a/docs/cli/setup.md +++ b/docs/cli/setup.md @@ -22,7 +22,7 @@ b2c setup skills [SKILLSET] | Argument | Description | Default | |----------|-------------|---------| -| `SKILLSET` | Skill set to install: `b2c`, `b2c-cli`, or `all` | `all` | +| `SKILLSET` | Skill set to install: `b2c` or `b2c-cli` | Prompted interactively | ### Flags @@ -30,7 +30,7 @@ b2c setup skills [SKILLSET] |------|-------------|---------| | `--list`, `-l` | List available skills without installing | `false` | | `--skill` | Install specific skill(s) (can be repeated) | | -| `--ide` | Target IDE(s) (can be repeated) | Auto-detect | +| `--ide` | Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, manual | Auto-detect | | `--global`, `-g` | Install to user home directory (global scope) | `false` | | `--update`, `-u` | Update existing skills (overwrite) | `false` | | `--version` | Specific release version | `latest` | @@ -44,8 +44,8 @@ b2c setup skills [SKILLSET] | `claude-code` | Claude Code | `.claude/skills/` | `~/.claude/skills/` | | `cursor` | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | | `windsurf` | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | -| `github-copilot` | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | -| `codex` | Codex CLI | `.codex/skills/` | `~/.codex/skills/` | +| `vscode` | VS Code / GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | +| `codex` | OpenAI Codex CLI | `.codex/skills/` | `~/.codex/skills/` | | `opencode` | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | | `manual` | Manual | `.claude/skills/` | `~/.claude/skills/` | @@ -54,50 +54,51 @@ Use `manual` when you want to install to the Claude Code paths without marketpla ### Examples ```bash -# List all available skills -b2c setup skills --list - -# Interactive installation (auto-detects IDEs) +# Interactive mode (prompts for skillset and IDEs) b2c setup skills -# Install to Cursor (project scope) -b2c setup skills --ide cursor +# List available skills in a skillset +b2c setup skills b2c --list +b2c setup skills b2c-cli --list -# Install to Cursor (global/user scope) -b2c setup skills --ide cursor --global +# Install b2c skills to Cursor (project scope) +b2c setup skills b2c --ide cursor -# Install to multiple IDEs -b2c setup skills --ide cursor --ide windsurf +# Install b2c-cli skills to Cursor (global/user scope) +b2c setup skills b2c-cli --ide cursor --global -# Install only b2c-cli skills -b2c setup skills b2c-cli --ide cursor +# Install to multiple IDEs +b2c setup skills b2c --ide cursor --ide windsurf # Install specific skills only -b2c setup skills --skill b2c-code --skill b2c-webdav --ide cursor +b2c setup skills b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor # Update existing skills -b2c setup skills --ide cursor --update +b2c setup skills b2c --ide cursor --update -# Non-interactive mode (for CI/CD) -b2c setup skills --ide cursor --global --force +# Non-interactive mode (for CI/CD) - skillset required +b2c setup skills b2c-cli --ide cursor --global --force # Install a specific version -b2c setup skills --version v0.1.0 --ide cursor +b2c setup skills b2c --version v0.1.0 --ide cursor # Output as JSON -b2c setup skills --list --json +b2c setup skills b2c --list --json ``` ### Interactive Mode When run without `--force`, the command provides an interactive experience: -1. Downloads skills from the latest release (or specified version) -2. Auto-detects installed IDEs -3. Prompts you to select target IDEs -4. Shows installation preview -5. Confirms before installing -6. Reports results +1. Prompts you to select skill set(s) (if not provided as argument) - you can select both `b2c` and `b2c-cli` +2. Downloads skills from the latest release (or specified version) +3. Auto-detects installed IDEs +4. Prompts you to select target IDEs +5. Shows installation preview +6. Confirms before installing +7. Reports results + +In non-interactive mode (`--force`), the skillset argument is required. ### Claude Code Recommendation @@ -122,7 +123,6 @@ Use `--ide manual` if you prefer manual installation to the same paths. |-----------|-------------| | `b2c` | B2C Commerce development patterns and practices | | `b2c-cli` | B2C CLI commands and operations | -| `all` | Both skill sets (default) | ### Output @@ -136,9 +136,9 @@ Example output: ``` Downloading skills from release latest... Detecting installed IDEs... -Installing 24 skills to Cursor (project) +Installing 12 skills to Cursor (project) -Successfully installed 24 skill(s): +Successfully installed 12 skill(s): - b2c-code → .cursor/skills/b2c-code/ - b2c-webdav → .cursor/skills/b2c-webdav/ ... diff --git a/docs/guide/agent-skills.md b/docs/guide/agent-skills.md index 0a81de31..e3d3e4fd 100644 --- a/docs/guide/agent-skills.md +++ b/docs/guide/agent-skills.md @@ -107,10 +107,21 @@ claude plugin marketplace remove b2c-developer-tooling The B2C CLI provides a `setup skills` command that downloads and installs agent skills to any supported IDE. +### Interactive Mode + +Run without arguments to interactively select skill sets and IDEs: + +```bash +b2c setup skills +``` + +This prompts you to select which skill sets (`b2c`, `b2c-cli`, or both) and which IDEs to install to. + ### List Available Skills ```bash -b2c setup skills --list +b2c setup skills b2c --list +b2c setup skills b2c-cli --list ``` ### Install to Specific IDEs @@ -118,22 +129,22 @@ b2c setup skills --list ::: code-group ```bash [Project Scope] -# Install to Cursor (current project only) -b2c setup skills --ide cursor +# Install b2c skills to Cursor (current project only) +b2c setup skills b2c --ide cursor -# Install to Windsurf -b2c setup skills --ide windsurf +# Install b2c-cli skills to Windsurf +b2c setup skills b2c-cli --ide windsurf # Install to multiple IDEs -b2c setup skills --ide cursor --ide windsurf +b2c setup skills b2c --ide cursor --ide windsurf ``` ```bash [User Scope] # Install globally (available in all projects) -b2c setup skills --ide cursor --global +b2c setup skills b2c --ide cursor --global # Install to GitHub Copilot globally -b2c setup skills --ide github-copilot --global +b2c setup skills b2c-cli --ide vscode --global ``` ::: @@ -141,26 +152,23 @@ b2c setup skills --ide github-copilot --global ### Install Specific Skills ```bash -# Install only certain skills -b2c setup skills --skill b2c-code --skill b2c-webdav --ide cursor - -# Install only b2c-cli skills (not b2c development skills) -b2c setup skills b2c-cli --ide cursor +# Install only certain skills from a skillset +b2c setup skills b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor ``` ### Update Existing Skills ```bash # Overwrite existing skills with latest versions -b2c setup skills --ide cursor --update +b2c setup skills b2c --ide cursor --update ``` ### Non-Interactive Mode -For CI/CD pipelines or scripted installations: +For CI/CD pipelines or scripted installations, the skillset argument is required: ```bash -b2c setup skills --ide cursor --global --force +b2c setup skills b2c-cli --ide cursor --global --force ``` See [Setup Commands](/cli/setup) for full CLI documentation. diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index 5fd61e2f..b17257a1 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -12,6 +12,7 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { + "@inquirer/prompts": "^8.2.0", "@oclif/core": "^4", "@oclif/plugin-autocomplete": "^3", "@oclif/plugin-help": "^6", diff --git a/packages/b2c-cli/src/commands/setup/skills.ts b/packages/b2c-cli/src/commands/setup/skills.ts index 385b59d7..a3cc9a21 100644 --- a/packages/b2c-cli/src/commands/setup/skills.ts +++ b/packages/b2c-cli/src/commands/setup/skills.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ -import * as readline from 'node:readline'; import {Args, Flags, ux} from '@oclif/core'; +import {checkbox, confirm} from '@inquirer/prompts'; import {BaseCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; import { type IdeType, @@ -17,58 +17,11 @@ import { scanSkills, installSkills, getIdeDisplayName, + getIdeDocsUrl, findSkillsByName, } from '@salesforce/b2c-tooling-sdk/skills'; import {t} from '../../i18n/index.js'; -/** - * Simple confirmation prompt. - */ -async function confirm(message: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stderr, - }); - - return new Promise((resolve) => { - rl.question(`${message} `, (answer) => { - rl.close(); - resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); - }); - }); -} - -/** - * Simple selection prompt (returns selected indices). - */ -async function selectMultiple(message: string, options: string[]): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stderr, - }); - - // Display options - process.stderr.write(`${message}\n`); - for (const [i, opt] of options.entries()) { - process.stderr.write(` ${i + 1}. ${opt}\n`); - } - - return new Promise((resolve) => { - rl.question('Enter numbers separated by commas (or "all" for all): ', (answer) => { - rl.close(); - if (answer.toLowerCase() === 'all') { - resolve(options.map((_, i) => i)); - return; - } - const indices = answer - .split(',') - .map((s) => Number.parseInt(s.trim(), 10) - 1) - .filter((n) => !Number.isNaN(n) && n >= 0 && n < options.length); - resolve(indices); - }); - }); -} - /** * Table columns for skill listing. */ @@ -106,9 +59,8 @@ interface SetupSkillsResponse { export default class SetupSkills extends BaseCommand { static args = { skillset: Args.string({ - description: 'Skill set to install: b2c, b2c-cli, or all', - options: ['b2c', 'b2c-cli', 'all'], - default: 'all', + description: 'Skill set to install: b2c or b2c-cli', + options: ['b2c', 'b2c-cli'], }), }; @@ -117,12 +69,11 @@ export default class SetupSkills extends BaseCommand { static enableJsonFlag = true; static examples = [ - '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> b2c', '<%= config.bin %> <%= command.id %> b2c-cli --ide cursor --global', - '<%= config.bin %> <%= command.id %> --list', - '<%= config.bin %> <%= command.id %> --skill b2c-code --skill b2c-webdav --ide cursor', - '<%= config.bin %> <%= command.id %> --global --update --force', - '<%= config.bin %> <%= command.id %> --version v0.1.0', + '<%= config.bin %> <%= command.id %> b2c --list', + '<%= config.bin %> <%= command.id %> b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor', + '<%= config.bin %> <%= command.id %> b2c --global --update --force', ]; static flags = { @@ -137,7 +88,7 @@ export default class SetupSkills extends BaseCommand { multiple: true, }), ide: Flags.string({ - description: 'Target IDE(s): claude-code, cursor, windsurf, github-copilot, codex, opencode, manual', + description: 'Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, manual', options: ALL_IDE_TYPES, multiple: true, }), @@ -161,7 +112,30 @@ export default class SetupSkills extends BaseCommand { }; async run(): Promise { - const skillsets: SkillSet[] = this.args.skillset === 'all' ? ['b2c', 'b2c-cli'] : [this.args.skillset as SkillSet]; + // Determine skillsets - prompt if not provided + let skillsets: SkillSet[]; + if (this.args.skillset) { + skillsets = [this.args.skillset as SkillSet]; + } else if (this.flags.force) { + this.error( + t( + 'commands.setup.skills.skillsetRequired', + 'Skillset argument required in non-interactive mode. Specify b2c or b2c-cli.', + ), + ); + } else { + skillsets = await checkbox({ + message: t('commands.setup.skills.selectSkillset', 'Select skill set(s) to install:'), + choices: [ + {name: 'b2c - B2C Commerce development patterns', value: 'b2c' as SkillSet}, + {name: 'b2c-cli - B2C CLI commands and operations', value: 'b2c-cli' as SkillSet}, + ], + }); + if (skillsets.length === 0) { + ux.stdout(t('commands.setup.skills.noSkillsetsSelected', 'No skill sets selected.')); + return {}; + } + } // Download and scan skills this.log( @@ -219,7 +193,7 @@ export default class SetupSkills extends BaseCommand { } if (skillsToInstall.length === 0) { - this.log(t('commands.setup.skills.noSkillsToInstall', 'No skills to install.')); + ux.stdout(t('commands.setup.skills.noSkillsToInstall', 'No skills to install.')); return {}; } @@ -232,7 +206,7 @@ export default class SetupSkills extends BaseCommand { const detectedIdes = await detectInstalledIdes(); if (detectedIdes.length === 0) { - this.log( + ux.stdout( t( 'commands.setup.skills.noIdesDetected', 'No IDEs detected. Use --ide to specify target (e.g., --ide cursor --ide manual).', @@ -241,26 +215,27 @@ export default class SetupSkills extends BaseCommand { return {}; } - if (this.flags.force) { - // Non-interactive: use all detected IDEs - targetIdes = detectedIdes; - } else { - // Interactive: let user select - const ideNames = detectedIdes.map((ide) => getIdeDisplayName(ide)); - const selected = await selectMultiple('Select target IDEs:', ideNames); - targetIdes = selected.map((i) => detectedIdes[i]); - } + // Non-interactive: use all detected IDEs; Interactive: let user select + targetIdes = this.flags.force + ? detectedIdes + : await checkbox({ + message: t('commands.setup.skills.selectIdes', 'Select target IDEs:'), + choices: detectedIdes.map((ide) => ({ + name: getIdeDisplayName(ide), + value: ide, + })), + }); } if (targetIdes.length === 0) { - this.log(t('commands.setup.skills.noIdesSelected', 'No IDEs selected.')); + ux.stdout(t('commands.setup.skills.noIdesSelected', 'No IDEs selected.')); return {}; } // Claude Code marketplace recommendation if (targetIdes.includes('claude-code')) { - this.log(''); - this.log( + ux.stdout(''); + ux.stdout( t( 'commands.setup.skills.claudeCodeRecommendation', 'Note: For Claude Code, we recommend using the plugin marketplace for automatic updates:\n' + @@ -272,11 +247,14 @@ export default class SetupSkills extends BaseCommand { ); if (!this.flags.force) { - const proceed = await confirm('Continue with Claude Code installation? (y/n)'); + const proceed = await confirm({ + message: t('commands.setup.skills.confirmClaudeCode', 'Continue with Claude Code installation?'), + default: true, + }); if (!proceed) { targetIdes = targetIdes.filter((ide) => ide !== 'claude-code'); if (targetIdes.length === 0) { - this.log(t('commands.setup.skills.cancelled', 'Installation cancelled.')); + ux.stdout(t('commands.setup.skills.cancelled', 'Installation cancelled.')); return {}; } } @@ -285,8 +263,8 @@ export default class SetupSkills extends BaseCommand { // Show installation preview const scope = this.flags.global ? 'global (user home)' : 'project'; - this.log(''); - this.log( + ux.stdout(''); + ux.stdout( t('commands.setup.skills.preview', 'Installing {{count}} skills to {{ides}} ({{scope}})', { count: skillsToInstall.length, ides: targetIdes.map((ide) => getIdeDisplayName(ide)).join(', '), @@ -296,14 +274,16 @@ export default class SetupSkills extends BaseCommand { // Confirm installation if (!this.flags.force) { - const proceed = await confirm('Proceed with installation? (y/n)'); + const proceed = await confirm({ + message: t('commands.setup.skills.confirmInstall', 'Proceed with installation?'), + default: true, + }); if (!proceed) { - this.log(t('commands.setup.skills.cancelled', 'Installation cancelled.')); + ux.stdout(t('commands.setup.skills.cancelled', 'Installation cancelled.')); return {}; } } - // Install skills for each skillset // Install skills for all skillsets in parallel const installPromises = skillsets .map((skillset) => { @@ -320,59 +300,81 @@ export default class SetupSkills extends BaseCommand { const installResults = await Promise.all(installPromises); - const combinedResult: InstallSkillsResult = { + const result: InstallSkillsResult = { installed: [], skipped: [], errors: [], }; - for (const result of installResults) { - combinedResult.installed.push(...result.installed); - combinedResult.skipped.push(...result.skipped); - combinedResult.errors.push(...result.errors); + for (const r of installResults) { + result.installed.push(...r.installed); + result.skipped.push(...r.skipped); + result.errors.push(...r.errors); } // Report results - if (combinedResult.installed.length > 0) { - this.log(''); - this.log( + if (result.installed.length > 0) { + ux.stdout(''); + ux.stdout( t('commands.setup.skills.installed', 'Successfully installed {{count}} skill(s):', { - count: combinedResult.installed.length, + count: result.installed.length, }), ); - for (const item of combinedResult.installed) { - this.log(` - ${item.skill} → ${item.path}`); + for (const item of result.installed) { + ux.stdout(` - ${item.skill} → ${item.path}`); } } - if (combinedResult.skipped.length > 0) { - this.log(''); - this.log( + if (result.skipped.length > 0) { + ux.stdout(''); + ux.stdout( t('commands.setup.skills.skippedCount', 'Skipped {{count}} skill(s):', { - count: combinedResult.skipped.length, + count: result.skipped.length, }), ); - for (const item of combinedResult.skipped) { - this.log(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.reason}`); + for (const item of result.skipped) { + ux.stdout(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.reason}`); } } - if (combinedResult.errors.length > 0) { - this.log(''); + if (result.errors.length > 0) { + ux.stdout(''); this.warn( t('commands.setup.skills.errorsCount', 'Failed to install {{count}} skill(s):', { - count: combinedResult.errors.length, + count: result.errors.length, }), ); - for (const item of combinedResult.errors) { - this.log(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.error}`); + for (const item of result.errors) { + ux.stdout(` - ${item.skill} (${getIdeDisplayName(item.ide)}): ${item.error}`); + } + } + + // Show IDE-specific documentation notes + if (result.installed.length > 0) { + const installedIdes = [...new Set(result.installed.map((item) => item.ide))]; + const ideNotes: Array<{displayName: string; docsUrl: string}> = []; + + for (const ide of installedIdes) { + if (ide === 'manual') continue; + const docsUrl = getIdeDocsUrl(ide); + if (docsUrl) { + ideNotes.push({displayName: getIdeDisplayName(ide), docsUrl}); + } + } + + if (ideNotes.length > 0) { + ux.stdout(''); + ux.stdout(t('commands.setup.skills.ideNotes', 'See IDE documentation for skill configuration:')); + for (const note of ideNotes) { + ux.stdout(` - ${note.displayName}: ${note.docsUrl}`); + } } } return { - installed: combinedResult.installed, - skipped: combinedResult.skipped, - errors: combinedResult.errors, + installed: result.installed, + skipped: result.skipped, + errors: result.errors, }; } } diff --git a/packages/b2c-cli/src/i18n/locales/en.ts b/packages/b2c-cli/src/i18n/locales/en.ts index 2fa216ec..5ef46af1 100644 --- a/packages/b2c-cli/src/i18n/locales/en.ts +++ b/packages/b2c-cli/src/i18n/locales/en.ts @@ -140,6 +140,13 @@ export const en = { installed: 'Successfully installed {{count}} skill(s):', skippedCount: 'Skipped {{count}} skill(s):', errorsCount: 'Failed to install {{count}} skill(s):', + skillsetRequired: 'Skillset argument required in non-interactive mode. Specify b2c or b2c-cli.', + selectSkillset: 'Select skill set(s) to install:', + noSkillsetsSelected: 'No skill sets selected.', + selectIdes: 'Select target IDEs:', + confirmClaudeCode: 'Continue with Claude Code installation?', + confirmInstall: 'Proceed with installation?', + ideNotes: 'See IDE documentation for skill configuration:', }, }, }, diff --git a/packages/b2c-tooling-sdk/src/skills/agents.ts b/packages/b2c-tooling-sdk/src/skills/agents.ts index d8de4049..2719eca6 100644 --- a/packages/b2c-tooling-sdk/src/skills/agents.ts +++ b/packages/b2c-tooling-sdk/src/skills/agents.ts @@ -13,7 +13,6 @@ const home = os.homedir(); /** * IDE configurations with paths and detection logic. - * Based on patterns from the add-skill reference implementation. */ export const IDE_CONFIGS: Record = { 'claude-code': { @@ -26,6 +25,7 @@ export const IDE_CONFIGS: Record = { detectInstalled: async () => { return fs.existsSync(path.join(home, '.claude')); }, + docsUrl: 'https://docs.anthropic.com/en/docs/claude-code', }, cursor: { id: 'cursor', @@ -37,6 +37,7 @@ export const IDE_CONFIGS: Record = { detectInstalled: async () => { return fs.existsSync(path.join(home, '.cursor')); }, + docsUrl: 'https://cursor.com/docs/context/skills', }, windsurf: { id: 'windsurf', @@ -48,10 +49,11 @@ export const IDE_CONFIGS: Record = { detectInstalled: async () => { return fs.existsSync(path.join(home, '.codeium/windsurf')); }, + docsUrl: 'https://docs.windsurf.com/', }, - 'github-copilot': { - id: 'github-copilot', - displayName: 'GitHub Copilot', + vscode: { + id: 'vscode', + displayName: 'VS Code / GitHub Copilot', paths: { projectDir: '.github/skills', globalDir: path.join(home, '.copilot/skills'), @@ -60,10 +62,11 @@ export const IDE_CONFIGS: Record = { // Check for either .github directory (project-based) or ~/.copilot return fs.existsSync(path.join(home, '.copilot')); }, + docsUrl: 'https://code.visualstudio.com/docs/copilot/customization/agent-skills', }, codex: { id: 'codex', - displayName: 'OpenAI Codex', + displayName: 'OpenAI Codex CLI', paths: { projectDir: '.codex/skills', globalDir: path.join(home, '.codex/skills'), @@ -71,6 +74,7 @@ export const IDE_CONFIGS: Record = { detectInstalled: async () => { return fs.existsSync(path.join(home, '.codex')); }, + docsUrl: 'https://github.com/openai/codex', }, opencode: { id: 'opencode', @@ -82,6 +86,7 @@ export const IDE_CONFIGS: Record = { detectInstalled: async () => { return fs.existsSync(path.join(home, '.config/opencode')); }, + docsUrl: 'https://opencode.ai/', }, manual: { id: 'manual', @@ -101,15 +106,7 @@ export const IDE_CONFIGS: Record = { /** * All supported IDE types in display order. */ -export const ALL_IDE_TYPES: IdeType[] = [ - 'claude-code', - 'cursor', - 'windsurf', - 'github-copilot', - 'codex', - 'opencode', - 'manual', -]; +export const ALL_IDE_TYPES: IdeType[] = ['claude-code', 'cursor', 'windsurf', 'vscode', 'codex', 'opencode', 'manual']; /** * Detect which IDEs are installed on the system. @@ -167,3 +164,13 @@ export function getSkillInstallPath( export function getIdeDisplayName(ide: IdeType): string { return IDE_CONFIGS[ide].displayName; } + +/** + * Get the documentation URL for an IDE. + * + * @param ide - IDE type + * @returns Documentation URL or undefined if not available + */ +export function getIdeDocsUrl(ide: IdeType): string | undefined { + return IDE_CONFIGS[ide].docsUrl; +} diff --git a/packages/b2c-tooling-sdk/src/skills/github.ts b/packages/b2c-tooling-sdk/src/skills/github.ts index f7c172b6..8c53b913 100644 --- a/packages/b2c-tooling-sdk/src/skills/github.ts +++ b/packages/b2c-tooling-sdk/src/skills/github.ts @@ -13,6 +13,7 @@ import {getLogger} from '../logging/logger.js'; const GITHUB_REPO = 'SalesforceCommerceCloud/b2c-developer-tooling'; const GITHUB_API_BASE = 'https://api.github.com'; +const GITHUB_DOWNLOAD_BASE = 'https://github.com'; /** * Asset filename patterns for skill archives. @@ -22,6 +23,22 @@ const ASSET_NAMES: Record = { 'b2c-cli': 'b2c-cli-skills.zip', }; +/** + * Build direct download URL for a release asset. + * These URLs don't require API calls and avoid rate limiting. + * + * @param version - 'latest' or specific version tag + * @param assetName - Name of the asset file + * @returns Direct download URL + */ +function getDirectDownloadUrl(version: string, assetName: string): string { + if (version === 'latest') { + return `${GITHUB_DOWNLOAD_BASE}/${GITHUB_REPO}/releases/latest/download/${assetName}`; + } + const tag = version.startsWith('v') ? version : `v${version}`; + return `${GITHUB_DOWNLOAD_BASE}/${GITHUB_REPO}/releases/download/${tag}/${assetName}`; +} + /** * Get the cache directory for skills. * Uses XDG_CACHE_HOME on Linux, ~/.cache otherwise. @@ -149,8 +166,21 @@ export function getCachedArtifact(version: string, skillSet: SkillSet): CachedAr } } +/** + * Extract version tag from a GitHub release download URL. + * URLs follow pattern: .../releases/download/{tag}/... + * + * @param url - The final URL after redirects + * @returns Version tag or null if not found + */ +function extractVersionFromUrl(url: string): string | null { + const match = url.match(/\/releases\/download\/([^/]+)\//); + return match ? match[1] : null; +} + /** * Download and extract skills artifact. + * Uses direct download URLs to avoid GitHub API rate limits. * * @param skillSet - Which skill set to download ('b2c' or 'b2c-cli') * @param options - Download options @@ -160,42 +190,53 @@ export function getCachedArtifact(version: string, skillSet: SkillSet): CachedAr export async function downloadSkillsArtifact(skillSet: SkillSet, options: DownloadSkillsOptions = {}): Promise { const logger = getLogger(); const {version = 'latest', forceDownload = false} = options; + const assetName = ASSET_NAMES[skillSet]; - // Get release info to determine version and asset URL - const release = await getRelease(version); - const actualVersion = release.tagName; - - // Check cache first (unless forced) - if (!forceDownload) { - const cached = getCachedArtifact(actualVersion, skillSet); + // For specific versions, check cache first (before any network calls) + if (version !== 'latest' && !forceDownload) { + const versionTag = version.startsWith('v') ? version : `v${version}`; + const cached = getCachedArtifact(versionTag, skillSet); if (cached && fs.existsSync(cached.path)) { - logger.debug({version: actualVersion, skillSet, path: cached.path}, 'Using cached skills'); + logger.debug({version: versionTag, skillSet, path: cached.path}, 'Using cached skills'); return cached.path; } } - // Determine asset URL - const assetUrl = skillSet === 'b2c' ? release.b2cSkillsAssetUrl : release.b2cCliSkillsAssetUrl; + // Build direct download URL (avoids API rate limits) + const downloadUrl = getDirectDownloadUrl(version, assetName); + logger.debug({url: downloadUrl, skillSet}, 'Downloading skills artifact'); - if (!assetUrl) { - throw new Error(`Skills artifact '${ASSET_NAMES[skillSet]}' not found in release ${actualVersion}`); - } - - logger.debug({url: assetUrl, skillSet}, 'Downloading skills artifact'); - - // Download artifact - const response = await fetch(assetUrl, { + // Download artifact - GitHub will redirect to the actual file + const response = await fetch(downloadUrl, { headers: { 'User-Agent': 'b2c-cli', }, + redirect: 'follow', }); if (!response.ok) { + if (response.status === 404) { + throw new Error( + `Skills artifact '${assetName}' not found for ${version === 'latest' ? 'latest release' : `version ${version}`}`, + ); + } throw new Error(`Failed to download skills: ${response.status} ${response.statusText}`); } + // Extract actual version from the final URL (after redirects) + const actualVersion = extractVersionFromUrl(response.url) || version; + + // Check cache for the resolved version (for 'latest' which we now know) + if (version === 'latest' && !forceDownload) { + const cached = getCachedArtifact(actualVersion, skillSet); + if (cached && fs.existsSync(cached.path)) { + logger.debug({version: actualVersion, skillSet, path: cached.path}, 'Using cached skills (resolved latest)'); + return cached.path; + } + } + const zipBuffer = Buffer.from(await response.arrayBuffer()); - logger.debug({size: zipBuffer.length}, 'Downloaded skills archive'); + logger.debug({size: zipBuffer.length, version: actualVersion}, 'Downloaded skills archive'); // Extract to cache directory const cacheDir = options.cacheDir || getCacheDir(); diff --git a/packages/b2c-tooling-sdk/src/skills/index.ts b/packages/b2c-tooling-sdk/src/skills/index.ts index 2259b496..a562e549 100644 --- a/packages/b2c-tooling-sdk/src/skills/index.ts +++ b/packages/b2c-tooling-sdk/src/skills/index.ts @@ -60,7 +60,14 @@ export type { } from './types.js'; // Agent/IDE utilities -export {IDE_CONFIGS, ALL_IDE_TYPES, detectInstalledIdes, getSkillInstallPath, getIdeDisplayName} from './agents.js'; +export { + IDE_CONFIGS, + ALL_IDE_TYPES, + detectInstalledIdes, + getSkillInstallPath, + getIdeDisplayName, + getIdeDocsUrl, +} from './agents.js'; // GitHub/download utilities export { diff --git a/packages/b2c-tooling-sdk/src/skills/types.ts b/packages/b2c-tooling-sdk/src/skills/types.ts index 0daa88d4..c37883df 100644 --- a/packages/b2c-tooling-sdk/src/skills/types.ts +++ b/packages/b2c-tooling-sdk/src/skills/types.ts @@ -7,7 +7,7 @@ /** * Supported IDE types for skill installation. */ -export type IdeType = 'claude-code' | 'cursor' | 'windsurf' | 'github-copilot' | 'codex' | 'opencode' | 'manual'; +export type IdeType = 'claude-code' | 'cursor' | 'windsurf' | 'vscode' | 'codex' | 'opencode' | 'manual'; /** * Skill set categories matching the plugins directory structure. @@ -36,6 +36,8 @@ export interface IdeConfig { paths: IdePaths; /** Function to detect if IDE is installed */ detectInstalled: () => Promise; + /** Optional documentation URL for skill configuration */ + docsUrl?: string; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5860654b..52957484 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: packages/b2c-cli: dependencies: + '@inquirer/prompts': + specifier: ^8.2.0 + version: 8.2.0(@types/node@18.19.130) '@oclif/core': specifier: ^4 version: 4.8.0 @@ -1166,6 +1169,10 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} + '@inquirer/ansi@2.0.3': + resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/checkbox@4.3.1': resolution: {integrity: sha512-rOcLotrptYIy59SGQhKlU0xBg1vvcVl2FdPIEclUvKHh0wo12OfGkId/01PIMJ/V+EimJ77t085YabgnQHBa5A==} engines: {node: '>=18'} @@ -1175,6 +1182,15 @@ packages: '@types/node': optional: true + '@inquirer/checkbox@5.0.4': + resolution: {integrity: sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@3.2.0': resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} engines: {node: '>=18'} @@ -1188,6 +1204,15 @@ packages: '@types/node': optional: true + '@inquirer/confirm@6.0.4': + resolution: {integrity: sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@10.3.1': resolution: {integrity: sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==} engines: {node: '>=18'} @@ -1197,6 +1222,15 @@ packages: '@types/node': optional: true + '@inquirer/core@11.1.1': + resolution: {integrity: sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@9.2.1': resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} engines: {node: '>=18'} @@ -1210,6 +1244,15 @@ packages: '@types/node': optional: true + '@inquirer/editor@5.0.4': + resolution: {integrity: sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/expand@4.0.22': resolution: {integrity: sha512-9XOjCjvioLjwlq4S4yXzhvBmAXj5tG+jvva0uqedEsQ9VD8kZ+YT7ap23i0bIXOtow+di4+u3i6u26nDqEfY4Q==} engines: {node: '>=18'} @@ -1219,6 +1262,15 @@ packages: '@types/node': optional: true + '@inquirer/expand@5.0.4': + resolution: {integrity: sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -1228,10 +1280,23 @@ packages: '@types/node': optional: true + '@inquirer/external-editor@2.0.3': + resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} + '@inquirer/figures@2.0.3': + resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/input@2.3.0': resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==} engines: {node: '>=18'} @@ -1245,6 +1310,15 @@ packages: '@types/node': optional: true + '@inquirer/input@5.0.4': + resolution: {integrity: sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/number@3.0.22': resolution: {integrity: sha512-oAdMJXz++fX58HsIEYmvuf5EdE8CfBHHXjoi9cTcQzgFoHGZE+8+Y3P38MlaRMeBvAVnkWtAxMUF6urL2zYsbg==} engines: {node: '>=18'} @@ -1254,6 +1328,15 @@ packages: '@types/node': optional: true + '@inquirer/number@4.0.4': + resolution: {integrity: sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/password@4.0.22': resolution: {integrity: sha512-CbdqK1ioIr0Y3akx03k/+Twf+KSlHjn05hBL+rmubMll7PsDTGH0R4vfFkr+XrkB0FOHrjIwVP9crt49dgt+1g==} engines: {node: '>=18'} @@ -1263,6 +1346,15 @@ packages: '@types/node': optional: true + '@inquirer/password@5.0.4': + resolution: {integrity: sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/prompts@7.10.0': resolution: {integrity: sha512-X2HAjY9BClfFkJ2RP3iIiFxlct5JJVdaYYXhA7RKxsbc9KL+VbId79PSoUGH/OLS011NFbHHDMDcBKUj3T89+Q==} engines: {node: '>=18'} @@ -1272,6 +1364,15 @@ packages: '@types/node': optional: true + '@inquirer/prompts@8.2.0': + resolution: {integrity: sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/rawlist@4.1.10': resolution: {integrity: sha512-Du4uidsgTMkoH5izgpfyauTL/ItVHOLsVdcY+wGeoGaG56BV+/JfmyoQGniyhegrDzXpfn3D+LFHaxMDRygcAw==} engines: {node: '>=18'} @@ -1281,6 +1382,15 @@ packages: '@types/node': optional: true + '@inquirer/rawlist@5.2.0': + resolution: {integrity: sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/search@3.2.1': resolution: {integrity: sha512-cKiuUvETublmTmaOneEermfG2tI9ABpb7fW/LqzZAnSv4ZaJnbEis05lOkiBuYX5hNdnX0Q9ryOQyrNidb55WA==} engines: {node: '>=18'} @@ -1290,6 +1400,15 @@ packages: '@types/node': optional: true + '@inquirer/search@4.1.0': + resolution: {integrity: sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/select@2.5.0': resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==} engines: {node: '>=18'} @@ -1303,6 +1422,15 @@ packages: '@types/node': optional: true + '@inquirer/select@5.0.4': + resolution: {integrity: sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@1.5.5': resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} engines: {node: '>=18'} @@ -1320,6 +1448,15 @@ packages: '@types/node': optional: true + '@inquirer/type@4.0.3': + resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -4073,6 +4210,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -7335,6 +7476,8 @@ snapshots: '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.3': {} + '@inquirer/checkbox@4.3.1(@types/node@18.19.130)': dependencies: '@inquirer/ansi': 1.0.2 @@ -7355,6 +7498,15 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/checkbox@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/confirm@3.2.0': dependencies: '@inquirer/core': 9.2.1 @@ -7374,6 +7526,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/confirm@6.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/core@10.3.1(@types/node@18.19.130)': dependencies: '@inquirer/ansi': 1.0.2 @@ -7400,6 +7559,18 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/core@11.1.1(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@18.19.130) + cli-width: 4.1.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + wrap-ansi: 9.0.2 + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/core@9.2.1': dependencies: '@inquirer/figures': 1.0.15 @@ -7431,6 +7602,14 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/editor@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/external-editor': 2.0.3(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/expand@4.0.22(@types/node@18.19.130)': dependencies: '@inquirer/core': 10.3.1(@types/node@18.19.130) @@ -7447,6 +7626,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/expand@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/external-editor@1.0.3(@types/node@18.19.130)': dependencies: chardet: 2.1.1 @@ -7461,8 +7647,17 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/external-editor@2.0.3(@types/node@18.19.130)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.3': {} + '@inquirer/input@2.3.0': dependencies: '@inquirer/core': 9.2.1 @@ -7482,6 +7677,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/input@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/number@3.0.22(@types/node@18.19.130)': dependencies: '@inquirer/core': 10.3.1(@types/node@18.19.130) @@ -7496,6 +7698,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/number@4.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/password@4.0.22(@types/node@18.19.130)': dependencies: '@inquirer/ansi': 1.0.2 @@ -7512,6 +7721,14 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/password@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/prompts@7.10.0(@types/node@18.19.130)': dependencies: '@inquirer/checkbox': 4.3.1(@types/node@18.19.130) @@ -7542,6 +7759,21 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/prompts@8.2.0(@types/node@18.19.130)': + dependencies: + '@inquirer/checkbox': 5.0.4(@types/node@18.19.130) + '@inquirer/confirm': 6.0.4(@types/node@18.19.130) + '@inquirer/editor': 5.0.4(@types/node@18.19.130) + '@inquirer/expand': 5.0.4(@types/node@18.19.130) + '@inquirer/input': 5.0.4(@types/node@18.19.130) + '@inquirer/number': 4.0.4(@types/node@18.19.130) + '@inquirer/password': 5.0.4(@types/node@18.19.130) + '@inquirer/rawlist': 5.2.0(@types/node@18.19.130) + '@inquirer/search': 4.1.0(@types/node@18.19.130) + '@inquirer/select': 5.0.4(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/rawlist@4.1.10(@types/node@18.19.130)': dependencies: '@inquirer/core': 10.3.1(@types/node@18.19.130) @@ -7558,6 +7790,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/rawlist@5.2.0(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/search@3.2.1(@types/node@18.19.130)': dependencies: '@inquirer/core': 10.3.1(@types/node@18.19.130) @@ -7576,6 +7815,14 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/search@4.1.0(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/select@2.5.0': dependencies: '@inquirer/core': 9.2.1 @@ -7604,6 +7851,15 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/select@5.0.4(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@18.19.130) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + '@inquirer/type@1.5.5': dependencies: mute-stream: 1.0.0 @@ -7620,6 +7876,10 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@inquirer/type@4.0.3(@types/node@18.19.130)': + optionalDependencies: + '@types/node': 18.19.130 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -10807,6 +11067,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {}