From ba3d3d9238246b05ae71e27a47b6c1e5638b1eca Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 15 May 2026 16:47:13 +0400 Subject: [PATCH 01/36] Skill Quick Install --- README.md | 25 ++++++++ install.ps1 | 172 +++++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 install.ps1 create mode 100755 install.sh diff --git a/README.md b/README.md index df0ed4a..485a60c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,31 @@ Copy or symlink the content of the `skills/` subdirectory to the folder recogniz | OpenCode | `~/.config/opencode/skills/` | | Junie | `~/.junie/skills/` | +### Quick Install + +**macOS / Linux:** + +```bash +curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash +``` + +**Windows (PowerShell 5+):** + +```powershell +iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex +``` + +**Flags:** + +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:--------------|:------------------|:--------|:-----------------------------------------------------| +| `--version N` | `-Version N` | `2` | Major guideline version. Reads `v/skills/`. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) to download. | +| `--no-claude` | `-NoClaude` | off | Skip `~/.claude/skills/`. | +| `--no-codex` | `-NoCodex` | off | Skip `~/.codex/skills/`. | + +> The quick install covers global skills only. For the project `AGENTS.md` / `CLAUDE.md` file, MCP servers, and Playwright setup, follow the manual steps in the [How to Use](#how-to-use) section below. + #### Example Using symlink for Claude Code: diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..df87a69 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,172 @@ +<# +.SYNOPSIS + Installs Jmix agent skills into the global skills directories used by + Claude Code (~/.claude/skills), Codex (~/.codex/skills), + OpenCode (~/.config/opencode/skills) and Junie (~/.junie/skills). + +.DESCRIPTION + Downloads the jmix-framework/jmix-agent-guidelines repository archive at + the requested git ref, then copies every direct subdirectory of + v/skills/ into the global skills folder of each enabled agent. + Existing skill directories are backed up to .bak-/ before + being overwritten. + +.PARAMETER Version + Major guideline version. Default: 2. Reads v/skills/ from repo. + +.PARAMETER Ref + Git ref (branch or tag) to download. Default: main. + +.PARAMETER NoClaude + Skip installing into ~/.claude/skills. + +.PARAMETER NoCodex + Skip installing into ~/.codex/skills. + +.PARAMETER NoOpenCode + Skip installing into ~/.config/opencode/skills. + +.PARAMETER NoJunie + Skip installing into ~/.junie/skills. + +.EXAMPLE + iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex + +.EXAMPLE + & ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -NoCodex +#> +[CmdletBinding()] +param( + [string]$Version = '2', + [string]$Ref = 'main', + [switch]$NoClaude, + [switch]$NoCodex, + [switch]$NoOpenCode, + [switch]$NoJunie +) + +$ErrorActionPreference = 'Stop' + +$RepoOwner = 'jmix-framework' +$RepoName = 'jmix-agent-guidelines' + +function Write-Info { + param([string]$Message) + Write-Output $Message +} + +function Write-ErrAndExit { + param([string]$Message) + [Console]::Error.WriteLine("error: $Message") + exit 1 +} + +if ($NoClaude -and $NoCodex -and $NoOpenCode -and $NoJunie) { + Write-ErrAndExit 'nothing to install (all -No* flags set)' +} + +if (-not (Get-Command Expand-Archive -ErrorAction SilentlyContinue)) { + Write-ErrAndExit 'Expand-Archive not found. PowerShell 5+ is required.' +} + +$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("jmix-install-" + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $staging -Force | Out-Null + +try { + $archiveUrl = "https://codeload.github.com/$RepoOwner/$RepoName/zip/$Ref" + $zipPath = Join-Path $staging 'source.zip' + + Write-Info "Downloading $archiveUrl" + try { + Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile $zipPath + } catch { + $status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } + Write-ErrAndExit "failed to download $archiveUrl (HTTP $status)" + } + + Expand-Archive -Path $zipPath -DestinationPath $staging -Force + + $extractedDir = Get-ChildItem -Path $staging -Directory | + Where-Object { $_.Name -like "$RepoName-*" } | + Select-Object -First 1 + + if (-not $extractedDir) { + Write-ErrAndExit "extracted source directory not found in $staging" + } + + $sourceSkillsDir = Join-Path $extractedDir.FullName "v$Version/skills" + if (-not (Test-Path $sourceSkillsDir -PathType Container)) { + $available = (Get-ChildItem -Path $extractedDir.FullName -Directory | Select-Object -ExpandProperty Name) -join ' ' + Write-ErrAndExit "v$Version/skills/ not found in $Ref. Available top-level entries: $available" + } + + $script:sourceSkillsDir = $sourceSkillsDir + $script:timestamp = (Get-Date).ToString('yyyyMMdd-HHmmss') + + function Install-Skills { + param( + [string]$TargetDir, + [string]$AgentLabel + ) + + Write-Info '' + Write-Info "Installing skills for $AgentLabel into $TargetDir" + + try { + if (-not (Test-Path $TargetDir)) { + New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null + } + } catch { + Write-ErrAndExit "cannot write to ${TargetDir}: $($_.Exception.Message)" + } + + $count = 0 + $skillDirs = Get-ChildItem -Path $script:sourceSkillsDir -Directory + foreach ($skill in $skillDirs) { + $name = $skill.Name + $dest = Join-Path $TargetDir $name + $backupName = "$name.bak-$script:timestamp" + + try { + if (Test-Path $dest) { + Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop + Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Updated: $name (backup: $backupName)" + } else { + Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Installed: $name" + } + } catch { + Write-ErrAndExit "cannot write to ${dest}: $($_.Exception.Message)" + } + + $count++ + } + + Write-Info " $count skill(s) processed for $AgentLabel" + } + + $targets = @() + if (-not $NoClaude) { + Install-Skills -TargetDir (Join-Path $HOME '.claude/skills') -AgentLabel 'Claude' + $targets += 'Claude' + } + if (-not $NoCodex) { + Install-Skills -TargetDir (Join-Path $HOME '.codex/skills') -AgentLabel 'Codex' + $targets += 'Codex' + } + if (-not $NoOpenCode) { + Install-Skills -TargetDir (Join-Path $HOME '.config/opencode/skills') -AgentLabel 'OpenCode' + $targets += 'OpenCode' + } + if (-not $NoJunie) { + Install-Skills -TargetDir (Join-Path $HOME '.junie/skills') -AgentLabel 'Junie' + $targets += 'Junie' + } + + Write-Info '' + Write-Info "Done. Installed skills for: $($targets -join ', ')" +} +finally { + Remove-Item -Recurse -Force -Path $staging -ErrorAction SilentlyContinue +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..75c55fe --- /dev/null +++ b/install.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# Installs Jmix agent skills into the global skills directories used by: +# - Claude Code (~/.claude/skills) +# - Codex (~/.codex/skills) +# - OpenCode (~/.config/opencode/skills) +# - Junie (~/.junie/skills) + +set -euo pipefail + +REPO_OWNER="jmix-framework" +REPO_NAME="jmix-agent-guidelines" +VERSION="2" +REF="main" +INSTALL_CLAUDE=1 +INSTALL_CODEX=1 +INSTALL_OPENCODE=1 +INSTALL_JUNIE=1 + +usage() { + cat <<'EOF' +Installs Jmix agent skills into global skills directories. + +Usage: + install.sh [--version N] [--ref REF] [--no-claude] [--no-codex] [--no-opencode] [--no-junie] + +Flags: + --version N Major guideline version (default: 2). Reads v/skills/ from repo. + --ref REF Git ref to download (default: main). + --no-claude Skip installing into ~/.claude/skills. + --no-codex Skip installing into ~/.codex/skills. + --no-opencode Skip installing into ~/.config/opencode/skills. + --no-junie Skip installing into ~/.junie/skills. + -h, --help Show this help. +EOF +} + +log() { + printf '%s\n' "$*" +} + +err() { + printf 'error: %s\n' "$*" >&2 +} + +die() { + err "$*" + exit 1 +} + +require_tool() { + command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." +} + +while [ $# -gt 0 ]; do + case "$1" in + --version) + [ $# -ge 2 ] || die "--version requires an argument" + VERSION="$2" + shift 2 + ;; + --ref) + [ $# -ge 2 ] || die "--ref requires an argument" + REF="$2" + shift 2 + ;; + --no-claude) + INSTALL_CLAUDE=0 + shift + ;; + --no-codex) + INSTALL_CODEX=0 + shift + ;; + --no-opencode) + INSTALL_OPENCODE=0 + shift + ;; + --no-junie) + INSTALL_JUNIE=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_JUNIE" -eq 0 ]; then + die "nothing to install (all --no-* flags set)" +fi + +require_tool curl +require_tool tar + +STAGING="$(mktemp -d 2>/dev/null || mktemp -d -t jmix-install)" +trap 'rm -rf "$STAGING"' EXIT + +TARBALL_URL="https://codeload.github.com/${REPO_OWNER}/${REPO_NAME}/tar.gz/${REF}" +TARBALL_PATH="${STAGING}/source.tar.gz" + +log "Downloading ${TARBALL_URL}" +HTTP_STATUS="$(curl -sSL -w '%{http_code}' -o "$TARBALL_PATH" "$TARBALL_URL" || echo "000")" +if [ "$HTTP_STATUS" != "200" ]; then + die "failed to download ${TARBALL_URL} (HTTP ${HTTP_STATUS})" +fi + +tar -xzf "$TARBALL_PATH" -C "$STAGING" + +EXTRACTED_DIR="$(find "$STAGING" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n 1)" +[ -n "$EXTRACTED_DIR" ] || die "extracted source directory not found in ${STAGING}" + +SOURCE_SKILLS_DIR="${EXTRACTED_DIR}/v${VERSION}/skills" +if [ ! -d "$SOURCE_SKILLS_DIR" ]; then + AVAILABLE="$(find "$EXTRACTED_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ' ')" + die "v${VERSION}/skills/ not found in ${REF}. Available top-level entries: ${AVAILABLE}" +fi + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" + +install_to_target() { + target_dir="$1" + agent_label="$2" + + log "" + log "Installing skills for ${agent_label} into ${target_dir}" + mkdir -p "$target_dir" || die "cannot write to ${target_dir}: mkdir failed" + + count=0 + for skill in "$SOURCE_SKILLS_DIR"/*/; do + [ -d "$skill" ] || continue + name="$(basename "$skill")" + dest="${target_dir}/${name}" + + if [ -e "$dest" ]; then + backup="${dest}.bak-${TIMESTAMP}" + mv "$dest" "$backup" || die "cannot write to ${dest}: rename failed" + cp -R "$skill" "$dest" || die "cannot write to ${dest}: copy failed" + log " Updated: ${name} (backup: $(basename "$backup"))" + else + cp -R "$skill" "$dest" || die "cannot write to ${dest}: copy failed" + log " Installed: ${name}" + fi + count=$((count + 1)) + done + + log " ${count} skill(s) processed for ${agent_label}" +} + +TARGETS="" + +append_target() { + if [ -n "$TARGETS" ]; then + TARGETS="${TARGETS}, $1" + else + TARGETS="$1" + fi +} + +if [ "$INSTALL_CLAUDE" -eq 1 ]; then + install_to_target "${HOME}/.claude/skills" "Claude" + append_target "Claude" +fi +if [ "$INSTALL_CODEX" -eq 1 ]; then + install_to_target "${HOME}/.codex/skills" "Codex" + append_target "Codex" +fi +if [ "$INSTALL_OPENCODE" -eq 1 ]; then + install_to_target "${HOME}/.config/opencode/skills" "OpenCode" + append_target "OpenCode" +fi +if [ "$INSTALL_JUNIE" -eq 1 ]; then + install_to_target "${HOME}/.junie/skills" "Junie" + append_target "Junie" +fi + +log "" +log "Done. Installed skills for: ${TARGETS}" From 784354a50d5de1ab58c1f3029dbd708d65dc3ac3 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 15 May 2026 16:56:07 +0400 Subject: [PATCH 02/36] Add Studio meta-data --- .studio/studio-meta-data.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .studio/studio-meta-data.json diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json new file mode 100644 index 0000000..d894ba4 --- /dev/null +++ b/.studio/studio-meta-data.json @@ -0,0 +1,13 @@ +{ + "skill-install": { + "windows": { + "command": "iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex" + }, + "macos": { + "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash" + }, + "linux": { + "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash" + } + } +} \ No newline at end of file From e9f3cb4ff71849c2420dcb56b85550ebc53a2a39 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 15 May 2026 18:27:00 +0400 Subject: [PATCH 03/36] Add support for Jmix version flag --- .studio/studio-meta-data.json | 6 ++-- README.md | 12 +++---- install.ps1 | 63 +++++++++++++++++++++++++++++++--- install.sh | 64 +++++++++++++++++++++++++++++++---- 4 files changed, 125 insertions(+), 20 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index d894ba4..b558aa2 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -1,13 +1,13 @@ { "skill-install": { "windows": { - "command": "iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex" + "command": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -Version \"${JMIX_VERSION}\"" }, "macos": { - "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash" + "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" }, "linux": { - "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash" + "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" } } } \ No newline at end of file diff --git a/README.md b/README.md index 485a60c..b64bc7d 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,12 @@ iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines **Flags:** -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:--------------|:------------------|:--------|:-----------------------------------------------------| -| `--version N` | `-Version N` | `2` | Major guideline version. Reads `v/skills/`. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) to download. | -| `--no-claude` | `-NoClaude` | off | Skip `~/.claude/skills/`. | -| `--no-codex` | `-NoCodex` | off | Skip `~/.codex/skills/`. | +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:------------------|:------------------|:--------|:-------------------------------------| +| `--version x.y.z` | `-Version x.y.z` | `` | Jmix version. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) to download. | +| `--no-claude` | `-NoClaude` | off | Skip `~/.claude/skills/`. | +| `--no-codex` | `-NoCodex` | off | Skip `~/.codex/skills/`. | > The quick install covers global skills only. For the project `AGENTS.md` / `CLAUDE.md` file, MCP servers, and Playwright setup, follow the manual steps in the [How to Use](#how-to-use) section below. diff --git a/install.ps1 b/install.ps1 index df87a69..c471075 100644 --- a/install.ps1 +++ b/install.ps1 @@ -12,7 +12,9 @@ being overwritten. .PARAMETER Version - Major guideline version. Default: 2. Reads v/skills/ from repo. + Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks the + best-matching guideline folder: exact -> major.minor -> major. When + omitted or empty, the highest available major version (vN) is used. .PARAMETER Ref Git ref (branch or tag) to download. Default: main. @@ -37,7 +39,7 @@ #> [CmdletBinding()] param( - [string]$Version = '2', + [string]$Version = '', [string]$Ref = 'main', [switch]$NoClaude, [switch]$NoCodex, @@ -94,12 +96,63 @@ try { Write-ErrAndExit "extracted source directory not found in $staging" } - $sourceSkillsDir = Join-Path $extractedDir.FullName "v$Version/skills" - if (-not (Test-Path $sourceSkillsDir -PathType Container)) { + function Resolve-SkillsDir { + param( + [string]$ExtractedDir, + [string]$Requested + ) + + if ([string]::IsNullOrWhiteSpace($Requested)) { + $best = $null + $bestNum = -1 + foreach ($dir in Get-ChildItem -Path $ExtractedDir -Directory) { + if ($dir.Name -notmatch '^v(\d+)$') { continue } + $skillsPath = Join-Path $dir.FullName 'skills' + if (-not (Test-Path $skillsPath -PathType Container)) { continue } + $num = [int]$Matches[1] + if ($num -gt $bestNum) { + $bestNum = $num + $best = $skillsPath + } + } + return $best + } + + $exact = Join-Path $ExtractedDir "v$Requested/skills" + if (Test-Path $exact -PathType Container) { return $exact } + + $parts = $Requested -split '[.-]' + if ($parts.Length -ge 2 -and $parts[0] -ne '' -and $parts[1] -ne '') { + $majorMinor = "$($parts[0]).$($parts[1])" + if ($majorMinor -ne $Requested) { + $candidate = Join-Path $ExtractedDir "v$majorMinor/skills" + if (Test-Path $candidate -PathType Container) { return $candidate } + } + } + + if ($parts.Length -ge 1 -and $parts[0] -ne '') { + $major = $parts[0] + if ($major -ne $Requested) { + $candidate = Join-Path $ExtractedDir "v$major/skills" + if (Test-Path $candidate -PathType Container) { return $candidate } + } + } + + return $null + } + + $sourceSkillsDir = Resolve-SkillsDir -ExtractedDir $extractedDir.FullName -Requested $Version + if (-not $sourceSkillsDir) { $available = (Get-ChildItem -Path $extractedDir.FullName -Directory | Select-Object -ExpandProperty Name) -join ' ' - Write-ErrAndExit "v$Version/skills/ not found in $Ref. Available top-level entries: $available" + if ([string]::IsNullOrWhiteSpace($Version)) { + Write-ErrAndExit "no v/skills/ directory found in $Ref. Available top-level entries: $available" + } else { + Write-ErrAndExit "no guideline folder matches version '$Version' in $Ref. Tried exact, major.minor, major. Available top-level entries: $available" + } } + Write-Info "Using guidelines from $($sourceSkillsDir.Substring($extractedDir.FullName.Length + 1))" + $script:sourceSkillsDir = $sourceSkillsDir $script:timestamp = (Get-Date).ToString('yyyyMMdd-HHmmss') diff --git a/install.sh b/install.sh index 75c55fe..184c2c6 100755 --- a/install.sh +++ b/install.sh @@ -9,7 +9,7 @@ set -euo pipefail REPO_OWNER="jmix-framework" REPO_NAME="jmix-agent-guidelines" -VERSION="2" +VERSION="" REF="main" INSTALL_CLAUDE=1 INSTALL_CODEX=1 @@ -21,10 +21,12 @@ usage() { Installs Jmix agent skills into global skills directories. Usage: - install.sh [--version N] [--ref REF] [--no-claude] [--no-codex] [--no-opencode] [--no-junie] + install.sh [--version V] [--ref REF] [--no-claude] [--no-codex] [--no-opencode] [--no-junie] Flags: - --version N Major guideline version (default: 2). Reads v/skills/ from repo. + --version V Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks + the best-matching guideline folder: exact -> major.minor -> major. + When omitted, the highest available major version (vN) is used. --ref REF Git ref to download (default: main). --no-claude Skip installing into ~/.claude/skills. --no-codex Skip installing into ~/.codex/skills. @@ -113,12 +115,62 @@ tar -xzf "$TARBALL_PATH" -C "$STAGING" EXTRACTED_DIR="$(find "$STAGING" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n 1)" [ -n "$EXTRACTED_DIR" ] || die "extracted source directory not found in ${STAGING}" -SOURCE_SKILLS_DIR="${EXTRACTED_DIR}/v${VERSION}/skills" -if [ ! -d "$SOURCE_SKILLS_DIR" ]; then +resolve_skills_dir() { + extracted="$1" + requested="$2" + + if [ -z "$requested" ]; then + best="" + best_num=-1 + for dir in "$extracted"/v*/; do + [ -d "${dir}skills" ] || continue + name="${dir%/}" + name="${name##*/v}" + case "$name" in + ''|*[!0-9]*) continue ;; + esac + if [ "$name" -gt "$best_num" ]; then + best_num="$name" + best="${dir}skills" + fi + done + [ -n "$best" ] || return 1 + printf '%s\n' "$best" + return 0 + fi + + if [ -d "${extracted}/v${requested}/skills" ]; then + printf '%s\n' "${extracted}/v${requested}/skills" + return 0 + fi + + major_minor="$(printf '%s' "$requested" | awk -F'[.-]' '{ if (NF >= 2 && $1 != "" && $2 != "") print $1"."$2 }')" + if [ -n "$major_minor" ] && [ "$major_minor" != "$requested" ] && [ -d "${extracted}/v${major_minor}/skills" ]; then + printf '%s\n' "${extracted}/v${major_minor}/skills" + return 0 + fi + + major="$(printf '%s' "$requested" | awk -F'[.-]' '{print $1}')" + if [ -n "$major" ] && [ "$major" != "$requested" ] && [ -d "${extracted}/v${major}/skills" ]; then + printf '%s\n' "${extracted}/v${major}/skills" + return 0 + fi + + return 1 +} + +SOURCE_SKILLS_DIR="$(resolve_skills_dir "$EXTRACTED_DIR" "$VERSION" || true)" +if [ -z "$SOURCE_SKILLS_DIR" ]; then AVAILABLE="$(find "$EXTRACTED_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ' ')" - die "v${VERSION}/skills/ not found in ${REF}. Available top-level entries: ${AVAILABLE}" + if [ -z "$VERSION" ]; then + die "no v/skills/ directory found in ${REF}. Available top-level entries: ${AVAILABLE}" + else + die "no guideline folder matches version '${VERSION}' in ${REF}. Tried exact, major.minor, major. Available top-level entries: ${AVAILABLE}" + fi fi +log "Using guidelines from ${SOURCE_SKILLS_DIR#${EXTRACTED_DIR}/}" + TIMESTAMP="$(date +%Y%m%d-%H%M%S)" install_to_target() { From 70610c2638c41c2ccc81f056d84f47acdd5117c1 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 15 May 2026 18:36:28 +0400 Subject: [PATCH 04/36] Add version detect fallback mechanism --- install.ps1 | 90 ++++++++++++++++++++++++++++++++++++++--------------- install.sh | 79 ++++++++++++++++++++++++++++++---------------- 2 files changed, 117 insertions(+), 52 deletions(-) diff --git a/install.ps1 b/install.ps1 index c471075..af81fbf 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,8 +13,9 @@ .PARAMETER Version Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks the - best-matching guideline folder: exact -> major.minor -> major. When - omitted or empty, the highest available major version (vN) is used. + best-matching guideline folder: exact -> major.minor -> major. If + none of these match, or when omitted/empty, the latest available + version is used (sorted by major, then minor, then patch). .PARAMETER Ref Git ref (branch or tag) to download. Default: main. @@ -96,6 +97,42 @@ try { Write-ErrAndExit "extracted source directory not found in $staging" } + function Get-VersionSortKey { + param([string]$Version) + $parts = $Version -split '[.-]' + $key = '' + for ($i = 0; $i -lt 5; $i++) { + $segment = if ($i -lt $parts.Length) { $parts[$i] } else { '0' } + $value = 0 + [void][int]::TryParse($segment, [ref]$value) + $key += $value.ToString('00000') + } + return $key + } + + function Find-LatestSkillsDir { + param([string]$ExtractedDir) + $bestKey = $null + $bestPath = $null + foreach ($dir in Get-ChildItem -Path $ExtractedDir -Directory) { + if (-not $dir.Name.StartsWith('v')) { continue } + $skillsPath = Join-Path $dir.FullName 'skills' + if (-not (Test-Path $skillsPath -PathType Container)) { continue } + $version = $dir.Name.Substring(1) + if ([string]::IsNullOrEmpty($version)) { continue } + $key = Get-VersionSortKey -Version $version + if ($null -eq $bestKey -or [string]::Compare($key, $bestKey) -gt 0) { + $bestKey = $key + $bestPath = $skillsPath + } + } + return $bestPath + } + + # Returns [PSCustomObject] with Path + Status: + # Status = 'matched' (exact, major.minor, major, or no-version default) + # Status = 'fallback' (requested version did not match any tier; latest used) + # Status = 'none' (no v*/skills dir exists) function Resolve-SkillsDir { param( [string]$ExtractedDir, @@ -103,30 +140,26 @@ try { ) if ([string]::IsNullOrWhiteSpace($Requested)) { - $best = $null - $bestNum = -1 - foreach ($dir in Get-ChildItem -Path $ExtractedDir -Directory) { - if ($dir.Name -notmatch '^v(\d+)$') { continue } - $skillsPath = Join-Path $dir.FullName 'skills' - if (-not (Test-Path $skillsPath -PathType Container)) { continue } - $num = [int]$Matches[1] - if ($num -gt $bestNum) { - $bestNum = $num - $best = $skillsPath - } + $path = Find-LatestSkillsDir -ExtractedDir $ExtractedDir + if ($path) { + return [PSCustomObject]@{ Path = $path; Status = 'matched' } } - return $best + return [PSCustomObject]@{ Path = $null; Status = 'none' } } $exact = Join-Path $ExtractedDir "v$Requested/skills" - if (Test-Path $exact -PathType Container) { return $exact } + if (Test-Path $exact -PathType Container) { + return [PSCustomObject]@{ Path = $exact; Status = 'matched' } + } $parts = $Requested -split '[.-]' if ($parts.Length -ge 2 -and $parts[0] -ne '' -and $parts[1] -ne '') { $majorMinor = "$($parts[0]).$($parts[1])" if ($majorMinor -ne $Requested) { $candidate = Join-Path $ExtractedDir "v$majorMinor/skills" - if (Test-Path $candidate -PathType Container) { return $candidate } + if (Test-Path $candidate -PathType Container) { + return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } + } } } @@ -134,23 +167,30 @@ try { $major = $parts[0] if ($major -ne $Requested) { $candidate = Join-Path $ExtractedDir "v$major/skills" - if (Test-Path $candidate -PathType Container) { return $candidate } + if (Test-Path $candidate -PathType Container) { + return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } + } } } - return $null + $fallback = Find-LatestSkillsDir -ExtractedDir $ExtractedDir + if ($fallback) { + return [PSCustomObject]@{ Path = $fallback; Status = 'fallback' } + } + return [PSCustomObject]@{ Path = $null; Status = 'none' } } - $sourceSkillsDir = Resolve-SkillsDir -ExtractedDir $extractedDir.FullName -Requested $Version - if (-not $sourceSkillsDir) { + $resolved = Resolve-SkillsDir -ExtractedDir $extractedDir.FullName -Requested $Version + if ($resolved.Status -eq 'none' -or -not $resolved.Path) { $available = (Get-ChildItem -Path $extractedDir.FullName -Directory | Select-Object -ExpandProperty Name) -join ' ' - if ([string]::IsNullOrWhiteSpace($Version)) { - Write-ErrAndExit "no v/skills/ directory found in $Ref. Available top-level entries: $available" - } else { - Write-ErrAndExit "no guideline folder matches version '$Version' in $Ref. Tried exact, major.minor, major. Available top-level entries: $available" - } + Write-ErrAndExit "no v*/skills directory found in $Ref. Available top-level entries: $available" } + $sourceSkillsDir = $resolved.Path + $resolvedVersionDir = Split-Path -Leaf (Split-Path -Parent $sourceSkillsDir) + if ($resolved.Status -eq 'fallback') { + Write-Info "Version '$Version' did not match any folder, falling back to latest available ($resolvedVersionDir)" + } Write-Info "Using guidelines from $($sourceSkillsDir.Substring($extractedDir.FullName.Length + 1))" $script:sourceSkillsDir = $sourceSkillsDir diff --git a/install.sh b/install.sh index 184c2c6..db64011 100755 --- a/install.sh +++ b/install.sh @@ -25,8 +25,10 @@ Usage: Flags: --version V Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks - the best-matching guideline folder: exact -> major.minor -> major. - When omitted, the highest available major version (vN) is used. + the best-matching guideline folder: exact -> major.minor -> + major. If none of these match, or when the flag is omitted / + empty, the latest available version is used (sorted by + major, then minor, then patch). --ref REF Git ref to download (default: main). --no-claude Skip installing into ~/.claude/skills. --no-codex Skip installing into ~/.codex/skills. @@ -115,28 +117,48 @@ tar -xzf "$TARBALL_PATH" -C "$STAGING" EXTRACTED_DIR="$(find "$STAGING" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n 1)" [ -n "$EXTRACTED_DIR" ] || die "extracted source directory not found in ${STAGING}" +version_sort_key() { + printf '%s' "$1" | awk -F'[.-]' '{ + for (i = 1; i <= 5; i++) { + v = (i <= NF) ? $i : 0 + if (v ~ /^[0-9]+$/) printf "%05d", v + else printf "%05d", 0 + } + print "" + }' +} + +find_latest_skills_dir() { + extracted="$1" + best_key="" + best_path="" + for dir in "$extracted"/v*/; do + [ -d "${dir}skills" ] || continue + name="${dir%/}" + name="${name##*/v}" + [ -n "$name" ] || continue + key="$(version_sort_key "$name")" + if [ -z "$best_key" ] || [ "$key" \> "$best_key" ]; then + best_key="$key" + best_path="${dir}skills" + fi + done + [ -n "$best_path" ] || return 1 + printf '%s\n' "$best_path" +} + +# Prints resolved skills dir on success. +# Exit codes: +# 0 - matched (exact, major.minor, major, or no-version default) +# 2 - fallback used (requested version didn't match any tier) +# 1 - no v*/skills dir present at all resolve_skills_dir() { extracted="$1" requested="$2" if [ -z "$requested" ]; then - best="" - best_num=-1 - for dir in "$extracted"/v*/; do - [ -d "${dir}skills" ] || continue - name="${dir%/}" - name="${name##*/v}" - case "$name" in - ''|*[!0-9]*) continue ;; - esac - if [ "$name" -gt "$best_num" ]; then - best_num="$name" - best="${dir}skills" - fi - done - [ -n "$best" ] || return 1 - printf '%s\n' "$best" - return 0 + find_latest_skills_dir "$extracted" + return $? fi if [ -d "${extracted}/v${requested}/skills" ]; then @@ -156,19 +178,22 @@ resolve_skills_dir() { return 0 fi - return 1 + fallback_path="$(find_latest_skills_dir "$extracted")" || return 1 + printf '%s\n' "$fallback_path" + return 2 } -SOURCE_SKILLS_DIR="$(resolve_skills_dir "$EXTRACTED_DIR" "$VERSION" || true)" -if [ -z "$SOURCE_SKILLS_DIR" ]; then +RESOLVE_STATUS=0 +SOURCE_SKILLS_DIR="$(resolve_skills_dir "$EXTRACTED_DIR" "$VERSION")" || RESOLVE_STATUS=$? +if [ "$RESOLVE_STATUS" -eq 1 ] || [ -z "$SOURCE_SKILLS_DIR" ]; then AVAILABLE="$(find "$EXTRACTED_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ' ')" - if [ -z "$VERSION" ]; then - die "no v/skills/ directory found in ${REF}. Available top-level entries: ${AVAILABLE}" - else - die "no guideline folder matches version '${VERSION}' in ${REF}. Tried exact, major.minor, major. Available top-level entries: ${AVAILABLE}" - fi + die "no v*/skills directory found in ${REF}. Available top-level entries: ${AVAILABLE}" fi +RESOLVED_VERSION_DIR="$(basename "$(dirname "$SOURCE_SKILLS_DIR")")" +if [ "$RESOLVE_STATUS" -eq 2 ]; then + log "Version '${VERSION}' did not match any folder, falling back to latest available (${RESOLVED_VERSION_DIR})" +fi log "Using guidelines from ${SOURCE_SKILLS_DIR#${EXTRACTED_DIR}/}" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" From 7907dc582b0878feb62b4c1b14e1fca7db1fca62 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 18 May 2026 17:32:21 +0400 Subject: [PATCH 05/36] Rewrite enhance scripts and studio-meta-data.json --- .studio/studio-meta-data.json | 30 +- install.ps1 | 761 ++++++++++++++++++++++++-------- install.sh | 803 ++++++++++++++++++++++++++++------ 3 files changed, 1276 insertions(+), 318 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index b558aa2..47fffa3 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -1,13 +1,23 @@ { - "skill-install": { - "windows": { - "command": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -Version \"${JMIX_VERSION}\"" - }, - "macos": { - "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" - }, - "linux": { - "command": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" - } + "install-flow": { + "steps": [ + { + "message": "Configure AI agents for Jmix", + "options": [ + { + "label": "Start setup", + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -Version \"${JMIX_VERSION}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" + } + }, + { + "label": "Skip", + "skip": true + } + ] + } + ] } } \ No newline at end of file diff --git a/install.ps1 b/install.ps1 index af81fbf..7f1685f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,47 +1,67 @@ <# .SYNOPSIS - Installs Jmix agent skills into the global skills directories used by - Claude Code (~/.claude/skills), Codex (~/.codex/skills), - OpenCode (~/.config/opencode/skills) and Junie (~/.junie/skills). + Jmix AI Agent Guidelines installer. .DESCRIPTION - Downloads the jmix-framework/jmix-agent-guidelines repository archive at - the requested git ref, then copies every direct subdirectory of - v/skills/ into the global skills folder of each enabled agent. - Existing skill directories are backed up to .bak-/ before - being overwritten. + Default invocation (no subcommand) launches an interactive wizard that + guides through: + 1. Installing Jmix skills globally for one or all agents. + 2. Adding project-level guidelines (CLAUDE.md / AGENTS.md / .junie\guidelines.md). + 3. Registering the JetBrains MCP server with the agent. + 4. Registering the Context7 MCP server with the agent. + + Subcommands are available for non-interactive use: + install.ps1 skills [-All | -Agent NAME] [-Version V] [-Ref REF] + install.ps1 agents-md [-All | -Agent NAME] [-Version V] [-Ref REF] + install.ps1 mcp-jetbrains [-All | -Agent NAME] + install.ps1 mcp-context7 [-All | -Agent NAME] [-Key KEY] + +.PARAMETER Subcommand + Optional subcommand. When omitted, the interactive wizard is started. .PARAMETER Version - Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks the - best-matching guideline folder: exact -> major.minor -> major. If - none of these match, or when omitted/empty, the latest available - version is used (sorted by major, then minor, then patch). + Jmix version (e.g. 2, 2.8, 2.8.0). Optional. Best-matching folder is picked: + exact -> major.minor -> major -> latest. .PARAMETER Ref Git ref (branch or tag) to download. Default: main. +.PARAMETER Agent + Single agent target: claude, codex, opencode, or junie. + +.PARAMETER All + Apply the subcommand to every supported agent. + +.PARAMETER Key + Context7 API key (mcp-context7). Prompted interactively when missing. + .PARAMETER NoClaude - Skip installing into ~/.claude/skills. + skills back-compat: skip ~/.claude/skills. .PARAMETER NoCodex - Skip installing into ~/.codex/skills. + skills back-compat: skip ~/.codex/skills. .PARAMETER NoOpenCode - Skip installing into ~/.config/opencode/skills. + skills back-compat: skip ~/.config/opencode/skills. .PARAMETER NoJunie - Skip installing into ~/.junie/skills. + skills back-compat: skip ~/.junie/skills. .EXAMPLE iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex .EXAMPLE - & ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -NoCodex + & ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) skills -Agent claude #> [CmdletBinding()] param( + [Parameter(Position = 0)] + [string]$Subcommand = '', [string]$Version = '', [string]$Ref = 'main', + [string]$Agent = '', + [switch]$All, + [string]$Key = '', [switch]$NoClaude, [switch]$NoCodex, [switch]$NoOpenCode, @@ -50,8 +70,24 @@ param( $ErrorActionPreference = 'Stop' -$RepoOwner = 'jmix-framework' -$RepoName = 'jmix-agent-guidelines' +$script:RepoOwner = 'jmix-framework' +$script:RepoName = 'jmix-agent-guidelines' + +$script:AllAgents = @('claude', 'codex', 'opencode', 'junie') +$script:JetbrainsAgents = @('claude', 'codex', 'opencode', 'junie') +$script:Context7Agents = @('claude', 'codex', 'opencode', 'junie') + +$script:TarballReady = $false +$script:Staging = $null +$script:ExtractedDir = $null +$script:SourceSkillsDir = $null +$script:SourceAgentsMd = $null +$script:ResolvedVersionDir = $null +$script:Timestamp = (Get-Date).ToString('yyyyMMdd-HHmmss') + +# ================================================================= +# Helpers +# ================================================================= function Write-Info { param([string]$Message) @@ -64,202 +100,585 @@ function Write-ErrAndExit { exit 1 } -if ($NoClaude -and $NoCodex -and $NoOpenCode -and $NoJunie) { - Write-ErrAndExit 'nothing to install (all -No* flags set)' +function Test-Tool { + param([string]$Tool) + if (-not (Get-Command $Tool -ErrorAction SilentlyContinue)) { + Write-ErrAndExit "$Tool not found. Install it and re-run." + } } -if (-not (Get-Command Expand-Archive -ErrorAction SilentlyContinue)) { - Write-ErrAndExit 'Expand-Archive not found. PowerShell 5+ is required.' +function Read-Prompt { + param( + [string]$Message, + [string]$Default = '' + ) + $hint = '' + if ($Default) { $hint = " [$Default]" } + $answer = Read-Host "$Message$hint" + if ([string]::IsNullOrEmpty($answer) -and $Default) { + return $Default + } + return $answer } -$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("jmix-install-" + [guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $staging -Force | Out-Null +function Read-YesNo { + param( + [string]$Message, + [string]$Default = 'y' + ) + $hint = if ($Default -eq 'n') { '[y/N]' } else { '[Y/n]' } + $answer = Read-Prompt -Message "$Message $hint" -Default $Default + return ($answer -match '^(y|yes)$') +} -try { - $archiveUrl = "https://codeload.github.com/$RepoOwner/$RepoName/zip/$Ref" - $zipPath = Join-Path $staging 'source.zip' +function Get-AgentLabel { + param([string]$Agent) + switch ($Agent) { + 'claude' { 'Claude Code' } + 'codex' { 'Codex' } + 'opencode' { 'OpenCode' } + 'junie' { 'Junie' } + default { $Agent } + } +} - Write-Info "Downloading $archiveUrl" - try { - Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile $zipPath - } catch { - $status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } - Write-ErrAndExit "failed to download $archiveUrl (HTTP $status)" +# ================================================================= +# Tarball + version resolution +# ================================================================= + +function Get-VersionSortKey { + param([string]$Version) + $parts = $Version -split '[.-]' + $key = '' + for ($i = 0; $i -lt 5; $i++) { + $segment = if ($i -lt $parts.Length) { $parts[$i] } else { '0' } + $value = 0 + [void][int]::TryParse($segment, [ref]$value) + $key += $value.ToString('00000') } + return $key +} - Expand-Archive -Path $zipPath -DestinationPath $staging -Force +function Find-LatestSkillsDir { + param([string]$ExtractedDir) + $bestKey = $null + $bestPath = $null + foreach ($dir in Get-ChildItem -Path $ExtractedDir -Directory) { + if (-not $dir.Name.StartsWith('v')) { continue } + $skillsPath = Join-Path $dir.FullName 'skills' + if (-not (Test-Path $skillsPath -PathType Container)) { continue } + $name = $dir.Name.Substring(1) + if ([string]::IsNullOrEmpty($name)) { continue } + $key = Get-VersionSortKey -Version $name + if ($null -eq $bestKey -or [string]::Compare($key, $bestKey) -gt 0) { + $bestKey = $key + $bestPath = $skillsPath + } + } + return $bestPath +} - $extractedDir = Get-ChildItem -Path $staging -Directory | - Where-Object { $_.Name -like "$RepoName-*" } | - Select-Object -First 1 +function Resolve-SkillsDir { + param( + [string]$ExtractedDir, + [string]$Requested + ) - if (-not $extractedDir) { - Write-ErrAndExit "extracted source directory not found in $staging" + if ([string]::IsNullOrWhiteSpace($Requested)) { + $path = Find-LatestSkillsDir -ExtractedDir $ExtractedDir + if ($path) { + return [PSCustomObject]@{ Path = $path; Status = 'matched' } + } + return [PSCustomObject]@{ Path = $null; Status = 'none' } } - function Get-VersionSortKey { - param([string]$Version) - $parts = $Version -split '[.-]' - $key = '' - for ($i = 0; $i -lt 5; $i++) { - $segment = if ($i -lt $parts.Length) { $parts[$i] } else { '0' } - $value = 0 - [void][int]::TryParse($segment, [ref]$value) - $key += $value.ToString('00000') - } - return $key - } - - function Find-LatestSkillsDir { - param([string]$ExtractedDir) - $bestKey = $null - $bestPath = $null - foreach ($dir in Get-ChildItem -Path $ExtractedDir -Directory) { - if (-not $dir.Name.StartsWith('v')) { continue } - $skillsPath = Join-Path $dir.FullName 'skills' - if (-not (Test-Path $skillsPath -PathType Container)) { continue } - $version = $dir.Name.Substring(1) - if ([string]::IsNullOrEmpty($version)) { continue } - $key = Get-VersionSortKey -Version $version - if ($null -eq $bestKey -or [string]::Compare($key, $bestKey) -gt 0) { - $bestKey = $key - $bestPath = $skillsPath + $exact = Join-Path $ExtractedDir "v$Requested/skills" + if (Test-Path $exact -PathType Container) { + return [PSCustomObject]@{ Path = $exact; Status = 'matched' } + } + + $parts = $Requested -split '[.-]' + if ($parts.Length -ge 2 -and $parts[0] -ne '' -and $parts[1] -ne '') { + $majorMinor = "$($parts[0]).$($parts[1])" + if ($majorMinor -ne $Requested) { + $candidate = Join-Path $ExtractedDir "v$majorMinor/skills" + if (Test-Path $candidate -PathType Container) { + return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } } } - return $bestPath - } - - # Returns [PSCustomObject] with Path + Status: - # Status = 'matched' (exact, major.minor, major, or no-version default) - # Status = 'fallback' (requested version did not match any tier; latest used) - # Status = 'none' (no v*/skills dir exists) - function Resolve-SkillsDir { - param( - [string]$ExtractedDir, - [string]$Requested - ) - - if ([string]::IsNullOrWhiteSpace($Requested)) { - $path = Find-LatestSkillsDir -ExtractedDir $ExtractedDir - if ($path) { - return [PSCustomObject]@{ Path = $path; Status = 'matched' } + } + + if ($parts.Length -ge 1 -and $parts[0] -ne '') { + $major = $parts[0] + if ($major -ne $Requested) { + $candidate = Join-Path $ExtractedDir "v$major/skills" + if (Test-Path $candidate -PathType Container) { + return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } } - return [PSCustomObject]@{ Path = $null; Status = 'none' } } + } - $exact = Join-Path $ExtractedDir "v$Requested/skills" - if (Test-Path $exact -PathType Container) { - return [PSCustomObject]@{ Path = $exact; Status = 'matched' } - } + $fallback = Find-LatestSkillsDir -ExtractedDir $ExtractedDir + if ($fallback) { + return [PSCustomObject]@{ Path = $fallback; Status = 'fallback' } + } + return [PSCustomObject]@{ Path = $null; Status = 'none' } +} - $parts = $Requested -split '[.-]' - if ($parts.Length -ge 2 -and $parts[0] -ne '' -and $parts[1] -ne '') { - $majorMinor = "$($parts[0]).$($parts[1])" - if ($majorMinor -ne $Requested) { - $candidate = Join-Path $ExtractedDir "v$majorMinor/skills" - if (Test-Path $candidate -PathType Container) { - return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } - } - } - } +function Initialize-Tarball { + if ($script:TarballReady) { return } - if ($parts.Length -ge 1 -and $parts[0] -ne '') { - $major = $parts[0] - if ($major -ne $Requested) { - $candidate = Join-Path $ExtractedDir "v$major/skills" - if (Test-Path $candidate -PathType Container) { - return [PSCustomObject]@{ Path = $candidate; Status = 'matched' } - } - } - } + if (-not (Get-Command Expand-Archive -ErrorAction SilentlyContinue)) { + Write-ErrAndExit 'Expand-Archive not found. PowerShell 5+ is required.' + } - $fallback = Find-LatestSkillsDir -ExtractedDir $ExtractedDir - if ($fallback) { - return [PSCustomObject]@{ Path = $fallback; Status = 'fallback' } - } - return [PSCustomObject]@{ Path = $null; Status = 'none' } + $script:Staging = Join-Path ([System.IO.Path]::GetTempPath()) ("jmix-install-" + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $script:Staging -Force | Out-Null + + $archiveUrl = "https://codeload.github.com/$($script:RepoOwner)/$($script:RepoName)/zip/$Ref" + $zipPath = Join-Path $script:Staging 'source.zip' + + Write-Info "Downloading $archiveUrl" + try { + Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile $zipPath + } catch { + $status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } + Write-ErrAndExit "failed to download $archiveUrl (HTTP $status)" + } + + Expand-Archive -Path $zipPath -DestinationPath $script:Staging -Force + + $script:ExtractedDir = (Get-ChildItem -Path $script:Staging -Directory | + Where-Object { $_.Name -like "$($script:RepoName)-*" } | + Select-Object -First 1).FullName + + if (-not $script:ExtractedDir) { + Write-ErrAndExit "extracted source directory not found in $($script:Staging)" } - $resolved = Resolve-SkillsDir -ExtractedDir $extractedDir.FullName -Requested $Version + $resolved = Resolve-SkillsDir -ExtractedDir $script:ExtractedDir -Requested $Version if ($resolved.Status -eq 'none' -or -not $resolved.Path) { - $available = (Get-ChildItem -Path $extractedDir.FullName -Directory | Select-Object -ExpandProperty Name) -join ' ' + $available = (Get-ChildItem -Path $script:ExtractedDir -Directory | Select-Object -ExpandProperty Name) -join ' ' Write-ErrAndExit "no v*/skills directory found in $Ref. Available top-level entries: $available" } - $sourceSkillsDir = $resolved.Path - $resolvedVersionDir = Split-Path -Leaf (Split-Path -Parent $sourceSkillsDir) + $script:SourceSkillsDir = $resolved.Path + $script:ResolvedVersionDir = Split-Path -Leaf (Split-Path -Parent $script:SourceSkillsDir) + $script:SourceAgentsMd = Join-Path (Split-Path -Parent $script:SourceSkillsDir) 'AGENTS.md' + if ($resolved.Status -eq 'fallback') { - Write-Info "Version '$Version' did not match any folder, falling back to latest available ($resolvedVersionDir)" + Write-Info "Version '$Version' did not match any folder, falling back to latest available ($($script:ResolvedVersionDir))" } - Write-Info "Using guidelines from $($sourceSkillsDir.Substring($extractedDir.FullName.Length + 1))" + Write-Info "Using guidelines from $($script:SourceSkillsDir.Substring($script:ExtractedDir.Length + 1))" - $script:sourceSkillsDir = $sourceSkillsDir - $script:timestamp = (Get-Date).ToString('yyyyMMdd-HHmmss') + $script:TarballReady = $true +} - function Install-Skills { - param( - [string]$TargetDir, - [string]$AgentLabel - ) +# ================================================================= +# skills install (global, per agent) +# ================================================================= + +function Get-SkillsTarget { + param([string]$Agent) + switch ($Agent) { + 'claude' { Join-Path $HOME '.claude/skills' } + 'codex' { Join-Path $HOME '.codex/skills' } + 'opencode' { Join-Path $HOME '.config/opencode/skills' } + 'junie' { Join-Path $HOME '.junie/skills' } + default { throw "unknown agent '$Agent'" } + } +} - Write-Info '' - Write-Info "Installing skills for $AgentLabel into $TargetDir" +function Install-SkillsForAgent { + param([string]$Agent) + $target = Get-SkillsTarget -Agent $Agent + $label = Get-AgentLabel -Agent $Agent - try { - if (-not (Test-Path $TargetDir)) { - New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null - } - } catch { - Write-ErrAndExit "cannot write to ${TargetDir}: $($_.Exception.Message)" + Write-Info '' + Write-Info "Installing skills for $label into $target" + if (-not (Test-Path $target)) { + New-Item -ItemType Directory -Path $target -Force | Out-Null + } + + $count = 0 + foreach ($skill in Get-ChildItem -Path $script:SourceSkillsDir -Directory) { + $dest = Join-Path $target $skill.Name + $backupName = "$($skill.Name).bak-$($script:Timestamp)" + if (Test-Path $dest) { + Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop + Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Updated: $($skill.Name) (backup: $backupName)" + } else { + Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Installed: $($skill.Name)" } + $count++ + } + Write-Info " $count skill(s) processed for $label" +} - $count = 0 - $skillDirs = Get-ChildItem -Path $script:sourceSkillsDir -Directory - foreach ($skill in $skillDirs) { - $name = $skill.Name - $dest = Join-Path $TargetDir $name - $backupName = "$name.bak-$script:timestamp" - - try { - if (Test-Path $dest) { - Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop - Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Updated: $name (backup: $backupName)" - } else { - Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Installed: $name" - } - } catch { - Write-ErrAndExit "cannot write to ${dest}: $($_.Exception.Message)" - } +function Invoke-CmdSkills { + $agents = @() + if ($Agent) { + $agents = @($Agent) + } elseif ($All -or (-not ($NoClaude -or $NoCodex -or $NoOpenCode -or $NoJunie))) { + $agents = $script:AllAgents + } else { + if (-not $NoClaude) { $agents += 'claude' } + if (-not $NoCodex) { $agents += 'codex' } + if (-not $NoOpenCode) { $agents += 'opencode' } + if (-not $NoJunie) { $agents += 'junie' } + } - $count++ - } + if ($agents.Count -eq 0) { + Write-ErrAndExit 'nothing to install (all -No* flags set)' + } - Write-Info " $count skill(s) processed for $AgentLabel" + Initialize-Tarball + foreach ($a in $agents) { + Install-SkillsForAgent -Agent $a } + Write-Info '' + Write-Info "Done. Installed skills for: $($agents -join ', ')" +} - $targets = @() - if (-not $NoClaude) { - Install-Skills -TargetDir (Join-Path $HOME '.claude/skills') -AgentLabel 'Claude' - $targets += 'Claude' +# ================================================================= +# agents-md install (project-level) +# ================================================================= + +function Get-AgentsMdDest { + param([string]$Agent) + $proj = (Get-Location).Path + switch ($Agent) { + 'claude' { Join-Path $proj 'CLAUDE.md' } + 'codex' { Join-Path $proj 'AGENTS.md' } + 'opencode' { Join-Path $proj 'AGENTS.md' } + 'junie' { Join-Path $proj '.junie/guidelines.md' } + default { throw "unknown agent '$Agent'" } } - if (-not $NoCodex) { - Install-Skills -TargetDir (Join-Path $HOME '.codex/skills') -AgentLabel 'Codex' - $targets += 'Codex' +} + +function Install-AgentsMdFor { + param([string]$Agent) + $dest = Get-AgentsMdDest -Agent $Agent + $label = Get-AgentLabel -Agent $Agent + + if (-not (Test-Path $script:SourceAgentsMd)) { + Write-ErrAndExit "AGENTS.md not found in $($script:ResolvedVersionDir)" } - if (-not $NoOpenCode) { - Install-Skills -TargetDir (Join-Path $HOME '.config/opencode/skills') -AgentLabel 'OpenCode' - $targets += 'OpenCode' + + $destDir = Split-Path -Parent $dest + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null } - if (-not $NoJunie) { - Install-Skills -TargetDir (Join-Path $HOME '.junie/skills') -AgentLabel 'Junie' - $targets += 'Junie' + + if (Test-Path $dest) { + $backup = "$dest.bak-$($script:Timestamp)" + Rename-Item -Path $dest -NewName (Split-Path -Leaf $backup) -ErrorAction Stop + Copy-Item -Path $script:SourceAgentsMd -Destination $dest -Force -ErrorAction Stop + Write-Info " Updated: $dest (backup: $(Split-Path -Leaf $backup))" + } else { + Copy-Item -Path $script:SourceAgentsMd -Destination $dest -Force -ErrorAction Stop + Write-Info " Installed: $dest" } + Write-Info " Project guidelines installed for $label" +} +function Invoke-CmdAgentsMd { + $agents = @() + if ($Agent) { + $agents = @($Agent) + } elseif ($All) { + $agents = $script:AllAgents + } else { + Write-ErrAndExit 'agents-md: specify -All or -Agent NAME' + } + + Write-Info "Project guidelines target directory: $((Get-Location).Path)" + Initialize-Tarball + foreach ($a in $agents) { + Install-AgentsMdFor -Agent $a + } +} + +# ================================================================= +# MCP install - JetBrains +# ================================================================= + +function Get-OpencodeConfigPath { + $dir = Join-Path $HOME '.config/opencode' + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + $file = Join-Path $dir 'opencode.json' + if (-not (Test-Path $file)) { '{}' | Out-File -FilePath $file -Encoding utf8 } + return $file +} + +function Set-OpencodeMcpEntry { + param( + [string]$Name, + [hashtable]$Entry + ) + $file = Get-OpencodeConfigPath + $json = Get-Content -Raw -Path $file | ConvertFrom-Json -ErrorAction Stop + if (-not $json.PSObject.Properties.Match('mcp')) { + $json | Add-Member -MemberType NoteProperty -Name 'mcp' -Value (New-Object PSObject) + } + if ($json.mcp.PSObject.Properties.Match($Name)) { + $json.mcp.PSObject.Properties.Remove($Name) + } + $json.mcp | Add-Member -MemberType NoteProperty -Name $Name -Value ([PSCustomObject]$Entry) + $json | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 + Write-Info "Updated $file with $Name MCP entry." +} + +function Install-JetbrainsForClaude { + Test-Tool -Tool 'claude' + Write-Info 'Adding JetBrains MCP for Claude Code...' + & claude mcp add --transport sse jetbrains --scope user http://localhost:64342/sse +} + +function Install-JetbrainsForCodex { + Test-Tool -Tool 'codex' + Write-Info 'Adding JetBrains MCP for Codex (Streamable HTTP; requires IntelliJ 2026.1+)...' + Write-Info 'For older IntelliJ versions, follow the STDIO setup in the README manually.' + & codex mcp add jetbrains --url http://localhost:64342/stream +} + +function Install-JetbrainsForOpencode { + Set-OpencodeMcpEntry -Name 'jetbrains' -Entry @{ + type = 'remote' + url = 'http://localhost:64342/sse' + enabled = $true + } +} + +function Install-JetbrainsForJunie { + Write-Info 'Junie runs inside IntelliJ and already has native IDE access. No JetBrains MCP needed.' +} + +function Install-JetbrainsFor { + param([string]$Agent) Write-Info '' - Write-Info "Done. Installed skills for: $($targets -join ', ')" + Write-Info "[JetBrains MCP] $(Get-AgentLabel -Agent $Agent)" + switch ($Agent) { + 'claude' { Install-JetbrainsForClaude } + 'codex' { Install-JetbrainsForCodex } + 'opencode' { Install-JetbrainsForOpencode } + 'junie' { Install-JetbrainsForJunie } + default { throw "unknown agent '$Agent'" } + } +} + +function Invoke-CmdMcpJetbrains { + $agents = @() + if ($Agent) { + $agents = @($Agent) + } elseif ($All) { + $agents = $script:JetbrainsAgents + } else { + Write-ErrAndExit 'mcp-jetbrains: specify -All or -Agent NAME' + } + foreach ($a in $agents) { + try { Install-JetbrainsFor -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } + } +} + +# ================================================================= +# MCP install - Context7 +# ================================================================= + +function Install-Context7ForClaude { + param([string]$Key) + Test-Tool -Tool 'claude' + Write-Info 'Adding Context7 MCP for Claude Code...' + & claude mcp add context7 --scope user -- npx -y '@upstash/context7-mcp' --api-key $Key +} + +function Install-Context7ForCodex { + param([string]$Key) + Test-Tool -Tool 'codex' + Write-Info 'Adding Context7 MCP for Codex...' + & codex mcp add context7 -- npx -y '@upstash/context7-mcp' --api-key $Key +} + +function Install-Context7ForOpencode { + param([string]$Key) + Set-OpencodeMcpEntry -Name 'context7' -Entry @{ + type = 'local' + command = @('npx', '-y', '@upstash/context7-mcp', '--api-key', $Key) + enabled = $true + } +} + +function Install-Context7ForJunie { + param([string]$Key) + Write-Info 'Junie does not support automated MCP setup.' + Write-Info 'Open IntelliJ Settings -> Tools -> Junie -> MCP Settings, click Add, then paste:' + Write-Output @" +{ + "mcpServers": { + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp", "--api-key", "$Key"] + } + } +} +"@ +} + +function Install-Context7For { + param( + [string]$Agent, + [string]$Key + ) + Write-Info '' + Write-Info "[Context7 MCP] $(Get-AgentLabel -Agent $Agent)" + switch ($Agent) { + 'claude' { Install-Context7ForClaude -Key $Key } + 'codex' { Install-Context7ForCodex -Key $Key } + 'opencode' { Install-Context7ForOpencode -Key $Key } + 'junie' { Install-Context7ForJunie -Key $Key } + default { throw "unknown agent '$Agent'" } + } +} + +function Invoke-CmdMcpContext7 { + $agents = @() + if ($Agent) { + $agents = @($Agent) + } elseif ($All) { + $agents = $script:Context7Agents + } else { + Write-ErrAndExit 'mcp-context7: specify -All or -Agent NAME' + } + + $apiKey = $Key + if (-not $apiKey) { + $apiKey = Read-Prompt -Message 'Context7 API key' -Default '' + if (-not $apiKey) { Write-ErrAndExit 'Context7 API key is required' } + } + + foreach ($a in $agents) { + try { Install-Context7For -Agent $a -Key $apiKey } catch { Write-Info "error: $($_.Exception.Message)" } + } +} + +# ================================================================= +# Wizard +# ================================================================= + +function Read-AgentChoice { + param( + [string]$Label, + [string[]]$Options + ) + Write-Info '' + Write-Info $Label + $i = 1 + foreach ($opt in $Options) { + Write-Output (" {0}) {1}" -f $i, (Get-AgentLabel -Agent $opt)) + $i++ + } + Write-Output (" {0}) For all agents" -f $i) + Write-Output ' s) Skip' + + $answer = Read-Prompt -Message 'Choice' -Default 's' + if ($answer -match '^(s|skip)$') { return @('skip') } + if ($answer -notmatch '^\d+$') { + Write-Info "Unrecognized choice '$answer'. Skipping." + return @('skip') + } + $num = [int]$answer + if ($num -eq $i) { return $Options } + if ($num -ge 1 -and $num -le $Options.Length) { return @($Options[$num - 1]) } + Write-Info "Unrecognized choice '$answer'. Skipping." + return @('skip') +} + +function Invoke-Wizard { + Write-Info '=== Jmix AI Agent Guidelines - Setup ===' + if ($Version) { Write-Info "Jmix version: $Version" } + Write-Info "Working directory: $((Get-Location).Path)" + + $summaryStrings = @{ + skills = 'skipped' + guidelines = 'skipped' + jetbrains = 'skipped' + context7 = 'skipped' + } + + # Step 1: skills + $sel = Read-AgentChoice -Label '[1/4] Install Jmix skills globally?' -Options $script:AllAgents + if ($sel[0] -ne 'skip') { + Initialize-Tarball + foreach ($a in $sel) { + try { Install-SkillsForAgent -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } + } + $summaryStrings.skills = $sel -join ', ' + } + + # Step 2: agents-md + $sel = Read-AgentChoice -Label '[2/4] Add Jmix coding guidelines to project root?' -Options $script:AllAgents + if ($sel[0] -ne 'skip') { + if (Read-YesNo -Message "Target directory: $((Get-Location).Path). Proceed?" -Default 'y') { + Initialize-Tarball + foreach ($a in $sel) { + try { Install-AgentsMdFor -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } + } + $summaryStrings.guidelines = $sel -join ', ' + } else { + $summaryStrings.guidelines = 'skipped (declined)' + } + } + + # Step 3: JetBrains MCP + $sel = Read-AgentChoice -Label '[3/4] Connect agent to IntelliJ IDEA via JetBrains MCP?' -Options $script:JetbrainsAgents + if ($sel[0] -ne 'skip') { + foreach ($a in $sel) { + try { Install-JetbrainsFor -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } + } + $summaryStrings.jetbrains = $sel -join ', ' + } + + # Step 4: Context7 MCP + $sel = Read-AgentChoice -Label '[4/4] Connect agent to library docs via Context7 MCP?' -Options $script:Context7Agents + if ($sel[0] -ne 'skip') { + $apiKey = Read-Prompt -Message 'Context7 API key' -Default '' + if ($apiKey) { + foreach ($a in $sel) { + try { Install-Context7For -Agent $a -Key $apiKey } catch { Write-Info "error: $($_.Exception.Message)" } + } + $summaryStrings.context7 = $sel -join ', ' + } else { + Write-Info 'API key not provided, skipping Context7 setup.' + $summaryStrings.context7 = 'skipped (no key)' + } + } + + Write-Info '' + Write-Info '=== Setup complete ===' + Write-Info " Skills: $($summaryStrings.skills)" + Write-Info " Guidelines: $($summaryStrings.guidelines)" + Write-Info " JetBrains: $($summaryStrings.jetbrains)" + Write-Info " Context7: $($summaryStrings.context7)" +} + +# ================================================================= +# Main dispatch +# ================================================================= + +try { + switch ($Subcommand) { + '' { Invoke-Wizard } + 'skills' { Invoke-CmdSkills } + 'agents-md' { Invoke-CmdAgentsMd } + 'mcp-jetbrains' { Invoke-CmdMcpJetbrains } + 'mcp-context7' { Invoke-CmdMcpContext7 } + default { Write-ErrAndExit "unknown subcommand: $Subcommand" } + } } finally { - Remove-Item -Recurse -Force -Path $staging -ErrorAction SilentlyContinue + if ($script:Staging -and (Test-Path $script:Staging)) { + Remove-Item -Recurse -Force -Path $script:Staging -ErrorAction SilentlyContinue + } } diff --git a/install.sh b/install.sh index db64011..3990374 100755 --- a/install.sh +++ b/install.sh @@ -1,42 +1,39 @@ #!/usr/bin/env bash -# Installs Jmix agent skills into the global skills directories used by: -# - Claude Code (~/.claude/skills) -# - Codex (~/.codex/skills) -# - OpenCode (~/.config/opencode/skills) -# - Junie (~/.junie/skills) +# Jmix AI Agent Guidelines installer. +# +# Default (no subcommand) launches an interactive wizard that guides through: +# 1. Installing Jmix skills globally for one or all agents. +# 2. Adding project-level guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md). +# 3. Registering the JetBrains MCP server with the agent. +# 4. Registering the Context7 MCP server with the agent. +# +# Subcommands are available for non-interactive use; see `install.sh --help`. set -euo pipefail REPO_OWNER="jmix-framework" REPO_NAME="jmix-agent-guidelines" + +# Global state populated by ensure_tarball() +STAGING="" +EXTRACTED_DIR="" +SOURCE_SKILLS_DIR="" +SOURCE_AGENTS_MD="" +RESOLVED_VERSION_DIR="" +TARBALL_READY=0 + VERSION="" REF="main" -INSTALL_CLAUDE=1 -INSTALL_CODEX=1 -INSTALL_OPENCODE=1 -INSTALL_JUNIE=1 -usage() { - cat <<'EOF' -Installs Jmix agent skills into global skills directories. +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" -Usage: - install.sh [--version V] [--ref REF] [--no-claude] [--no-codex] [--no-opencode] [--no-junie] - -Flags: - --version V Jmix version (e.g. 2, 2.8, 2.8.0). Optional. The script picks - the best-matching guideline folder: exact -> major.minor -> - major. If none of these match, or when the flag is omitted / - empty, the latest available version is used (sorted by - major, then minor, then patch). - --ref REF Git ref to download (default: main). - --no-claude Skip installing into ~/.claude/skills. - --no-codex Skip installing into ~/.codex/skills. - --no-opencode Skip installing into ~/.config/opencode/skills. - --no-junie Skip installing into ~/.junie/skills. - -h, --help Show this help. -EOF -} +ALL_AGENTS="claude codex opencode junie" +JETBRAINS_AGENTS="claude codex opencode junie" +CONTEXT7_AGENTS="claude codex opencode junie" + +# ================================================================= +# Helpers +# ================================================================= log() { printf '%s\n' "$*" @@ -55,67 +52,75 @@ require_tool() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." } -while [ $# -gt 0 ]; do - case "$1" in - --version) - [ $# -ge 2 ] || die "--version requires an argument" - VERSION="$2" - shift 2 - ;; - --ref) - [ $# -ge 2 ] || die "--ref requires an argument" - REF="$2" - shift 2 - ;; - --no-claude) - INSTALL_CLAUDE=0 - shift - ;; - --no-codex) - INSTALL_CODEX=0 - shift - ;; - --no-opencode) - INSTALL_OPENCODE=0 - shift - ;; - --no-junie) - INSTALL_JUNIE=0 - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - die "unknown argument: $1" - ;; - esac -done - -if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_JUNIE" -eq 0 ]; then - die "nothing to install (all --no-* flags set)" -fi - -require_tool curl -require_tool tar +# Reads a line from /dev/tty so prompts work under `curl ... | bash`. +# Falls back to the supplied default when no TTY is available. +prompt() { + local message="$1" + local default="${2:-}" + local hint="" + [ -n "$default" ] && hint=" [${default}]" + + # Subshell with stderr silenced so /dev/tty redirection errors stay quiet + # in headless environments. + local answer + answer="$( + exec 2>/dev/null + if printf '%s%s: ' "$message" "$hint" >/dev/tty; then + local ans="" + IFS= read -r ans major.minor -> major -> latest. + --ref REF Git ref to download (default: main). + --agent NAME Apply to one of: claude, codex, opencode, junie. + --all Apply to every supported agent for the subcommand. + -h, --help Show this help. + +skills options: + --no-claude --no-codex --no-opencode --no-junie (back-compat with --all) + +mcp-context7 options: + --key KEY Context7 API key. Prompted interactively when missing. +EOF +} -EXTRACTED_DIR="$(find "$STAGING" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n 1)" -[ -n "$EXTRACTED_DIR" ] || die "extracted source directory not found in ${STAGING}" +# ================================================================= +# Tarball + version resolution +# ================================================================= version_sort_key() { printf '%s' "$1" | awk -F'[.-]' '{ @@ -129,14 +134,15 @@ version_sort_key() { } find_latest_skills_dir() { - extracted="$1" - best_key="" - best_path="" + local extracted="$1" + local best_key="" + local best_path="" for dir in "$extracted"/v*/; do [ -d "${dir}skills" ] || continue - name="${dir%/}" + local name="${dir%/}" name="${name##*/v}" [ -n "$name" ] || continue + local key key="$(version_sort_key "$name")" if [ -z "$best_key" ] || [ "$key" \> "$best_key" ]; then best_key="$key" @@ -147,14 +153,14 @@ find_latest_skills_dir() { printf '%s\n' "$best_path" } -# Prints resolved skills dir on success. -# Exit codes: -# 0 - matched (exact, major.minor, major, or no-version default) -# 2 - fallback used (requested version didn't match any tier) -# 1 - no v*/skills dir present at all +# Resolves skills dir using tiered match (exact, major.minor, major) with +# latest-version fallback. Exit codes: +# 0 - matched (or no-version default) +# 2 - fallback used (requested didn't match any tier) +# 1 - no v*/skills dir found resolve_skills_dir() { - extracted="$1" - requested="$2" + local extracted="$1" + local requested="$2" if [ -z "$requested" ]; then find_latest_skills_dir "$extracted" @@ -166,52 +172,111 @@ resolve_skills_dir() { return 0 fi + local major_minor major_minor="$(printf '%s' "$requested" | awk -F'[.-]' '{ if (NF >= 2 && $1 != "" && $2 != "") print $1"."$2 }')" if [ -n "$major_minor" ] && [ "$major_minor" != "$requested" ] && [ -d "${extracted}/v${major_minor}/skills" ]; then printf '%s\n' "${extracted}/v${major_minor}/skills" return 0 fi + local major major="$(printf '%s' "$requested" | awk -F'[.-]' '{print $1}')" if [ -n "$major" ] && [ "$major" != "$requested" ] && [ -d "${extracted}/v${major}/skills" ]; then printf '%s\n' "${extracted}/v${major}/skills" return 0 fi + local fallback_path fallback_path="$(find_latest_skills_dir "$extracted")" || return 1 printf '%s\n' "$fallback_path" return 2 } -RESOLVE_STATUS=0 -SOURCE_SKILLS_DIR="$(resolve_skills_dir "$EXTRACTED_DIR" "$VERSION")" || RESOLVE_STATUS=$? -if [ "$RESOLVE_STATUS" -eq 1 ] || [ -z "$SOURCE_SKILLS_DIR" ]; then - AVAILABLE="$(find "$EXTRACTED_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ' ')" - die "no v*/skills directory found in ${REF}. Available top-level entries: ${AVAILABLE}" -fi +# Downloads and extracts the tarball, resolves the version folder, and populates +# SOURCE_SKILLS_DIR / SOURCE_AGENTS_MD / RESOLVED_VERSION_DIR. Idempotent. +ensure_tarball() { + [ "$TARBALL_READY" -eq 1 ] && return 0 -RESOLVED_VERSION_DIR="$(basename "$(dirname "$SOURCE_SKILLS_DIR")")" -if [ "$RESOLVE_STATUS" -eq 2 ]; then - log "Version '${VERSION}' did not match any folder, falling back to latest available (${RESOLVED_VERSION_DIR})" -fi -log "Using guidelines from ${SOURCE_SKILLS_DIR#${EXTRACTED_DIR}/}" + require_tool curl + require_tool tar -TIMESTAMP="$(date +%Y%m%d-%H%M%S)" + STAGING="$(mktemp -d 2>/dev/null || mktemp -d -t jmix-install)" + trap 'rm -rf "$STAGING"' EXIT + + local tarball_url="https://codeload.github.com/${REPO_OWNER}/${REPO_NAME}/tar.gz/${REF}" + local tarball_path="${STAGING}/source.tar.gz" + + log "Downloading ${tarball_url}" + local http_status + http_status="$(curl -sSL -w '%{http_code}' -o "$tarball_path" "$tarball_url" || echo "000")" + if [ "$http_status" != "200" ]; then + die "failed to download ${tarball_url} (HTTP ${http_status})" + fi + + tar -xzf "$tarball_path" -C "$STAGING" + EXTRACTED_DIR="$(find "$STAGING" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n 1)" + [ -n "$EXTRACTED_DIR" ] || die "extracted source directory not found in ${STAGING}" + + local resolve_status=0 + SOURCE_SKILLS_DIR="$(resolve_skills_dir "$EXTRACTED_DIR" "$VERSION")" || resolve_status=$? + if [ "$resolve_status" -eq 1 ] || [ -z "$SOURCE_SKILLS_DIR" ]; then + local available + available="$(find "$EXTRACTED_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ' ')" + die "no v*/skills directory found in ${REF}. Available top-level entries: ${available}" + fi + + RESOLVED_VERSION_DIR="$(basename "$(dirname "$SOURCE_SKILLS_DIR")")" + SOURCE_AGENTS_MD="$(dirname "$SOURCE_SKILLS_DIR")/AGENTS.md" + + if [ "$resolve_status" -eq 2 ]; then + log "Version '${VERSION}' did not match any folder, falling back to latest available (${RESOLVED_VERSION_DIR})" + fi + log "Using guidelines from ${SOURCE_SKILLS_DIR#${EXTRACTED_DIR}/}" + + TARBALL_READY=1 +} -install_to_target() { - target_dir="$1" - agent_label="$2" +# ================================================================= +# skills install (global, per agent) +# ================================================================= + +skills_target_for_agent() { + case "$1" in + claude) printf '%s' "${HOME}/.claude/skills" ;; + codex) printf '%s' "${HOME}/.codex/skills" ;; + opencode) printf '%s' "${HOME}/.config/opencode/skills" ;; + junie) printf '%s' "${HOME}/.junie/skills" ;; + *) die "unknown agent '$1'" ;; + esac +} + +agent_label() { + case "$1" in + claude) printf 'Claude Code' ;; + codex) printf 'Codex' ;; + opencode) printf 'OpenCode' ;; + junie) printf 'Junie' ;; + *) printf '%s' "$1" ;; + esac +} + +install_skills_for_agent() { + local agent="$1" + local target_dir + target_dir="$(skills_target_for_agent "$agent")" + local label + label="$(agent_label "$agent")" log "" - log "Installing skills for ${agent_label} into ${target_dir}" + log "Installing skills for ${label} into ${target_dir}" mkdir -p "$target_dir" || die "cannot write to ${target_dir}: mkdir failed" - count=0 + local count=0 + local skill name dest backup for skill in "$SOURCE_SKILLS_DIR"/*/; do [ -d "$skill" ] || continue name="$(basename "$skill")" dest="${target_dir}/${name}" - if [ -e "$dest" ]; then backup="${dest}.bak-${TIMESTAMP}" mv "$dest" "$backup" || die "cannot write to ${dest}: rename failed" @@ -223,36 +288,500 @@ install_to_target() { fi count=$((count + 1)) done + log " ${count} skill(s) processed for ${label}" +} + +cmd_skills() { + local agents="" + local pick_all=0 + local pick_agent="" + + # Back-compat flags + local nc=0 nx=0 no=0 nj=0 + + while [ $# -gt 0 ]; do + case "$1" in + --all) pick_all=1; shift ;; + --agent) + [ $# -ge 2 ] || die "--agent requires an argument" + pick_agent="$2"; shift 2 ;; + --no-claude) nc=1; shift ;; + --no-codex) nx=1; shift ;; + --no-opencode) no=1; shift ;; + --no-junie) nj=1; shift ;; + --version) + [ $# -ge 2 ] || die "--version requires an argument" + VERSION="$2"; shift 2 ;; + --ref) + [ $# -ge 2 ] || die "--ref requires an argument" + REF="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + done + + if [ -n "$pick_agent" ]; then + agents="$pick_agent" + elif [ "$pick_all" -eq 1 ] || [ "$nc$nx$no$nj" = "0000" ]; then + agents="$ALL_AGENTS" + else + [ "$nc" -eq 0 ] && agents="${agents} claude" + [ "$nx" -eq 0 ] && agents="${agents} codex" + [ "$no" -eq 0 ] && agents="${agents} opencode" + [ "$nj" -eq 0 ] && agents="${agents} junie" + fi + + agents="$(printf '%s' "$agents" | tr -s ' ' ' ' | sed 's/^ //;s/ $//')" + [ -n "$agents" ] || die "nothing to install (all --no-* flags set)" + + ensure_tarball - log " ${count} skill(s) processed for ${agent_label}" + local agent + for agent in $agents; do + install_skills_for_agent "$agent" + done + log "" + log "Done. Installed skills for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" } -TARGETS="" +# ================================================================= +# agents-md install (project-level) +# ================================================================= + +agents_md_dest_for_agent() { + local agent="$1" + local pwd_ + pwd_="$(pwd -P)" + case "$agent" in + claude) printf '%s/CLAUDE.md' "$pwd_" ;; + codex) printf '%s/AGENTS.md' "$pwd_" ;; + opencode) printf '%s/AGENTS.md' "$pwd_" ;; + junie) printf '%s/.junie/guidelines.md' "$pwd_" ;; + *) die "unknown agent '$1'" ;; + esac +} + +install_agents_md_for() { + local agent="$1" + local dest + dest="$(agents_md_dest_for_agent "$agent")" + local label + label="$(agent_label "$agent")" + + [ -f "$SOURCE_AGENTS_MD" ] || die "AGENTS.md not found in ${RESOLVED_VERSION_DIR}" + + local dest_dir + dest_dir="$(dirname "$dest")" + mkdir -p "$dest_dir" || die "cannot create directory ${dest_dir}" -append_target() { - if [ -n "$TARGETS" ]; then - TARGETS="${TARGETS}, $1" + if [ -e "$dest" ]; then + local backup="${dest}.bak-${TIMESTAMP}" + mv "$dest" "$backup" || die "cannot rename existing ${dest}" + cp "$SOURCE_AGENTS_MD" "$dest" || die "cannot write ${dest}" + log " Updated: ${dest} (backup: $(basename "$backup"))" else - TARGETS="$1" + cp "$SOURCE_AGENTS_MD" "$dest" || die "cannot write ${dest}" + log " Installed: ${dest}" fi + log " Project guidelines installed for ${label}" } -if [ "$INSTALL_CLAUDE" -eq 1 ]; then - install_to_target "${HOME}/.claude/skills" "Claude" - append_target "Claude" -fi -if [ "$INSTALL_CODEX" -eq 1 ]; then - install_to_target "${HOME}/.codex/skills" "Codex" - append_target "Codex" -fi -if [ "$INSTALL_OPENCODE" -eq 1 ]; then - install_to_target "${HOME}/.config/opencode/skills" "OpenCode" - append_target "OpenCode" -fi -if [ "$INSTALL_JUNIE" -eq 1 ]; then - install_to_target "${HOME}/.junie/skills" "Junie" - append_target "Junie" +cmd_agents_md() { + local pick_all=0 + local pick_agent="" + + while [ $# -gt 0 ]; do + case "$1" in + --all) pick_all=1; shift ;; + --agent) + [ $# -ge 2 ] || die "--agent requires an argument" + pick_agent="$2"; shift 2 ;; + --version) + [ $# -ge 2 ] || die "--version requires an argument" + VERSION="$2"; shift 2 ;; + --ref) + [ $# -ge 2 ] || die "--ref requires an argument" + REF="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + done + + local agents="" + if [ -n "$pick_agent" ]; then + agents="$pick_agent" + elif [ "$pick_all" -eq 1 ]; then + agents="$ALL_AGENTS" + else + die "agents-md: specify --all or --agent NAME" + fi + + log "Project guidelines target directory: $(pwd -P)" + ensure_tarball + + local agent + for agent in $agents; do + install_agents_md_for "$agent" + done +} + +# ================================================================= +# MCP install - JetBrains +# ================================================================= + +mcp_jetbrains_for_claude() { + require_tool claude + log "Adding JetBrains MCP for Claude Code..." + claude mcp add --transport sse jetbrains --scope user http://localhost:64342/sse +} + +mcp_jetbrains_for_codex() { + require_tool codex + log "Adding JetBrains MCP for Codex (Streamable HTTP; requires IntelliJ 2026.1+)..." + log "For older IntelliJ versions, follow the STDIO setup in the README manually." + codex mcp add jetbrains --url http://localhost:64342/stream +} + +mcp_jetbrains_for_opencode() { + local config_dir="${HOME}/.config/opencode" + local config_file="${config_dir}/opencode.json" + mkdir -p "$config_dir" + [ -f "$config_file" ] || echo '{}' > "$config_file" + + if ! command -v jq >/dev/null 2>&1; then + log "OpenCode requires jq to edit ${config_file}. Add this block manually:" + cat <<'EOF' + "mcp": { + "jetbrains": { + "type": "remote", + "url": "http://localhost:64342/sse", + "enabled": true + } + } +EOF + return 1 + fi + + local tmp + tmp="$(mktemp)" + jq '.mcp = (.mcp // {}) | .mcp.jetbrains = {"type":"remote","url":"http://localhost:64342/sse","enabled":true}' "$config_file" > "$tmp" + mv "$tmp" "$config_file" + log "Updated ${config_file} with JetBrains MCP entry." +} + +mcp_jetbrains_for_junie() { + log "Junie runs inside IntelliJ and already has native IDE access. No JetBrains MCP needed." +} + +install_jetbrains_for() { + local agent="$1" + log "" + log "[JetBrains MCP] $(agent_label "$agent")" + case "$agent" in + claude) mcp_jetbrains_for_claude ;; + codex) mcp_jetbrains_for_codex ;; + opencode) mcp_jetbrains_for_opencode ;; + junie) mcp_jetbrains_for_junie ;; + *) die "unknown agent '$1'" ;; + esac +} + +cmd_mcp_jetbrains() { + local pick_all=0 + local pick_agent="" + + while [ $# -gt 0 ]; do + case "$1" in + --all) pick_all=1; shift ;; + --agent) + [ $# -ge 2 ] || die "--agent requires an argument" + pick_agent="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + done + + local agents="" + if [ -n "$pick_agent" ]; then + agents="$pick_agent" + elif [ "$pick_all" -eq 1 ]; then + agents="$JETBRAINS_AGENTS" + else + die "mcp-jetbrains: specify --all or --agent NAME" + fi + + local agent rc=0 + for agent in $agents; do + install_jetbrains_for "$agent" || rc=1 + done + return $rc +} + +# ================================================================= +# MCP install - Context7 +# ================================================================= + +mcp_context7_for_claude() { + local key="$1" + require_tool claude + log "Adding Context7 MCP for Claude Code..." + claude mcp add context7 --scope user -- npx -y @upstash/context7-mcp --api-key "$key" +} + +mcp_context7_for_codex() { + local key="$1" + require_tool codex + log "Adding Context7 MCP for Codex..." + codex mcp add context7 -- npx -y @upstash/context7-mcp --api-key "$key" +} + +mcp_context7_for_opencode() { + local key="$1" + local config_dir="${HOME}/.config/opencode" + local config_file="${config_dir}/opencode.json" + mkdir -p "$config_dir" + [ -f "$config_file" ] || echo '{}' > "$config_file" + + if ! command -v jq >/dev/null 2>&1; then + log "OpenCode requires jq to edit ${config_file}. Add this block manually:" + cat < "$tmp" + mv "$tmp" "$config_file" + log "Updated ${config_file} with Context7 MCP entry." +} + +mcp_context7_for_junie() { + local key="$1" + log "Junie does not support automated MCP setup." + log "Open IntelliJ Settings -> Tools -> Junie -> MCP Settings, click Add, then paste:" + cat <&2 + + local total=$((i)) + local answer + answer="$(prompt 'Choice' 's')" + case "$answer" in + s|S|skip|SKIP) printf 'skip'; return 0 ;; + esac + if ! printf '%s' "$answer" | grep -Eq '^[0-9]+$'; then + log "Unrecognized choice '${answer}'. Skipping." >&2 + printf 'skip' + return 0 + fi + if [ "$answer" -eq "$total" ]; then + printf '%s' "$options" + return 0 + fi + local idx=1 + for opt in $options; do + if [ "$idx" -eq "$answer" ]; then + printf '%s' "$opt" + return 0 + fi + idx=$((idx + 1)) + done + log "Unrecognized choice '${answer}'. Skipping." >&2 + printf 'skip' +} + +cmd_wizard() { + while [ $# -gt 0 ]; do + case "$1" in + --version) + [ $# -ge 2 ] || die "--version requires an argument" + VERSION="$2"; shift 2 ;; + --ref) + [ $# -ge 2 ] || die "--ref requires an argument" + REF="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + done + + log "=== Jmix AI Agent Guidelines - Setup ===" + [ -n "$VERSION" ] && log "Jmix version: ${VERSION}" + log "Working directory: $(pwd -P)" + + local summary_skills="skipped" + local summary_guidelines="skipped" + local summary_jetbrains="skipped" + local summary_context7="skipped" + + # Step 1: skills + local sel + sel="$(wizard_pick_agent '[1/4] Install Jmix skills globally?' $ALL_AGENTS)" + if [ "$sel" != "skip" ]; then + ensure_tarball + local agent + for agent in $sel; do + install_skills_for_agent "$agent" || true + done + summary_skills="$sel" + fi + + # Step 2: agents-md + sel="$(wizard_pick_agent '[2/4] Add Jmix coding guidelines to project root?' $ALL_AGENTS)" + if [ "$sel" != "skip" ]; then + if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then + ensure_tarball + local agent + for agent in $sel; do + install_agents_md_for "$agent" || true + done + summary_guidelines="$sel" + else + summary_guidelines="skipped (declined)" + fi + fi + + # Step 3: JetBrains MCP + sel="$(wizard_pick_agent '[3/4] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" + if [ "$sel" != "skip" ]; then + local agent + for agent in $sel; do + install_jetbrains_for "$agent" || true + done + summary_jetbrains="$sel" + fi + + # Step 4: Context7 MCP + sel="$(wizard_pick_agent '[4/4] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" + if [ "$sel" != "skip" ]; then + local key + key="$(prompt 'Context7 API key' '')" + if [ -n "$key" ]; then + local agent + for agent in $sel; do + install_context7_for "$agent" "$key" || true + done + summary_context7="$sel" + else + log "API key not provided, skipping Context7 setup." + summary_context7="skipped (no key)" + fi + fi + + log "" + log "=== Setup complete ===" + log " Skills: ${summary_skills}" + log " Guidelines: ${summary_guidelines}" + log " JetBrains: ${summary_jetbrains}" + log " Context7: ${summary_context7}" +} + +# ================================================================= +# Main dispatch +# ================================================================= + +if [ $# -eq 0 ]; then + cmd_wizard + exit $? fi -log "" -log "Done. Installed skills for: ${TARGETS}" +case "$1" in + skills) shift; cmd_skills "$@" ;; + agents-md) shift; cmd_agents_md "$@" ;; + mcp-jetbrains) shift; cmd_mcp_jetbrains "$@" ;; + mcp-context7) shift; cmd_mcp_context7 "$@" ;; + -h|--help) usage ;; + --*) cmd_wizard "$@" ;; + *) die "unknown subcommand: $1" ;; +esac From 43bf7bee9fa643783121f1b804794d60b17902d4 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 18 May 2026 18:04:43 +0400 Subject: [PATCH 06/36] Update studio-meta-data.json steps --- .studio/studio-meta-data.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 47fffa3..35079aa 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -5,16 +5,12 @@ "message": "Configure AI agents for Jmix", "options": [ { - "label": "Start setup", + "label": "Configure via Jmix CLI script", "command": { "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -Version \"${JMIX_VERSION}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- --version \"${JMIX_VERSION}\"" + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash --version \"${JMIX_VERSION}\"" } - }, - { - "label": "Skip", - "skip": true } ] } From 2ab58d9014e82586c82eba75229ba4e27598e609 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 18 May 2026 19:27:32 +0400 Subject: [PATCH 07/36] Update studio-meta-data.json steps --- .studio/studio-meta-data.json | 97 +++++++++++++-- .studio/studio-meta-data.schema.json | 180 +++++++++++++++++++++++++++ install.ps1 | 49 +++++++- install.sh | 55 +++++++- 4 files changed, 365 insertions(+), 16 deletions(-) create mode 100644 .studio/studio-meta-data.schema.json diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 35079aa..42910dd 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -1,18 +1,93 @@ { + "$schema": "./studio-meta-data.schema.json", "install-flow": { "steps": [ { - "message": "Configure AI agents for Jmix", - "options": [ - { - "label": "Configure via Jmix CLI script", - "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) -Version \"${JMIX_VERSION}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash --version \"${JMIX_VERSION}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash --version \"${JMIX_VERSION}\"" - } - } - ] + "id": "skillsAgents", + "message": "Pick agents to install Jmix skills for.", + "input": { + "type": "options", + "multi": true, + "choices": [ + { "value": "claude", "label": "Claude Code", "default": true }, + { "value": "codex", "label": "Codex", "default": true }, + { "value": "opencode", "label": "OpenCode", "default": true }, + { "value": "junie", "label": "Junie", "default": true } + ] + }, + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) skills -Agents \"${skillsAgents}\" -Version \"${JMIX_VERSION}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"" + } + }, + { + "id": "installGuidelines", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md) to the project root?", + "input": { + "type": "checkbox", + "label": "Install project guidelines", + "default": true + } + }, + { + "id": "guidelinesAgents", + "runIf": "installGuidelines", + "message": "Pick agents for the project guideline files.", + "input": { + "type": "options", + "multi": true, + "choices": [ + { "value": "claude", "label": "Claude Code", "default": true }, + { "value": "codex", "label": "Codex", "default": true }, + { "value": "opencode", "label": "OpenCode", "default": true }, + { "value": "junie", "label": "Junie", "default": true } + ] + }, + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) agents-md -Agents \"${guidelinesAgents}\" -Version \"${JMIX_VERSION}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"" + } + }, + { + "id": "installJetbrainsMcp", + "message": "Register the JetBrains MCP server with installed agents? Recommended.", + "input": { + "type": "checkbox", + "label": "Register JetBrains MCP", + "default": true + }, + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -All", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --all", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --all" + } + }, + { + "id": "installContext7", + "message": "Register the Context7 MCP server (provides up-to-date library documentation)?", + "input": { + "type": "checkbox", + "label": "Register Context7 MCP", + "default": false + } + }, + { + "id": "context7Key", + "runIf": "installContext7", + "message": "Enter your Context7 API key. The key is sent to the MCP CLI and never stored by Studio.", + "input": { + "type": "userInput", + "regex": "^.{8,}$", + "errorMessage": "Context7 API key must be at least 8 characters.", + "placeholder": "ctx7_..." + }, + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-context7 -All -Key \"${context7Key}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --all --key \"${context7Key}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --all --key \"${context7Key}\"" + } } ] } diff --git a/.studio/studio-meta-data.schema.json b/.studio/studio-meta-data.schema.json new file mode 100644 index 0000000..cf141b2 --- /dev/null +++ b/.studio/studio-meta-data.schema.json @@ -0,0 +1,180 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/.studio/studio-meta-data.schema.json", + "title": "Jmix Studio install-flow metadata", + "description": "Metadata consumed by Jmix Studio to drive the 'Configure AI Agents' wizard. Defines a sequence of typed-input steps; each step optionally executes a shell command assembled from collected values.", + "type": "object", + "additionalProperties": true, + "required": ["install-flow"], + "properties": { + "install-flow": { + "type": "object", + "description": "Top-level container for the wizard flow.", + "required": ["steps"], + "additionalProperties": false, + "properties": { + "steps": { + "type": "array", + "description": "Steps shown to the user in order. Steps with a 'runIf' that points to a falsy earlier step are skipped entirely.", + "items": { "$ref": "#/$defs/step" } + } + } + } + }, + "$defs": { + "step": { + "type": "object", + "description": "A single step of the install flow. Steps may have an input (to collect a value), a command (to execute), or both.", + "additionalProperties": false, + "required": ["message"], + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Unique step identifier. Used to substitute the collected value into other steps' commands via ${id}, and as the target of 'runIf'. Required if the step has 'input' or is referenced elsewhere." + }, + "message": { + "type": "string", + "description": "Header text shown above the input widget." + }, + "runIf": { + "type": "string", + "description": "If set, the step is rendered only when the value of the referenced earlier step is truthy. Falsy values: checkbox=false, options-multi=empty, userInput=blank." + }, + "input": { + "description": "Optional input widget shown to the user. Discriminated by 'type'.", + "oneOf": [ + { "$ref": "#/$defs/inputUserInput" }, + { "$ref": "#/$defs/inputOptions" }, + { "$ref": "#/$defs/inputCheckbox" } + ] + }, + "command": { + "$ref": "#/$defs/commandByOs", + "description": "Optional per-OS shell command run after the user confirms the step. Supports ${id} placeholders that reference values from earlier steps and the implicit ${JMIX_VERSION} placeholder. The command is skipped when the step's own input value is falsy." + } + }, + "allOf": [ + { + "if": { "required": ["input"] }, + "then": { "required": ["id"] } + } + ] + }, + + "inputUserInput": { + "type": "object", + "description": "Free-text input. Optionally validated against a regular expression.", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { "const": "userInput" }, + "regex": { + "type": "string", + "description": "Java regular expression. The full value must match (Regex.matches semantics)." + }, + "errorMessage": { + "type": "string", + "description": "Inline error shown when the regex does not match. Falls back to a generic message." + }, + "placeholder": { + "type": "string", + "description": "Hint shown inside the text field when it is empty." + }, + "default": { + "type": "string", + "description": "Initial value pre-filled in the field." + } + } + }, + + "inputOptions": { + "type": "object", + "description": "Choice between predefined values. Single-select renders as a combo box; multi-select renders as a check-box list.", + "additionalProperties": false, + "required": ["type", "choices"], + "properties": { + "type": { "const": "options" }, + "multi": { + "type": "boolean", + "default": false, + "description": "When true, multiple choices may be selected; the resolved value is a comma-joined list of chosen 'value' fields." + }, + "choices": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/optionsChoice" } + } + } + }, + + "optionsChoice": { + "type": "object", + "additionalProperties": false, + "required": ["value", "label"], + "properties": { + "value": { + "type": "string", + "description": "Value substituted into commands when this choice is selected." + }, + "label": { + "type": "string", + "description": "Human-readable label shown in the UI." + }, + "default": { + "type": "boolean", + "default": false, + "description": "When true, this choice is preselected. For single-select inputs the last 'default: true' wins; for multi-select all matching choices are preselected." + } + } + }, + + "inputCheckbox": { + "type": "object", + "description": "Single boolean toggle.", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { "const": "checkbox" }, + "label": { + "type": "string", + "description": "Label shown next to the checkbox." + }, + "default": { + "type": "boolean", + "default": false, + "description": "Initial checked state." + }, + "valueIfTrue": { + "type": "string", + "description": "Substitution string when the checkbox is checked. Defaults to 'true'." + }, + "valueIfFalse": { + "type": "string", + "description": "Substitution string when the checkbox is unchecked. Defaults to 'false'." + } + } + }, + + "commandByOs": { + "type": "object", + "description": "Per-OS shell command. The host OS is detected via SystemInfo and the corresponding entry is executed.", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "windows": { + "type": "string", + "description": "Command executed via 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command'." + }, + "macos": { + "type": "string", + "description": "Command executed via '/bin/bash -c'." + }, + "linux": { + "type": "string", + "description": "Command executed via '/bin/bash -c'." + } + } + } + } +} diff --git a/install.ps1 b/install.ps1 index 7f1685f..f660049 100644 --- a/install.ps1 +++ b/install.ps1 @@ -29,6 +29,10 @@ .PARAMETER Agent Single agent target: claude, codex, opencode, or junie. +.PARAMETER Agents + Comma-separated list of agents (e.g. "claude,codex"). Mutually exclusive + with -Agent and -All. + .PARAMETER All Apply the subcommand to every supported agent. @@ -60,6 +64,7 @@ param( [string]$Version = '', [string]$Ref = 'main', [string]$Agent = '', + [string]$Agents = '', [switch]$All, [string]$Key = '', [switch]$NoClaude, @@ -142,6 +147,20 @@ function Get-AgentLabel { } } +function Resolve-AgentsCsv { + param([string]$Csv) + $known = @('claude', 'codex', 'opencode', 'junie') + $tokens = $Csv -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + $resolved = @() + foreach ($t in $tokens) { + if ($known -notcontains $t) { + Write-ErrAndExit "unknown agent in -Agents: '$t'" + } + $resolved += $t + } + return $resolved +} + # ================================================================= # Tarball + version resolution # ================================================================= @@ -318,9 +337,22 @@ function Install-SkillsForAgent { } function Invoke-CmdSkills { + $exclusive = 0 + if ($All) { $exclusive++ } + if ($Agent) { $exclusive++ } + if ($Agents) { $exclusive++ } + if ($exclusive -gt 1) { + Write-ErrAndExit '-All, -Agent and -Agents are mutually exclusive' + } + $agents = @() if ($Agent) { $agents = @($Agent) + } elseif ($Agents) { + $agents = Resolve-AgentsCsv -Csv $Agents + if ($agents.Count -eq 0) { + Write-ErrAndExit 'nothing to install (-Agents resolved to empty list)' + } } elseif ($All -or (-not ($NoClaude -or $NoCodex -or $NoOpenCode -or $NoJunie))) { $agents = $script:AllAgents } else { @@ -385,13 +417,26 @@ function Install-AgentsMdFor { } function Invoke-CmdAgentsMd { + $exclusive = 0 + if ($All) { $exclusive++ } + if ($Agent) { $exclusive++ } + if ($Agents) { $exclusive++ } + if ($exclusive -gt 1) { + Write-ErrAndExit '-All, -Agent and -Agents are mutually exclusive' + } + $agents = @() if ($Agent) { $agents = @($Agent) + } elseif ($Agents) { + $agents = Resolve-AgentsCsv -Csv $Agents + if ($agents.Count -eq 0) { + Write-ErrAndExit 'agents-md: -Agents resolved to empty list' + } } elseif ($All) { $agents = $script:AllAgents } else { - Write-ErrAndExit 'agents-md: specify -All or -Agent NAME' + Write-ErrAndExit 'agents-md: specify -All, -Agent NAME, or -Agents CSV' } Write-Info "Project guidelines target directory: $((Get-Location).Path)" @@ -618,7 +663,7 @@ function Invoke-Wizard { } # Step 2: agents-md - $sel = Read-AgentChoice -Label '[2/4] Add Jmix coding guidelines to project root?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[2/4] Add Jmix coding guidelines to this directory?' -Options $script:AllAgents if ($sel[0] -ne 'skip') { if (Read-YesNo -Message "Target directory: $((Get-Location).Path). Proceed?" -Default 'y') { Initialize-Tarball diff --git a/install.sh b/install.sh index 3990374..32ccb32 100755 --- a/install.sh +++ b/install.sh @@ -111,6 +111,8 @@ Common options: -h, --help Show this help. skills options: + --agents CSV Comma-separated agent list (e.g. claude,codex). + Mutually exclusive with --agent and --all. --no-claude --no-codex --no-opencode --no-junie (back-compat with --all) mcp-context7 options: @@ -295,6 +297,7 @@ cmd_skills() { local agents="" local pick_all=0 local pick_agent="" + local pick_agents_csv="" # Back-compat flags local nc=0 nx=0 no=0 nj=0 @@ -305,6 +308,9 @@ cmd_skills() { --agent) [ $# -ge 2 ] || die "--agent requires an argument" pick_agent="$2"; shift 2 ;; + --agents) + [ $# -ge 2 ] || die "--agents requires an argument" + pick_agents_csv="$2"; shift 2 ;; --no-claude) nc=1; shift ;; --no-codex) nx=1; shift ;; --no-opencode) no=1; shift ;; @@ -320,8 +326,27 @@ cmd_skills() { esac done + local exclusive_count=0 + [ "$pick_all" -eq 1 ] && exclusive_count=$((exclusive_count + 1)) + [ -n "$pick_agent" ] && exclusive_count=$((exclusive_count + 1)) + [ -n "$pick_agents_csv" ] && exclusive_count=$((exclusive_count + 1)) + if [ "$exclusive_count" -gt 1 ]; then + die "--all, --agent and --agents are mutually exclusive" + fi + if [ -n "$pick_agent" ]; then agents="$pick_agent" + elif [ -n "$pick_agents_csv" ]; then + # Parse CSV -> space-separated list, validate each token. + local raw token + raw="$(printf '%s' "$pick_agents_csv" | tr ',' ' ' | tr -s ' ' ' ')" + for token in $raw; do + case "$token" in + claude|codex|opencode|junie) agents="${agents} ${token}" ;; + "") ;; + *) die "unknown agent in --agents: '$token'" ;; + esac + done elif [ "$pick_all" -eq 1 ] || [ "$nc$nx$no$nj" = "0000" ]; then agents="$ALL_AGENTS" else @@ -332,7 +357,7 @@ cmd_skills() { fi agents="$(printf '%s' "$agents" | tr -s ' ' ' ' | sed 's/^ //;s/ $//')" - [ -n "$agents" ] || die "nothing to install (all --no-* flags set)" + [ -n "$agents" ] || die "nothing to install (no agents resolved)" ensure_tarball @@ -389,6 +414,7 @@ install_agents_md_for() { cmd_agents_md() { local pick_all=0 local pick_agent="" + local pick_agents_csv="" while [ $# -gt 0 ]; do case "$1" in @@ -396,6 +422,9 @@ cmd_agents_md() { --agent) [ $# -ge 2 ] || die "--agent requires an argument" pick_agent="$2"; shift 2 ;; + --agents) + [ $# -ge 2 ] || die "--agents requires an argument" + pick_agents_csv="$2"; shift 2 ;; --version) [ $# -ge 2 ] || die "--version requires an argument" VERSION="$2"; shift 2 ;; @@ -407,13 +436,33 @@ cmd_agents_md() { esac done + local exclusive_count=0 + [ "$pick_all" -eq 1 ] && exclusive_count=$((exclusive_count + 1)) + [ -n "$pick_agent" ] && exclusive_count=$((exclusive_count + 1)) + [ -n "$pick_agents_csv" ] && exclusive_count=$((exclusive_count + 1)) + if [ "$exclusive_count" -gt 1 ]; then + die "--all, --agent and --agents are mutually exclusive" + fi + local agents="" if [ -n "$pick_agent" ]; then agents="$pick_agent" + elif [ -n "$pick_agents_csv" ]; then + local raw token + raw="$(printf '%s' "$pick_agents_csv" | tr ',' ' ' | tr -s ' ' ' ')" + for token in $raw; do + case "$token" in + claude|codex|opencode|junie) agents="${agents} ${token}" ;; + "") ;; + *) die "unknown agent in --agents: '$token'" ;; + esac + done + agents="$(printf '%s' "$agents" | sed 's/^ //;s/ $//')" + [ -n "$agents" ] || die "agents-md: --agents resolved to empty list" elif [ "$pick_all" -eq 1 ]; then agents="$ALL_AGENTS" else - die "agents-md: specify --all or --agent NAME" + die "agents-md: specify --all, --agent NAME, or --agents CSV" fi log "Project guidelines target directory: $(pwd -P)" @@ -718,7 +767,7 @@ cmd_wizard() { fi # Step 2: agents-md - sel="$(wizard_pick_agent '[2/4] Add Jmix coding guidelines to project root?' $ALL_AGENTS)" + sel="$(wizard_pick_agent '[2/4] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then ensure_tarball From 0ce5101beed99fb17e6607e44c0b4b786c2b138c Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 18 May 2026 20:45:58 +0400 Subject: [PATCH 08/36] Update studio-meta-data.json steps --- .studio/studio-meta-data.json | 26 +++- README.md | 75 +++++---- install.ps1 | 220 ++++++++++++++------------- install.sh | 277 ++++++++++++++++++---------------- 4 files changed, 328 insertions(+), 270 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 42910dd..7b2a515 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -59,9 +59,9 @@ "default": true }, "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -All", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --all", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --all" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -Agents \"${skillsAgents}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"" } }, { @@ -84,9 +84,23 @@ "placeholder": "ctx7_..." }, "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-context7 -All -Key \"${context7Key}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --all --key \"${context7Key}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --all --key \"${context7Key}\"" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-context7 -Agents \"${skillsAgents}\" -Context7Key \"${context7Key}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${skillsAgents}\" --context7-key \"${context7Key}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${skillsAgents}\" --context7-key \"${context7Key}\"" + } + }, + { + "id": "installPlaywright", + "message": "Install Playwright testing skills? Requires npm to be installed on PATH.", + "input": { + "type": "checkbox", + "label": "Install Playwright skills", + "default": false + }, + "command": { + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) playwright -Agents \"${skillsAgents}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${skillsAgents}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${skillsAgents}\"" } } ] diff --git a/README.md b/README.md index b64bc7d..5a1bdc8 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,55 @@ The AI agent will use these resources to understand Jmix-specific patterns, mand - `SKILL.md`: Detailed instructions and rules for the agent regarding a specific Jmix feature. - Optional subdirectories with examples or other materials. -## How to Use +## Automatic Installation -To enable these guidelines for your AI agent, follow the steps below. +A single command launches an interactive wizard that walks through every setup step: installing global skills for one or more agents, adding the project guideline file (`CLAUDE.md` / `AGENTS.md` / `.junie/guidelines.md`) to the project root, and registering the JetBrains and Context7 MCP servers. -Take the files from the `v2/` directory if you are using Jmix 2.x. +**macOS / Linux:** + +```bash +curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash +``` + +**Windows (PowerShell 5+):** + +```powershell +iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex +``` + +In Jmix Studio plugin, the same wizard is available from the **Configure AI Agents for Jmix** action. + +### Non-Interactive Subcommands + +Use these to run a single step without the wizard. Every subcommand takes the +same `--agents CSV` flag: + +```bash +install.sh skills --agents CSV [--version V] +install.sh agents-md --agents CSV [--version V] +install.sh mcp-jetbrains --agents CSV +install.sh mcp-context7 --agents CSV [--context7-key KEY] +install.sh playwright --agents CSV # requires npm on PATH +``` + +PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `install.ps1 mcp-context7 -Agents claude -Context7Key KEY`, `install.ps1 playwright -Agents claude,codex`, etc. + +**CSV** = comma-separated agent list (e.g. `claude,codex`) or a single value (e.g. `claude`). Valid agents: `claude`, `codex`, `opencode`, `junie`. + +### Flags + +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:-------------------|:------------------|:--------|:--------------------------------------------------------| +| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | +| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | + +> The automatic installer covers global skills, project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. + +## Manual Installation + +If you prefer not to run the script, follow these steps. Take the files from the `v2/` directory if you are using Jmix 2.x. ### 1. Project Guidelines @@ -40,31 +84,6 @@ Copy or symlink the content of the `skills/` subdirectory to the folder recogniz | OpenCode | `~/.config/opencode/skills/` | | Junie | `~/.junie/skills/` | -### Quick Install - -**macOS / Linux:** - -```bash -curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -``` - -**Windows (PowerShell 5+):** - -```powershell -iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex -``` - -**Flags:** - -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:------------------|:------------------|:--------|:-------------------------------------| -| `--version x.y.z` | `-Version x.y.z` | `` | Jmix version. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) to download. | -| `--no-claude` | `-NoClaude` | off | Skip `~/.claude/skills/`. | -| `--no-codex` | `-NoCodex` | off | Skip `~/.codex/skills/`. | - -> The quick install covers global skills only. For the project `AGENTS.md` / `CLAUDE.md` file, MCP servers, and Playwright setup, follow the manual steps in the [How to Use](#how-to-use) section below. - #### Example Using symlink for Claude Code: diff --git a/install.ps1 b/install.ps1 index f660049..62af4bc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,10 +11,11 @@ 4. Registering the Context7 MCP server with the agent. Subcommands are available for non-interactive use: - install.ps1 skills [-All | -Agent NAME] [-Version V] [-Ref REF] - install.ps1 agents-md [-All | -Agent NAME] [-Version V] [-Ref REF] - install.ps1 mcp-jetbrains [-All | -Agent NAME] - install.ps1 mcp-context7 [-All | -Agent NAME] [-Key KEY] + install.ps1 skills -Agents CSV [-Version V] [-Ref REF] + install.ps1 agents-md -Agents CSV [-Version V] [-Ref REF] + install.ps1 mcp-jetbrains -Agents CSV + install.ps1 mcp-context7 -Agents CSV [-Context7Key KEY] + install.ps1 playwright -Agents CSV # requires npm on PATH .PARAMETER Subcommand Optional subcommand. When omitted, the interactive wizard is started. @@ -26,31 +27,14 @@ .PARAMETER Ref Git ref (branch or tag) to download. Default: main. -.PARAMETER Agent - Single agent target: claude, codex, opencode, or junie. - .PARAMETER Agents - Comma-separated list of agents (e.g. "claude,codex"). Mutually exclusive - with -Agent and -All. - -.PARAMETER All - Apply the subcommand to every supported agent. + Comma-separated list of agents (e.g. "claude,codex"). Single value is also + accepted (e.g. "claude"). Required by every subcommand. Valid values: + claude, codex, opencode, junie. -.PARAMETER Key +.PARAMETER Context7Key Context7 API key (mcp-context7). Prompted interactively when missing. -.PARAMETER NoClaude - skills back-compat: skip ~/.claude/skills. - -.PARAMETER NoCodex - skills back-compat: skip ~/.codex/skills. - -.PARAMETER NoOpenCode - skills back-compat: skip ~/.config/opencode/skills. - -.PARAMETER NoJunie - skills back-compat: skip ~/.junie/skills. - .EXAMPLE iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex @@ -63,14 +47,8 @@ param( [string]$Subcommand = '', [string]$Version = '', [string]$Ref = 'main', - [string]$Agent = '', [string]$Agents = '', - [switch]$All, - [string]$Key = '', - [switch]$NoClaude, - [switch]$NoCodex, - [switch]$NoOpenCode, - [switch]$NoJunie + [string]$Context7Key = '' ) $ErrorActionPreference = 'Stop' @@ -148,7 +126,13 @@ function Get-AgentLabel { } function Resolve-AgentsCsv { - param([string]$Csv) + param( + [string]$Csv, + [string]$Subcommand + ) + if ([string]::IsNullOrWhiteSpace($Csv)) { + Write-ErrAndExit "${Subcommand}: -Agents is required (e.g. -Agents claude,codex)" + } $known = @('claude', 'codex', 'opencode', 'junie') $tokens = $Csv -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } $resolved = @() @@ -158,6 +142,9 @@ function Resolve-AgentsCsv { } $resolved += $t } + if ($resolved.Count -eq 0) { + Write-ErrAndExit "${Subcommand}: -Agents resolved to an empty list" + } return $resolved } @@ -337,35 +324,7 @@ function Install-SkillsForAgent { } function Invoke-CmdSkills { - $exclusive = 0 - if ($All) { $exclusive++ } - if ($Agent) { $exclusive++ } - if ($Agents) { $exclusive++ } - if ($exclusive -gt 1) { - Write-ErrAndExit '-All, -Agent and -Agents are mutually exclusive' - } - - $agents = @() - if ($Agent) { - $agents = @($Agent) - } elseif ($Agents) { - $agents = Resolve-AgentsCsv -Csv $Agents - if ($agents.Count -eq 0) { - Write-ErrAndExit 'nothing to install (-Agents resolved to empty list)' - } - } elseif ($All -or (-not ($NoClaude -or $NoCodex -or $NoOpenCode -or $NoJunie))) { - $agents = $script:AllAgents - } else { - if (-not $NoClaude) { $agents += 'claude' } - if (-not $NoCodex) { $agents += 'codex' } - if (-not $NoOpenCode) { $agents += 'opencode' } - if (-not $NoJunie) { $agents += 'junie' } - } - - if ($agents.Count -eq 0) { - Write-ErrAndExit 'nothing to install (all -No* flags set)' - } - + $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'skills' Initialize-Tarball foreach ($a in $agents) { Install-SkillsForAgent -Agent $a @@ -417,28 +376,7 @@ function Install-AgentsMdFor { } function Invoke-CmdAgentsMd { - $exclusive = 0 - if ($All) { $exclusive++ } - if ($Agent) { $exclusive++ } - if ($Agents) { $exclusive++ } - if ($exclusive -gt 1) { - Write-ErrAndExit '-All, -Agent and -Agents are mutually exclusive' - } - - $agents = @() - if ($Agent) { - $agents = @($Agent) - } elseif ($Agents) { - $agents = Resolve-AgentsCsv -Csv $Agents - if ($agents.Count -eq 0) { - Write-ErrAndExit 'agents-md: -Agents resolved to empty list' - } - } elseif ($All) { - $agents = $script:AllAgents - } else { - Write-ErrAndExit 'agents-md: specify -All, -Agent NAME, or -Agents CSV' - } - + $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'agents-md' Write-Info "Project guidelines target directory: $((Get-Location).Path)" Initialize-Tarball foreach ($a in $agents) { @@ -515,14 +453,7 @@ function Install-JetbrainsFor { } function Invoke-CmdMcpJetbrains { - $agents = @() - if ($Agent) { - $agents = @($Agent) - } elseif ($All) { - $agents = $script:JetbrainsAgents - } else { - Write-ErrAndExit 'mcp-jetbrains: specify -All or -Agent NAME' - } + $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'mcp-jetbrains' foreach ($a in $agents) { try { Install-JetbrainsFor -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } } @@ -588,16 +519,9 @@ function Install-Context7For { } function Invoke-CmdMcpContext7 { - $agents = @() - if ($Agent) { - $agents = @($Agent) - } elseif ($All) { - $agents = $script:Context7Agents - } else { - Write-ErrAndExit 'mcp-context7: specify -All or -Agent NAME' - } + $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'mcp-context7' - $apiKey = $Key + $apiKey = $Context7Key if (-not $apiKey) { $apiKey = Read-Prompt -Message 'Context7 API key' -Default '' if (-not $apiKey) { Write-ErrAndExit 'Context7 API key is required' } @@ -608,6 +532,78 @@ function Invoke-CmdMcpContext7 { } } +# ================================================================= +# Playwright skills install (npm + playwright-cli) +# ================================================================= + +function Install-PlaywrightForAgents { + param([string[]]$Agents) + + Test-Tool -Tool 'npm' + Write-Info 'Installing/upgrading @playwright/cli globally via npm...' + & npm i -g '@playwright/cli@latest' + if ($LASTEXITCODE -ne 0) { + Write-ErrAndExit 'npm install of @playwright/cli failed' + } + + Test-Tool -Tool 'playwright-cli' + + $claudeSkills = Join-Path $HOME '.claude/skills' + if (-not (Test-Path $claudeSkills)) { + New-Item -ItemType Directory -Path $claudeSkills -Force | Out-Null + } + + $before = @() + if (Test-Path $claudeSkills) { + $before = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name + } + + Write-Info "Running 'playwright-cli install --skills'..." + & playwright-cli install --skills + if ($LASTEXITCODE -ne 0) { + Write-ErrAndExit 'playwright-cli install --skills failed' + } + + $after = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name + $newSkills = $after | Where-Object { $before -notcontains $_ } + + if ($newSkills.Count -eq 0) { + Write-Info " No new skill folders detected in $claudeSkills." + } else { + Write-Info " Playwright skill(s) installed in ${claudeSkills}: $($newSkills -join ' ')" + } + + foreach ($agent in $Agents) { + if ($agent -eq 'claude') { continue } + $target = Get-SkillsTarget -Agent $agent + if (-not (Test-Path $target)) { + New-Item -ItemType Directory -Path $target -Force | Out-Null + } + foreach ($skill in $newSkills) { + $src = Join-Path $claudeSkills $skill + if (-not (Test-Path $src)) { continue } + $dest = Join-Path $target $skill + if (Test-Path $dest) { + $backupName = "$skill.bak-$($script:Timestamp)" + Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop + Copy-Item -Path $src -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Updated: $dest (backup: $backupName)" + } else { + Copy-Item -Path $src -Destination $dest -Recurse -Force -ErrorAction Stop + Write-Info " Installed: $dest" + } + } + } + + Write-Info '' + Write-Info "Done. Playwright skills installed for: $($Agents -join ', ')" +} + +function Invoke-CmdPlaywright { + $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'playwright' + Install-PlaywrightForAgents -Agents $agents +} + # ================================================================= # Wizard # ================================================================= @@ -650,10 +646,11 @@ function Invoke-Wizard { guidelines = 'skipped' jetbrains = 'skipped' context7 = 'skipped' + playwright = 'skipped' } # Step 1: skills - $sel = Read-AgentChoice -Label '[1/4] Install Jmix skills globally?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills globally?' -Options $script:AllAgents if ($sel[0] -ne 'skip') { Initialize-Tarball foreach ($a in $sel) { @@ -663,7 +660,7 @@ function Invoke-Wizard { } # Step 2: agents-md - $sel = Read-AgentChoice -Label '[2/4] Add Jmix coding guidelines to this directory?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[2/5] Add Jmix coding guidelines to this directory?' -Options $script:AllAgents if ($sel[0] -ne 'skip') { if (Read-YesNo -Message "Target directory: $((Get-Location).Path). Proceed?" -Default 'y') { Initialize-Tarball @@ -677,7 +674,7 @@ function Invoke-Wizard { } # Step 3: JetBrains MCP - $sel = Read-AgentChoice -Label '[3/4] Connect agent to IntelliJ IDEA via JetBrains MCP?' -Options $script:JetbrainsAgents + $sel = Read-AgentChoice -Label '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' -Options $script:JetbrainsAgents if ($sel[0] -ne 'skip') { foreach ($a in $sel) { try { Install-JetbrainsFor -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } @@ -686,7 +683,7 @@ function Invoke-Wizard { } # Step 4: Context7 MCP - $sel = Read-AgentChoice -Label '[4/4] Connect agent to library docs via Context7 MCP?' -Options $script:Context7Agents + $sel = Read-AgentChoice -Label '[4/5] Connect agent to library docs via Context7 MCP?' -Options $script:Context7Agents if ($sel[0] -ne 'skip') { $apiKey = Read-Prompt -Message 'Context7 API key' -Default '' if ($apiKey) { @@ -700,12 +697,24 @@ function Invoke-Wizard { } } + # Step 5: Playwright + $sel = Read-AgentChoice -Label '[5/5] Install Playwright testing skills? (requires npm)' -Options $script:AllAgents + if ($sel[0] -ne 'skip') { + try { + Install-PlaywrightForAgents -Agents $sel + $summaryStrings.playwright = $sel -join ', ' + } catch { + Write-Info "error: $($_.Exception.Message)" + } + } + Write-Info '' Write-Info '=== Setup complete ===' Write-Info " Skills: $($summaryStrings.skills)" Write-Info " Guidelines: $($summaryStrings.guidelines)" Write-Info " JetBrains: $($summaryStrings.jetbrains)" Write-Info " Context7: $($summaryStrings.context7)" + Write-Info " Playwright: $($summaryStrings.playwright)" } # ================================================================= @@ -719,6 +728,7 @@ try { 'agents-md' { Invoke-CmdAgentsMd } 'mcp-jetbrains' { Invoke-CmdMcpJetbrains } 'mcp-context7' { Invoke-CmdMcpContext7 } + 'playwright' { Invoke-CmdPlaywright } default { Write-ErrAndExit "unknown subcommand: $Subcommand" } } } diff --git a/install.sh b/install.sh index 32ccb32..2563173 100755 --- a/install.sh +++ b/install.sh @@ -52,6 +52,30 @@ require_tool() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." } +# Parses a comma-separated agents list. Single value (e.g. "claude") is allowed. +# Validates each token. Emits a space-separated list to stdout. +# $1 - csv string (may be empty) +# $2 - subcommand name for the error message +parse_agents_csv() { + local csv="$1" + local subcommand="$2" + if [ -z "$csv" ]; then + die "${subcommand}: --agents is required (e.g. --agents claude,codex)" + fi + local result="" + local token + for token in $(printf '%s' "$csv" | tr ',' ' ' | tr -s ' ' ' '); do + case "$token" in + claude|codex|opencode|junie) result="${result} ${token}" ;; + "") ;; + *) die "unknown agent in --agents: '$token'" ;; + esac + done + result="$(printf '%s' "$result" | sed 's/^ //;s/ $//')" + [ -n "$result" ] || die "${subcommand}: --agents resolved to an empty list" + printf '%s' "$result" +} + # Reads a line from /dev/tty so prompts work under `curl ... | bash`. # Falls back to the supplied default when no TTY is available. prompt() { @@ -96,27 +120,27 @@ usage() { Jmix AI Agent Guidelines installer. Usage: - install.sh [--version V] [--ref REF] # interactive wizard - install.sh skills [options] # install global skills only - install.sh agents-md [options] # install project guidelines - install.sh mcp-jetbrains [options] # register JetBrains MCP - install.sh mcp-context7 [options] [--key KEY] # register Context7 MCP + install.sh [--version V] [--ref REF] # interactive wizard + install.sh skills [options] # install global skills only + install.sh agents-md [options] # install project guidelines + install.sh mcp-jetbrains [options] # register JetBrains MCP + install.sh mcp-context7 [options] [--context7-key KEY] # register Context7 MCP + install.sh playwright [options] # install Playwright testing skills Common options: --version V Jmix version (e.g. 2.8.0). Optional. Best-matching folder is picked: exact -> major.minor -> major -> latest. --ref REF Git ref to download (default: main). - --agent NAME Apply to one of: claude, codex, opencode, junie. - --all Apply to every supported agent for the subcommand. + --agents CSV Comma-separated agent list. Accepts a single value too + (e.g. "claude" or "claude,codex"). Required by every + subcommand. Valid values: claude, codex, opencode, junie. -h, --help Show this help. -skills options: - --agents CSV Comma-separated agent list (e.g. claude,codex). - Mutually exclusive with --agent and --all. - --no-claude --no-codex --no-opencode --no-junie (back-compat with --all) - mcp-context7 options: - --key KEY Context7 API key. Prompted interactively when missing. + --context7-key K Context7 API key. Prompted interactively when missing. + +playwright options: + (uses common --agents flag; requires `npm` on PATH) EOF } @@ -294,27 +318,13 @@ install_skills_for_agent() { } cmd_skills() { - local agents="" - local pick_all=0 - local pick_agent="" - local pick_agents_csv="" - - # Back-compat flags - local nc=0 nx=0 no=0 nj=0 + local agents_csv="" while [ $# -gt 0 ]; do case "$1" in - --all) pick_all=1; shift ;; - --agent) - [ $# -ge 2 ] || die "--agent requires an argument" - pick_agent="$2"; shift 2 ;; --agents) [ $# -ge 2 ] || die "--agents requires an argument" - pick_agents_csv="$2"; shift 2 ;; - --no-claude) nc=1; shift ;; - --no-codex) nx=1; shift ;; - --no-opencode) no=1; shift ;; - --no-junie) nj=1; shift ;; + agents_csv="$2"; shift 2 ;; --version) [ $# -ge 2 ] || die "--version requires an argument" VERSION="$2"; shift 2 ;; @@ -326,38 +336,8 @@ cmd_skills() { esac done - local exclusive_count=0 - [ "$pick_all" -eq 1 ] && exclusive_count=$((exclusive_count + 1)) - [ -n "$pick_agent" ] && exclusive_count=$((exclusive_count + 1)) - [ -n "$pick_agents_csv" ] && exclusive_count=$((exclusive_count + 1)) - if [ "$exclusive_count" -gt 1 ]; then - die "--all, --agent and --agents are mutually exclusive" - fi - - if [ -n "$pick_agent" ]; then - agents="$pick_agent" - elif [ -n "$pick_agents_csv" ]; then - # Parse CSV -> space-separated list, validate each token. - local raw token - raw="$(printf '%s' "$pick_agents_csv" | tr ',' ' ' | tr -s ' ' ' ')" - for token in $raw; do - case "$token" in - claude|codex|opencode|junie) agents="${agents} ${token}" ;; - "") ;; - *) die "unknown agent in --agents: '$token'" ;; - esac - done - elif [ "$pick_all" -eq 1 ] || [ "$nc$nx$no$nj" = "0000" ]; then - agents="$ALL_AGENTS" - else - [ "$nc" -eq 0 ] && agents="${agents} claude" - [ "$nx" -eq 0 ] && agents="${agents} codex" - [ "$no" -eq 0 ] && agents="${agents} opencode" - [ "$nj" -eq 0 ] && agents="${agents} junie" - fi - - agents="$(printf '%s' "$agents" | tr -s ' ' ' ' | sed 's/^ //;s/ $//')" - [ -n "$agents" ] || die "nothing to install (no agents resolved)" + local agents + agents="$(parse_agents_csv "$agents_csv" "skills")" ensure_tarball @@ -412,19 +392,13 @@ install_agents_md_for() { } cmd_agents_md() { - local pick_all=0 - local pick_agent="" - local pick_agents_csv="" + local agents_csv="" while [ $# -gt 0 ]; do case "$1" in - --all) pick_all=1; shift ;; - --agent) - [ $# -ge 2 ] || die "--agent requires an argument" - pick_agent="$2"; shift 2 ;; --agents) [ $# -ge 2 ] || die "--agents requires an argument" - pick_agents_csv="$2"; shift 2 ;; + agents_csv="$2"; shift 2 ;; --version) [ $# -ge 2 ] || die "--version requires an argument" VERSION="$2"; shift 2 ;; @@ -436,34 +410,8 @@ cmd_agents_md() { esac done - local exclusive_count=0 - [ "$pick_all" -eq 1 ] && exclusive_count=$((exclusive_count + 1)) - [ -n "$pick_agent" ] && exclusive_count=$((exclusive_count + 1)) - [ -n "$pick_agents_csv" ] && exclusive_count=$((exclusive_count + 1)) - if [ "$exclusive_count" -gt 1 ]; then - die "--all, --agent and --agents are mutually exclusive" - fi - - local agents="" - if [ -n "$pick_agent" ]; then - agents="$pick_agent" - elif [ -n "$pick_agents_csv" ]; then - local raw token - raw="$(printf '%s' "$pick_agents_csv" | tr ',' ' ' | tr -s ' ' ' ')" - for token in $raw; do - case "$token" in - claude|codex|opencode|junie) agents="${agents} ${token}" ;; - "") ;; - *) die "unknown agent in --agents: '$token'" ;; - esac - done - agents="$(printf '%s' "$agents" | sed 's/^ //;s/ $//')" - [ -n "$agents" ] || die "agents-md: --agents resolved to empty list" - elif [ "$pick_all" -eq 1 ]; then - agents="$ALL_AGENTS" - else - die "agents-md: specify --all, --agent NAME, or --agents CSV" - fi + local agents + agents="$(parse_agents_csv "$agents_csv" "agents-md")" log "Project guidelines target directory: $(pwd -P)" ensure_tarball @@ -536,28 +484,20 @@ install_jetbrains_for() { } cmd_mcp_jetbrains() { - local pick_all=0 - local pick_agent="" + local agents_csv="" while [ $# -gt 0 ]; do case "$1" in - --all) pick_all=1; shift ;; - --agent) - [ $# -ge 2 ] || die "--agent requires an argument" - pick_agent="$2"; shift 2 ;; + --agents) + [ $# -ge 2 ] || die "--agents requires an argument" + agents_csv="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; esac done - local agents="" - if [ -n "$pick_agent" ]; then - agents="$pick_agent" - elif [ "$pick_all" -eq 1 ]; then - agents="$JETBRAINS_AGENTS" - else - die "mcp-jetbrains: specify --all or --agent NAME" - fi + local agents + agents="$(parse_agents_csv "$agents_csv" "mcp-jetbrains")" local agent rc=0 for agent in $agents; do @@ -643,32 +583,24 @@ install_context7_for() { } cmd_mcp_context7() { - local pick_all=0 - local pick_agent="" + local agents_csv="" local key="" while [ $# -gt 0 ]; do case "$1" in - --all) pick_all=1; shift ;; - --agent) - [ $# -ge 2 ] || die "--agent requires an argument" - pick_agent="$2"; shift 2 ;; - --key) - [ $# -ge 2 ] || die "--key requires an argument" + --agents) + [ $# -ge 2 ] || die "--agents requires an argument" + agents_csv="$2"; shift 2 ;; + --context7-key) + [ $# -ge 2 ] || die "--context7-key requires an argument" key="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; esac done - local agents="" - if [ -n "$pick_agent" ]; then - agents="$pick_agent" - elif [ "$pick_all" -eq 1 ]; then - agents="$CONTEXT7_AGENTS" - else - die "mcp-context7: specify --all or --agent NAME" - fi + local agents + agents="$(parse_agents_csv "$agents_csv" "mcp-context7")" if [ -z "$key" ]; then key="$(prompt 'Context7 API key' '')" @@ -682,6 +614,77 @@ cmd_mcp_context7() { return $rc } +# ================================================================= +# Playwright skills install (npm + playwright-cli) +# ================================================================= + +cmd_playwright() { + local agents_csv="" + while [ $# -gt 0 ]; do + case "$1" in + --agents) + [ $# -ge 2 ] || die "--agents requires an argument" + agents_csv="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + done + + local agents + agents="$(parse_agents_csv "$agents_csv" "playwright")" + + require_tool npm + + log "Installing/upgrading @playwright/cli globally via npm..." + npm i -g @playwright/cli@latest || die "npm install of @playwright/cli failed" + + require_tool playwright-cli + + local claude_skills="${HOME}/.claude/skills" + mkdir -p "$claude_skills" || die "cannot create ${claude_skills}" + local before + before="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" + + log "Running 'playwright-cli install --skills'..." + playwright-cli install --skills || die "playwright-cli install --skills failed" + + local after + after="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" + local new_skills + new_skills="$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after"))" + + if [ -z "$new_skills" ]; then + log " No new skill folders detected in ${claude_skills}." + log " Playwright skills appear to be up to date for Claude Code." + else + log " Playwright skill(s) installed in ${claude_skills}: $(printf '%s' "$new_skills" | tr '\n' ' ')" + fi + + # Replicate the newly-installed skills to the other selected agents. + local agent target dest backup skill + for agent in $agents; do + [ "$agent" = "claude" ] && continue + target="$(skills_target_for_agent "$agent")" + mkdir -p "$target" || die "cannot create ${target}" + for skill in $new_skills; do + [ -d "${claude_skills}/${skill}" ] || continue + dest="${target}/${skill}" + if [ -e "$dest" ]; then + backup="${dest}.bak-${TIMESTAMP}" + mv "$dest" "$backup" || die "cannot rename ${dest}" + cp -R "${claude_skills}/${skill}" "$dest" || die "cannot copy to ${dest}" + log " Updated: ${dest} (backup: $(basename "$backup"))" + else + cp -R "${claude_skills}/${skill}" "$dest" || die "cannot copy to ${dest}" + log " Installed: ${dest}" + fi + done + done + + log "" + log "Done. Playwright skills installed for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" +} + # ================================================================= # Wizard # ================================================================= @@ -753,10 +756,11 @@ cmd_wizard() { local summary_guidelines="skipped" local summary_jetbrains="skipped" local summary_context7="skipped" + local summary_playwright="skipped" # Step 1: skills local sel - sel="$(wizard_pick_agent '[1/4] Install Jmix skills globally?' $ALL_AGENTS)" + sel="$(wizard_pick_agent '[1/5] Install Jmix skills globally?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then ensure_tarball local agent @@ -767,7 +771,7 @@ cmd_wizard() { fi # Step 2: agents-md - sel="$(wizard_pick_agent '[2/4] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" + sel="$(wizard_pick_agent '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then ensure_tarball @@ -782,7 +786,7 @@ cmd_wizard() { fi # Step 3: JetBrains MCP - sel="$(wizard_pick_agent '[3/4] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" + sel="$(wizard_pick_agent '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" if [ "$sel" != "skip" ]; then local agent for agent in $sel; do @@ -792,7 +796,7 @@ cmd_wizard() { fi # Step 4: Context7 MCP - sel="$(wizard_pick_agent '[4/4] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" + sel="$(wizard_pick_agent '[4/5] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" if [ "$sel" != "skip" ]; then local key key="$(prompt 'Context7 API key' '')" @@ -808,12 +812,22 @@ cmd_wizard() { fi fi + # Step 5: Playwright + sel="$(wizard_pick_agent '[5/5] Install Playwright testing skills? (requires npm)' $ALL_AGENTS)" + if [ "$sel" != "skip" ]; then + local pw_csv + pw_csv="$(printf '%s' "$sel" | tr ' ' ',' | sed 's/^,//;s/,$//')" + cmd_playwright --agents "$pw_csv" || true + summary_playwright="$sel" + fi + log "" log "=== Setup complete ===" log " Skills: ${summary_skills}" log " Guidelines: ${summary_guidelines}" log " JetBrains: ${summary_jetbrains}" log " Context7: ${summary_context7}" + log " Playwright: ${summary_playwright}" } # ================================================================= @@ -830,6 +844,7 @@ case "$1" in agents-md) shift; cmd_agents_md "$@" ;; mcp-jetbrains) shift; cmd_mcp_jetbrains "$@" ;; mcp-context7) shift; cmd_mcp_context7 "$@" ;; + playwright) shift; cmd_playwright "$@" ;; -h|--help) usage ;; --*) cmd_wizard "$@" ;; *) die "unknown subcommand: $1" ;; From 2d1190f058352c7ea873c17a1f8b4b57074f9acf Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 18 May 2026 22:56:54 +0400 Subject: [PATCH 09/36] Create backup files only in case of flag --backup-existing-files --- README.md | 15 +++++---- install.ps1 | 66 ++++++++++++++++++++---------------- install.sh | 96 +++++++++++++++++++++++++++++++++-------------------- 3 files changed, 106 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 5a1bdc8..0821663 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,17 @@ install.sh playwright --agents CSV # requires npm on PATH PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `install.ps1 mcp-context7 -Agents claude -Context7Key KEY`, `install.ps1 playwright -Agents claude,codex`, etc. -**CSV** = comma-separated agent list (e.g. `claude,codex`) or a single value (e.g. `claude`). Valid agents: `claude`, `codex`, `opencode`, `junie`. +**CSV** = comma-separated agent list (e.g. `claude,codex`) or a single value (e.g. `claude`). ### Flags -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:-------------------|:------------------|:--------|:--------------------------------------------------------| -| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | -| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | -| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:--------------------------|:-----------------------|:--------|:-------------------------------------------------------------------------------------------------------------| +| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | +| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | +| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | > The automatic installer covers global skills, project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. diff --git a/install.ps1 b/install.ps1 index 62af4bc..7ff6d46 100644 --- a/install.ps1 +++ b/install.ps1 @@ -17,6 +17,9 @@ install.ps1 mcp-context7 -Agents CSV [-Context7Key KEY] install.ps1 playwright -Agents CSV # requires npm on PATH + Add -BackupExistingFiles to any subcommand to rename overwritten files/dirs + to .bak- instead of deleting them. + .PARAMETER Subcommand Optional subcommand. When omitted, the interactive wizard is started. @@ -35,6 +38,11 @@ .PARAMETER Context7Key Context7 API key (mcp-context7). Prompted interactively when missing. +.PARAMETER BackupExistingFiles + When set, an existing destination file or folder is renamed to + .bak- instead of being deleted before the new content is + copied. Off by default. + .EXAMPLE iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex @@ -48,7 +56,8 @@ param( [string]$Version = '', [string]$Ref = 'main', [string]$Agents = '', - [string]$Context7Key = '' + [string]$Context7Key = '', + [switch]$BackupExistingFiles ) $ErrorActionPreference = 'Stop' @@ -125,6 +134,31 @@ function Get-AgentLabel { } } +function Write-Dest { + param( + [string]$Src, + [string]$Dest, + [string]$Label + ) + $existed = Test-Path $Dest + $backupInfo = '' + if ($existed) { + if ($BackupExistingFiles) { + $backupName = "$([System.IO.Path]::GetFileName($Dest)).bak-$($script:Timestamp)" + Rename-Item -Path $Dest -NewName $backupName -ErrorAction Stop + $backupInfo = " (backup: $backupName)" + } else { + Remove-Item -Path $Dest -Recurse -Force -ErrorAction Stop + } + } + Copy-Item -Path $Src -Destination $Dest -Recurse -Force -ErrorAction Stop + if ($existed) { + Write-Info " Updated: $Label$backupInfo" + } else { + Write-Info " Installed: $Label" + } +} + function Resolve-AgentsCsv { param( [string]$Csv, @@ -309,15 +343,7 @@ function Install-SkillsForAgent { $count = 0 foreach ($skill in Get-ChildItem -Path $script:SourceSkillsDir -Directory) { $dest = Join-Path $target $skill.Name - $backupName = "$($skill.Name).bak-$($script:Timestamp)" - if (Test-Path $dest) { - Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop - Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Updated: $($skill.Name) (backup: $backupName)" - } else { - Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Installed: $($skill.Name)" - } + Write-Dest -Src $skill.FullName -Dest $dest -Label $skill.Name $count++ } Write-Info " $count skill(s) processed for $label" @@ -363,15 +389,7 @@ function Install-AgentsMdFor { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } - if (Test-Path $dest) { - $backup = "$dest.bak-$($script:Timestamp)" - Rename-Item -Path $dest -NewName (Split-Path -Leaf $backup) -ErrorAction Stop - Copy-Item -Path $script:SourceAgentsMd -Destination $dest -Force -ErrorAction Stop - Write-Info " Updated: $dest (backup: $(Split-Path -Leaf $backup))" - } else { - Copy-Item -Path $script:SourceAgentsMd -Destination $dest -Force -ErrorAction Stop - Write-Info " Installed: $dest" - } + Write-Dest -Src $script:SourceAgentsMd -Dest $dest -Label $dest Write-Info " Project guidelines installed for $label" } @@ -583,15 +601,7 @@ function Install-PlaywrightForAgents { $src = Join-Path $claudeSkills $skill if (-not (Test-Path $src)) { continue } $dest = Join-Path $target $skill - if (Test-Path $dest) { - $backupName = "$skill.bak-$($script:Timestamp)" - Rename-Item -Path $dest -NewName $backupName -ErrorAction Stop - Copy-Item -Path $src -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Updated: $dest (backup: $backupName)" - } else { - Copy-Item -Path $src -Destination $dest -Recurse -Force -ErrorAction Stop - Write-Info " Installed: $dest" - } + Write-Dest -Src $src -Dest $dest -Label $dest } } diff --git a/install.sh b/install.sh index 2563173..41495b8 100755 --- a/install.sh +++ b/install.sh @@ -24,6 +24,7 @@ TARBALL_READY=0 VERSION="" REF="main" +BACKUP_EXISTING=0 TIMESTAMP="$(date +%Y%m%d-%H%M%S)" @@ -52,6 +53,36 @@ require_tool() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." } +# Replaces or installs $dest with a copy of $src. When BACKUP_EXISTING=1, an +# existing $dest is moved aside to .bak-; otherwise it is +# deleted. Prints a per-item log line. +# $1 - src path (file or dir) +# $2 - dest path +# $3 - short label shown in the log line +write_dest() { + local src="$1" + local dest="$2" + local label="$3" + local existed=0 + [ -e "$dest" ] && existed=1 + local backup_info="" + if [ "$existed" -eq 1 ]; then + if [ "$BACKUP_EXISTING" -eq 1 ]; then + local backup="${dest}.bak-${TIMESTAMP}" + mv "$dest" "$backup" || die "cannot rename ${dest}" + backup_info=" (backup: $(basename "$backup"))" + else + rm -rf "$dest" || die "cannot remove ${dest}" + fi + fi + cp -R "$src" "$dest" || die "cannot copy to ${dest}" + if [ "$existed" -eq 1 ]; then + log " Updated: ${label}${backup_info}" + else + log " Installed: ${label}" + fi +} + # Parses a comma-separated agents list. Single value (e.g. "claude") is allowed. # Validates each token. Emits a space-separated list to stdout. # $1 - csv string (may be empty) @@ -128,13 +159,18 @@ Usage: install.sh playwright [options] # install Playwright testing skills Common options: - --version V Jmix version (e.g. 2.8.0). Optional. Best-matching folder - is picked: exact -> major.minor -> major -> latest. - --ref REF Git ref to download (default: main). - --agents CSV Comma-separated agent list. Accepts a single value too - (e.g. "claude" or "claude,codex"). Required by every - subcommand. Valid values: claude, codex, opencode, junie. - -h, --help Show this help. + --version V Jmix version (e.g. 2.8.0). Optional. Best-matching + folder is picked: exact -> major.minor -> major -> + latest. + --ref REF Git ref to download (default: main). + --agents CSV Comma-separated agent list. Accepts a single value + too (e.g. "claude" or "claude,codex"). Required by + every subcommand. Valid values: + claude, codex, opencode, junie. + --backup-existing-files Rename overwritten files/dirs to + .bak- instead of deleting them. + Off by default. + -h, --help Show this help. mcp-context7 options: --context7-key K Context7 API key. Prompted interactively when missing. @@ -298,20 +334,12 @@ install_skills_for_agent() { mkdir -p "$target_dir" || die "cannot write to ${target_dir}: mkdir failed" local count=0 - local skill name dest backup + local skill name dest for skill in "$SOURCE_SKILLS_DIR"/*/; do [ -d "$skill" ] || continue name="$(basename "$skill")" dest="${target_dir}/${name}" - if [ -e "$dest" ]; then - backup="${dest}.bak-${TIMESTAMP}" - mv "$dest" "$backup" || die "cannot write to ${dest}: rename failed" - cp -R "$skill" "$dest" || die "cannot write to ${dest}: copy failed" - log " Updated: ${name} (backup: $(basename "$backup"))" - else - cp -R "$skill" "$dest" || die "cannot write to ${dest}: copy failed" - log " Installed: ${name}" - fi + write_dest "$skill" "$dest" "$name" count=$((count + 1)) done log " ${count} skill(s) processed for ${label}" @@ -325,6 +353,8 @@ cmd_skills() { --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; --version) [ $# -ge 2 ] || die "--version requires an argument" VERSION="$2"; shift 2 ;; @@ -379,15 +409,7 @@ install_agents_md_for() { dest_dir="$(dirname "$dest")" mkdir -p "$dest_dir" || die "cannot create directory ${dest_dir}" - if [ -e "$dest" ]; then - local backup="${dest}.bak-${TIMESTAMP}" - mv "$dest" "$backup" || die "cannot rename existing ${dest}" - cp "$SOURCE_AGENTS_MD" "$dest" || die "cannot write ${dest}" - log " Updated: ${dest} (backup: $(basename "$backup"))" - else - cp "$SOURCE_AGENTS_MD" "$dest" || die "cannot write ${dest}" - log " Installed: ${dest}" - fi + write_dest "$SOURCE_AGENTS_MD" "$dest" "$dest" log " Project guidelines installed for ${label}" } @@ -399,6 +421,8 @@ cmd_agents_md() { --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; --version) [ $# -ge 2 ] || die "--version requires an argument" VERSION="$2"; shift 2 ;; @@ -491,6 +515,8 @@ cmd_mcp_jetbrains() { --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; esac @@ -591,6 +617,8 @@ cmd_mcp_context7() { --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; --context7-key) [ $# -ge 2 ] || die "--context7-key requires an argument" key="$2"; shift 2 ;; @@ -625,6 +653,8 @@ cmd_playwright() { --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; esac @@ -661,7 +691,7 @@ cmd_playwright() { fi # Replicate the newly-installed skills to the other selected agents. - local agent target dest backup skill + local agent target dest skill for agent in $agents; do [ "$agent" = "claude" ] && continue target="$(skills_target_for_agent "$agent")" @@ -669,15 +699,7 @@ cmd_playwright() { for skill in $new_skills; do [ -d "${claude_skills}/${skill}" ] || continue dest="${target}/${skill}" - if [ -e "$dest" ]; then - backup="${dest}.bak-${TIMESTAMP}" - mv "$dest" "$backup" || die "cannot rename ${dest}" - cp -R "${claude_skills}/${skill}" "$dest" || die "cannot copy to ${dest}" - log " Updated: ${dest} (backup: $(basename "$backup"))" - else - cp -R "${claude_skills}/${skill}" "$dest" || die "cannot copy to ${dest}" - log " Installed: ${dest}" - fi + write_dest "${claude_skills}/${skill}" "$dest" "$dest" done done @@ -743,6 +765,8 @@ cmd_wizard() { --ref) [ $# -ge 2 ] || die "--ref requires an argument" REF="$2"; shift 2 ;; + --backup-existing-files) + BACKUP_EXISTING=1; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; esac From 6c982b5c817cb694c278d2f6a1189092af3e0fd8 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Tue, 19 May 2026 17:39:41 +0400 Subject: [PATCH 10/36] Fixes --- .studio/studio-meta-data.json | 4 ++-- install.ps1 | 4 ++-- install.sh | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 7b2a515..ca15439 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -91,10 +91,10 @@ }, { "id": "installPlaywright", - "message": "Install Playwright testing skills? Requires npm to be installed on PATH.", + "message": "Install Playwright? Requires npm to be installed on PATH.", "input": { "type": "checkbox", - "label": "Install Playwright skills", + "label": "Install Playwright", "default": false }, "command": { diff --git a/install.ps1 b/install.ps1 index 7ff6d46..edcc9fc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -551,7 +551,7 @@ function Invoke-CmdMcpContext7 { } # ================================================================= -# Playwright skills install (npm + playwright-cli) +# Playwright install (npm + playwright-cli) # ================================================================= function Install-PlaywrightForAgents { @@ -708,7 +708,7 @@ function Invoke-Wizard { } # Step 5: Playwright - $sel = Read-AgentChoice -Label '[5/5] Install Playwright testing skills? (requires npm)' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[5/5] Install Playwright? (requires npm)' -Options $script:AllAgents if ($sel[0] -ne 'skip') { try { Install-PlaywrightForAgents -Agents $sel diff --git a/install.sh b/install.sh index 41495b8..f27430d 100755 --- a/install.sh +++ b/install.sh @@ -156,7 +156,7 @@ Usage: install.sh agents-md [options] # install project guidelines install.sh mcp-jetbrains [options] # register JetBrains MCP install.sh mcp-context7 [options] [--context7-key KEY] # register Context7 MCP - install.sh playwright [options] # install Playwright testing skills + install.sh playwright [options] # install Playwright Common options: --version V Jmix version (e.g. 2.8.0). Optional. Best-matching @@ -643,7 +643,7 @@ cmd_mcp_context7() { } # ================================================================= -# Playwright skills install (npm + playwright-cli) +# Playwright install (npm + playwright-cli) # ================================================================= cmd_playwright() { @@ -837,7 +837,7 @@ cmd_wizard() { fi # Step 5: Playwright - sel="$(wizard_pick_agent '[5/5] Install Playwright testing skills? (requires npm)' $ALL_AGENTS)" + sel="$(wizard_pick_agent '[5/5] Install Playwright? (requires npm)' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then local pw_csv pw_csv="$(printf '%s' "$sel" | tr ' ' ',' | sed 's/^,//;s/,$//')" From 6dd26b128d8213f215cbccce44769307441fcba9 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Tue, 19 May 2026 18:24:07 +0400 Subject: [PATCH 11/36] Fixes --- .studio/studio-meta-data.json | 44 ++++++----------------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index ca15439..35a0752 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,7 +4,7 @@ "steps": [ { "id": "skillsAgents", - "message": "Pick agents to install Jmix skills for.", + "message": "Install Jmix skills globally for the selected agents. Click Skip to opt out.", "input": { "type": "options", "multi": true, @@ -21,19 +21,9 @@ "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"" } }, - { - "id": "installGuidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md) to the project root?", - "input": { - "type": "checkbox", - "label": "Install project guidelines", - "default": true - } - }, { "id": "guidelinesAgents", - "runIf": "installGuidelines", - "message": "Pick agents for the project guideline files.", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md) to the project root for the selected agents. Click Skip to leave the project as is.", "input": { "type": "options", "multi": true, @@ -51,32 +41,17 @@ } }, { - "id": "installJetbrainsMcp", - "message": "Register the JetBrains MCP server with installed agents? Recommended.", - "input": { - "type": "checkbox", - "label": "Register JetBrains MCP", - "default": true - }, + "id": "jetbrainsMcp", + "message": "Register the JetBrains MCP server with the agents you installed skills for. Recommended for IntelliJ users. Click Skip to opt out.", "command": { "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -Agents \"${skillsAgents}\"", "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"", "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"" } }, - { - "id": "installContext7", - "message": "Register the Context7 MCP server (provides up-to-date library documentation)?", - "input": { - "type": "checkbox", - "label": "Register Context7 MCP", - "default": false - } - }, { "id": "context7Key", - "runIf": "installContext7", - "message": "Enter your Context7 API key. The key is sent to the MCP CLI and never stored by Studio.", + "message": "Register the Context7 MCP server (provides up-to-date library documentation). Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", "input": { "type": "userInput", "regex": "^.{8,}$", @@ -90,13 +65,8 @@ } }, { - "id": "installPlaywright", - "message": "Install Playwright? Requires npm to be installed on PATH.", - "input": { - "type": "checkbox", - "label": "Install Playwright", - "default": false - }, + "id": "playwright", + "message": "Install Playwright testing skills. Requires npm to be installed on PATH. Click Skip to opt out.", "command": { "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) playwright -Agents \"${skillsAgents}\"", "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${skillsAgents}\"", From ac59eec3d56cb8b7480bee79a1f91b94bbad30be Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Tue, 19 May 2026 19:35:09 +0400 Subject: [PATCH 12/36] Fixes --- .studio/studio-meta-data.json | 222 +++++++++++++++++++++------ .studio/studio-meta-data.schema.json | 132 +++++++++++----- 2 files changed, 268 insertions(+), 86 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 35a0752..65382c3 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -3,74 +3,206 @@ "install-flow": { "steps": [ { - "id": "skillsAgents", + "id": "skills", "message": "Install Jmix skills globally for the selected agents. Click Skip to opt out.", - "input": { - "type": "options", - "multi": true, - "choices": [ - { "value": "claude", "label": "Claude Code", "default": true }, - { "value": "codex", "label": "Codex", "default": true }, - { "value": "opencode", "label": "OpenCode", "default": true }, - { "value": "junie", "label": "Junie", "default": true } - ] - }, + "inputs": [ + { + "id": "skillsAgents", + "label": "Targets", + "type": "options", + "multi": true, + "choices": [ + { + "value": "claude", + "label": "Claude Code", + "default": true + }, + { + "value": "codex", + "label": "Codex", + "default": true + }, + { + "value": "opencode", + "label": "OpenCode", + "default": true + }, + { + "value": "junie", + "label": "Junie", + "default": true + } + ] + } + ], "command": { "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) skills -Agents \"${skillsAgents}\" -Version \"${JMIX_VERSION}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"" + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"" } }, { - "id": "guidelinesAgents", + "id": "guidelines", "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md) to the project root for the selected agents. Click Skip to leave the project as is.", - "input": { - "type": "options", - "multi": true, - "choices": [ - { "value": "claude", "label": "Claude Code", "default": true }, - { "value": "codex", "label": "Codex", "default": true }, - { "value": "opencode", "label": "OpenCode", "default": true }, - { "value": "junie", "label": "Junie", "default": true } - ] - }, + "inputs": [ + { + "id": "guidelinesAgents", + "label": "Targets", + "type": "options", + "multi": true, + "choices": [ + { + "value": "claude", + "label": "Claude Code", + "default": true + }, + { + "value": "codex", + "label": "Codex", + "default": true + }, + { + "value": "opencode", + "label": "OpenCode", + "default": true + }, + { + "value": "junie", + "label": "Junie", + "default": true + } + ] + } + ], "command": { "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) agents-md -Agents \"${guidelinesAgents}\" -Version \"${JMIX_VERSION}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"" + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- agents-md --agents \"${guidelinesAgents}\" --version \"${JMIX_VERSION}\"" } }, { "id": "jetbrainsMcp", - "message": "Register the JetBrains MCP server with the agents you installed skills for. Recommended for IntelliJ users. Click Skip to opt out.", + "message": "Register the JetBrains MCP server with the selected agents. Recommended for IntelliJ users. Click Skip to opt out.", + "inputs": [ + { + "id": "jetbrainsMcpAgents", + "label": "Targets", + "type": "options", + "multi": true, + "choices": [ + { + "value": "claude", + "label": "Claude Code", + "default": true + }, + { + "value": "codex", + "label": "Codex", + "default": true + }, + { + "value": "opencode", + "label": "OpenCode", + "default": true + }, + { + "value": "junie", + "label": "Junie", + "default": true + } + ] + } + ], "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -Agents \"${skillsAgents}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${skillsAgents}\"" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-jetbrains -Agents \"${jetbrainsMcpAgents}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${jetbrainsMcpAgents}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-jetbrains --agents \"${jetbrainsMcpAgents}\"" } }, { - "id": "context7Key", - "message": "Register the Context7 MCP server (provides up-to-date library documentation). Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", - "input": { - "type": "userInput", - "regex": "^.{8,}$", - "errorMessage": "Context7 API key must be at least 8 characters.", - "placeholder": "ctx7_..." - }, + "id": "context7", + "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents. Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", + "inputs": [ + { + "id": "context7Agents", + "label": "Targets", + "type": "options", + "multi": true, + "choices": [ + { + "value": "claude", + "label": "Claude Code", + "default": true + }, + { + "value": "codex", + "label": "Codex", + "default": true + }, + { + "value": "opencode", + "label": "OpenCode", + "default": true + }, + { + "value": "junie", + "label": "Junie", + "default": true + } + ] + }, + { + "id": "context7Key", + "label": "API key", + "type": "userInput", + "regex": "^.{8,}$", + "errorMessage": "Context7 API key must be at least 8 characters.", + "placeholder": "ctx7_..." + } + ], "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-context7 -Agents \"${skillsAgents}\" -Context7Key \"${context7Key}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${skillsAgents}\" --context7-key \"${context7Key}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${skillsAgents}\" --context7-key \"${context7Key}\"" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) mcp-context7 -Agents \"${context7Agents}\" -Context7Key \"${context7Key}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${context7Agents}\" --context7-key \"${context7Key}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- mcp-context7 --agents \"${context7Agents}\" --context7-key \"${context7Key}\"" } }, { "id": "playwright", - "message": "Install Playwright testing skills. Requires npm to be installed on PATH. Click Skip to opt out.", + "message": "Install Playwright testing skills for the selected agents. Requires npm to be installed on PATH. Click Skip to opt out.", + "inputs": [ + { + "id": "playwrightAgents", + "label": "Targets", + "type": "options", + "multi": true, + "choices": [ + { + "value": "claude", + "label": "Claude Code", + "default": true + }, + { + "value": "codex", + "label": "Codex", + "default": true + }, + { + "value": "opencode", + "label": "OpenCode", + "default": true + }, + { + "value": "junie", + "label": "Junie", + "default": true + } + ] + } + ], "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) playwright -Agents \"${skillsAgents}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${skillsAgents}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${skillsAgents}\"" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) playwright -Agents \"${playwrightAgents}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${playwrightAgents}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- playwright --agents \"${playwrightAgents}\"" } } ] diff --git a/.studio/studio-meta-data.schema.json b/.studio/studio-meta-data.schema.json index cf141b2..62fed77 100644 --- a/.studio/studio-meta-data.schema.json +++ b/.studio/studio-meta-data.schema.json @@ -2,21 +2,27 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/.studio/studio-meta-data.schema.json", "title": "Jmix Studio install-flow metadata", - "description": "Metadata consumed by Jmix Studio to drive the 'Configure AI Agents' wizard. Defines a sequence of typed-input steps; each step optionally executes a shell command assembled from collected values.", + "description": "Metadata consumed by Jmix Studio to drive the 'Configure AI Agents' wizard. Defines a sequence of multi-input steps; each step optionally executes a shell command assembled from collected input values.", "type": "object", "additionalProperties": true, - "required": ["install-flow"], + "required": [ + "install-flow" + ], "properties": { "install-flow": { "type": "object", "description": "Top-level container for the wizard flow.", - "required": ["steps"], + "required": [ + "steps" + ], "additionalProperties": false, "properties": { "steps": { "type": "array", - "description": "Steps shown to the user in order. Steps with a 'runIf' that points to a falsy earlier step are skipped entirely.", - "items": { "$ref": "#/$defs/step" } + "description": "Steps shown to the user in order. Steps with a 'runIf' that points to a falsy earlier input id are skipped entirely.", + "items": { + "$ref": "#/$defs/step" + } } } } @@ -24,51 +30,69 @@ "$defs": { "step": { "type": "object", - "description": "A single step of the install flow. Steps may have an input (to collect a value), a command (to execute), or both.", + "description": "A single step of the install flow. Steps may collect multiple inputs and optionally execute a shell command.", "additionalProperties": false, - "required": ["message"], + "required": [ + "message" + ], "properties": { "id": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "description": "Unique step identifier. Used to substitute the collected value into other steps' commands via ${id}, and as the target of 'runIf'. Required if the step has 'input' or is referenced elsewhere." + "description": "Optional step identifier used for diagnostics and per-step state restoration. Command substitution uses input ids, not step ids." }, "message": { "type": "string", - "description": "Header text shown above the input widget." + "description": "Header text shown above the inputs." }, "runIf": { "type": "string", - "description": "If set, the step is rendered only when the value of the referenced earlier step is truthy. Falsy values: checkbox=false, options-multi=empty, userInput=blank." + "description": "If set, the step is rendered only when the referenced earlier input id has a truthy value. Falsy values: checkbox=false, options-multi=empty, userInput=blank." }, - "input": { - "description": "Optional input widget shown to the user. Discriminated by 'type'.", - "oneOf": [ - { "$ref": "#/$defs/inputUserInput" }, - { "$ref": "#/$defs/inputOptions" }, - { "$ref": "#/$defs/inputCheckbox" } - ] + "inputs": { + "type": "array", + "description": "Inputs collected from the user on this step. Each input has its own id used for ${id} command substitution.", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/inputUserInput" + }, + { + "$ref": "#/$defs/inputOptions" + }, + { + "$ref": "#/$defs/inputCheckbox" + } + ] + } }, "command": { "$ref": "#/$defs/commandByOs", - "description": "Optional per-OS shell command run after the user confirms the step. Supports ${id} placeholders that reference values from earlier steps and the implicit ${JMIX_VERSION} placeholder. The command is skipped when the step's own input value is falsy." - } - }, - "allOf": [ - { - "if": { "required": ["input"] }, - "then": { "required": ["id"] } + "description": "Optional per-OS shell command. Supports ${id} placeholders that reference input ids (from this step or earlier) plus the implicit ${JMIX_VERSION} placeholder. The command is skipped when any input on this step has a falsy value." } - ] + } }, - "inputUserInput": { "type": "object", "description": "Free-text input. Optionally validated against a regular expression.", "additionalProperties": false, - "required": ["type"], + "required": [ + "id", + "type" + ], "properties": { - "type": { "const": "userInput" }, + "id": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Unique input identifier used in command substitution via ${id}." + }, + "label": { + "type": "string", + "description": "Optional row prefix shown to the left of the text field." + }, + "type": { + "const": "userInput" + }, "regex": { "type": "string", "description": "Java regular expression. The full value must match (Regex.matches semantics)." @@ -87,14 +111,28 @@ } } }, - "inputOptions": { "type": "object", "description": "Choice between predefined values. Single-select renders as a combo box; multi-select renders as a check-box list.", "additionalProperties": false, - "required": ["type", "choices"], + "required": [ + "id", + "type", + "choices" + ], "properties": { - "type": { "const": "options" }, + "id": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Unique input identifier used in command substitution via ${id}." + }, + "label": { + "type": "string", + "description": "Optional row prefix shown to the left of the widget." + }, + "type": { + "const": "options" + }, "multi": { "type": "boolean", "default": false, @@ -103,15 +141,19 @@ "choices": { "type": "array", "minItems": 1, - "items": { "$ref": "#/$defs/optionsChoice" } + "items": { + "$ref": "#/$defs/optionsChoice" + } } } }, - "optionsChoice": { "type": "object", "additionalProperties": false, - "required": ["value", "label"], + "required": [ + "value", + "label" + ], "properties": { "value": { "type": "string", @@ -128,17 +170,26 @@ } } }, - "inputCheckbox": { "type": "object", - "description": "Single boolean toggle.", + "description": "Single boolean toggle. The 'label' field doubles as the text rendered next to the checkbox.", "additionalProperties": false, - "required": ["type"], + "required": [ + "id", + "type" + ], "properties": { - "type": { "const": "checkbox" }, + "id": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Unique input identifier used in command substitution via ${id}." + }, "label": { "type": "string", - "description": "Label shown next to the checkbox." + "description": "Text shown next to the checkbox." + }, + "type": { + "const": "checkbox" }, "default": { "type": "boolean", @@ -155,7 +206,6 @@ } } }, - "commandByOs": { "type": "object", "description": "Per-OS shell command. The host OS is detected via SystemInfo and the corresponding entry is executed.", @@ -177,4 +227,4 @@ } } } -} +} \ No newline at end of file From 217f42b31b2eca673457089fd90b300b3842ffe1 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 14:50:47 +0400 Subject: [PATCH 13/36] Skills installation scope --- .studio/studio-meta-data.json | 24 ++++++++++-- README.md | 5 ++- install.ps1 | 70 +++++++++++++++++++++++++-------- install.sh | 73 +++++++++++++++++++++++++++-------- 4 files changed, 133 insertions(+), 39 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 65382c3..30f3317 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,8 +4,24 @@ "steps": [ { "id": "skills", - "message": "Install Jmix skills globally for the selected agents. Click Skip to opt out.", + "message": "Install Jmix skills for the selected agents. Choose whether to install into this project or the user home. Click Skip to opt out.", "inputs": [ + { + "id": "skillsScope", + "label": "Install to", + "type": "options", + "choices": [ + { + "value": "local", + "label": "Project (local)", + "default": true + }, + { + "value": "global", + "label": "User home (global)" + } + ] + }, { "id": "skillsAgents", "label": "Targets", @@ -36,9 +52,9 @@ } ], "command": { - "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) skills -Agents \"${skillsAgents}\" -Version \"${JMIX_VERSION}\"", - "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"", - "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --version \"${JMIX_VERSION}\"" + "windows": "& ([scriptblock]::Create((iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1).Content)) skills -Agents \"${skillsAgents}\" -Scope \"${skillsScope}\" -Version \"${JMIX_VERSION}\"", + "macos": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --scope \"${skillsScope}\" --version \"${JMIX_VERSION}\"", + "linux": "curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.sh | bash -s -- skills --agents \"${skillsAgents}\" --scope \"${skillsScope}\" --version \"${JMIX_VERSION}\"" } }, { diff --git a/README.md b/README.md index 0821663..c6b8774 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Use these to run a single step without the wizard. Every subcommand takes the same `--agents CSV` flag: ```bash -install.sh skills --agents CSV [--version V] +install.sh skills --agents CSV [--scope global|local] [--version V] install.sh agents-md --agents CSV [--version V] install.sh mcp-jetbrains --agents CSV install.sh mcp-context7 --agents CSV [--context7-key KEY] @@ -54,10 +54,11 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i | `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | | `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | | `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs into the per-agent user-home dir; `local` installs into the project (e.g. `./.claude/skills`). | | `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | | `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | -> The automatic installer covers global skills, project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. +> The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. ## Manual Installation diff --git a/install.ps1 b/install.ps1 index edcc9fc..6465113 100644 --- a/install.ps1 +++ b/install.ps1 @@ -5,13 +5,13 @@ .DESCRIPTION Default invocation (no subcommand) launches an interactive wizard that guides through: - 1. Installing Jmix skills globally for one or all agents. + 1. Installing Jmix skills (globally or into the project) for one or all agents. 2. Adding project-level guidelines (CLAUDE.md / AGENTS.md / .junie\guidelines.md). 3. Registering the JetBrains MCP server with the agent. 4. Registering the Context7 MCP server with the agent. Subcommands are available for non-interactive use: - install.ps1 skills -Agents CSV [-Version V] [-Ref REF] + install.ps1 skills -Agents CSV [-Scope global|local] [-Version V] [-Ref REF] install.ps1 agents-md -Agents CSV [-Version V] [-Ref REF] install.ps1 mcp-jetbrains -Agents CSV install.ps1 mcp-context7 -Agents CSV [-Context7Key KEY] @@ -35,6 +35,11 @@ accepted (e.g. "claude"). Required by every subcommand. Valid values: claude, codex, opencode, junie. +.PARAMETER Scope + Skills install scope: "global" (default) writes to the per-agent user-home + dir; "local" writes to the matching dir under the current project (e.g. + .\.claude\skills). Applies to the `skills` subcommand. + .PARAMETER Context7Key Context7 API key (mcp-context7). Prompted interactively when missing. @@ -56,6 +61,7 @@ param( [string]$Version = '', [string]$Ref = 'main', [string]$Agents = '', + [string]$Scope = '', [string]$Context7Key = '', [switch]$BackupExistingFiles ) @@ -318,24 +324,51 @@ function Initialize-Tarball { # skills install (global, per agent) # ================================================================= +function Resolve-Scope { + param([string]$Scope) + if ([string]::IsNullOrWhiteSpace($Scope)) { return 'global' } + switch ($Scope) { + 'global' { return 'global' } + 'local' { return 'local' } + default { Write-ErrAndExit "skills: -Scope must be 'global' or 'local' (got '$Scope')" } + } +} + function Get-SkillsTarget { - param([string]$Agent) - switch ($Agent) { - 'claude' { Join-Path $HOME '.claude/skills' } - 'codex' { Join-Path $HOME '.codex/skills' } - 'opencode' { Join-Path $HOME '.config/opencode/skills' } - 'junie' { Join-Path $HOME '.junie/skills' } - default { throw "unknown agent '$Agent'" } + param( + [string]$Agent, + [string]$Scope = 'global' + ) + if ($Scope -eq 'local') { + $base = (Get-Location).Path + switch ($Agent) { + 'claude' { Join-Path $base '.claude/skills' } + 'codex' { Join-Path $base '.codex/skills' } + 'opencode' { Join-Path $base '.opencode/skills' } + 'junie' { Join-Path $base '.junie/skills' } + default { throw "unknown agent '$Agent'" } + } + } else { + switch ($Agent) { + 'claude' { Join-Path $HOME '.claude/skills' } + 'codex' { Join-Path $HOME '.codex/skills' } + 'opencode' { Join-Path $HOME '.config/opencode/skills' } + 'junie' { Join-Path $HOME '.junie/skills' } + default { throw "unknown agent '$Agent'" } + } } } function Install-SkillsForAgent { - param([string]$Agent) - $target = Get-SkillsTarget -Agent $Agent + param( + [string]$Agent, + [string]$Scope = 'global' + ) + $target = Get-SkillsTarget -Agent $Agent -Scope $Scope $label = Get-AgentLabel -Agent $Agent Write-Info '' - Write-Info "Installing skills for $label into $target" + Write-Info "Installing $Scope skills for $label into $target" if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null } @@ -351,12 +384,13 @@ function Install-SkillsForAgent { function Invoke-CmdSkills { $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'skills' + $resolvedScope = Resolve-Scope -Scope $Scope Initialize-Tarball foreach ($a in $agents) { - Install-SkillsForAgent -Agent $a + Install-SkillsForAgent -Agent $a -Scope $resolvedScope } Write-Info '' - Write-Info "Done. Installed skills for: $($agents -join ', ')" + Write-Info "Done. Installed $resolvedScope skills for: $($agents -join ', ')" } # ================================================================= @@ -660,13 +694,15 @@ function Invoke-Wizard { } # Step 1: skills - $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills globally?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills?' -Options $script:AllAgents if ($sel[0] -ne 'skip') { + $scopeAnswer = Read-Prompt -Message 'Install scope: (g)lobal user home or (l)ocal project dir' -Default 'g' + $resolvedScope = if ($scopeAnswer -match '^(l|local)$') { 'local' } else { 'global' } Initialize-Tarball foreach ($a in $sel) { - try { Install-SkillsForAgent -Agent $a } catch { Write-Info "error: $($_.Exception.Message)" } + try { Install-SkillsForAgent -Agent $a -Scope $resolvedScope } catch { Write-Info "error: $($_.Exception.Message)" } } - $summaryStrings.skills = $sel -join ', ' + $summaryStrings.skills = "$($sel -join ', ') ($resolvedScope)" } # Step 2: agents-md diff --git a/install.sh b/install.sh index f27430d..19c6d81 100755 --- a/install.sh +++ b/install.sh @@ -2,7 +2,7 @@ # Jmix AI Agent Guidelines installer. # # Default (no subcommand) launches an interactive wizard that guides through: -# 1. Installing Jmix skills globally for one or all agents. +# 1. Installing Jmix skills (globally or into the project) for one or all agents. # 2. Adding project-level guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md). # 3. Registering the JetBrains MCP server with the agent. # 4. Registering the Context7 MCP server with the agent. @@ -152,7 +152,7 @@ Jmix AI Agent Guidelines installer. Usage: install.sh [--version V] [--ref REF] # interactive wizard - install.sh skills [options] # install global skills only + install.sh skills [options] [--scope global|local] # install skills install.sh agents-md [options] # install project guidelines install.sh mcp-jetbrains [options] # register JetBrains MCP install.sh mcp-context7 [options] [--context7-key KEY] # register Context7 MCP @@ -172,6 +172,12 @@ Common options: Off by default. -h, --help Show this help. +skills options: + --scope global|local Where to install skills. "global" (default) writes to + the per-agent user-home dir; "local" writes to the + matching dir under the current project (e.g. + ./.claude/skills). + mcp-context7 options: --context7-key K Context7 API key. Prompted interactively when missing. @@ -302,16 +308,42 @@ ensure_tarball() { # skills install (global, per agent) # ================================================================= -skills_target_for_agent() { - case "$1" in - claude) printf '%s' "${HOME}/.claude/skills" ;; - codex) printf '%s' "${HOME}/.codex/skills" ;; - opencode) printf '%s' "${HOME}/.config/opencode/skills" ;; - junie) printf '%s' "${HOME}/.junie/skills" ;; - *) die "unknown agent '$1'" ;; +# Validates the install scope. Emits the normalized value ("global"/"local"). +# $1 - raw scope string (may be empty -> defaults to global) +parse_scope() { + case "${1:-global}" in + global|local) printf '%s' "${1:-global}" ;; + *) die "skills: --scope must be 'global' or 'local' (got '$1')" ;; esac } +# Resolves the skills target dir for an agent. +# $1 - agent +# $2 - scope: "global" (user home, default) or "local" (project working dir) +skills_target_for_agent() { + local agent="$1" + local scope="${2:-global}" + if [ "$scope" = "local" ]; then + local base + base="$(pwd -P)" + case "$agent" in + claude) printf '%s/.claude/skills' "$base" ;; + codex) printf '%s/.codex/skills' "$base" ;; + opencode) printf '%s/.opencode/skills' "$base" ;; + junie) printf '%s/.junie/skills' "$base" ;; + *) die "unknown agent '$agent'" ;; + esac + else + case "$agent" in + claude) printf '%s' "${HOME}/.claude/skills" ;; + codex) printf '%s' "${HOME}/.codex/skills" ;; + opencode) printf '%s' "${HOME}/.config/opencode/skills" ;; + junie) printf '%s' "${HOME}/.junie/skills" ;; + *) die "unknown agent '$agent'" ;; + esac + fi +} + agent_label() { case "$1" in claude) printf 'Claude Code' ;; @@ -324,13 +356,14 @@ agent_label() { install_skills_for_agent() { local agent="$1" + local scope="${2:-global}" local target_dir - target_dir="$(skills_target_for_agent "$agent")" + target_dir="$(skills_target_for_agent "$agent" "$scope")" local label label="$(agent_label "$agent")" log "" - log "Installing skills for ${label} into ${target_dir}" + log "Installing ${scope} skills for ${label} into ${target_dir}" mkdir -p "$target_dir" || die "cannot write to ${target_dir}: mkdir failed" local count=0 @@ -347,12 +380,16 @@ install_skills_for_agent() { cmd_skills() { local agents_csv="" + local scope="global" while [ $# -gt 0 ]; do case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" agents_csv="$2"; shift 2 ;; + --scope) + [ $# -ge 2 ] || die "--scope requires an argument" + scope="$2"; shift 2 ;; --backup-existing-files) BACKUP_EXISTING=1; shift ;; --version) @@ -368,15 +405,16 @@ cmd_skills() { local agents agents="$(parse_agents_csv "$agents_csv" "skills")" + scope="$(parse_scope "$scope")" ensure_tarball local agent for agent in $agents; do - install_skills_for_agent "$agent" + install_skills_for_agent "$agent" "$scope" done log "" - log "Done. Installed skills for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" + log "Done. Installed ${scope} skills for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" } # ================================================================= @@ -784,14 +822,17 @@ cmd_wizard() { # Step 1: skills local sel - sel="$(wizard_pick_agent '[1/5] Install Jmix skills globally?' $ALL_AGENTS)" + sel="$(wizard_pick_agent '[1/5] Install Jmix skills?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then + local scope_answer scope="global" + scope_answer="$(prompt 'Install scope: (g)lobal user home or (l)ocal project dir' 'g')" + case "$scope_answer" in l|L|local|LOCAL) scope="local" ;; esac ensure_tarball local agent for agent in $sel; do - install_skills_for_agent "$agent" || true + install_skills_for_agent "$agent" "$scope" || true done - summary_skills="$sel" + summary_skills="$sel (${scope})" fi # Step 2: agents-md From b7a76f43547b40696064e48cf193c57ffaa03949 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 15:26:49 +0400 Subject: [PATCH 14/36] Skills installation scope --- .studio/studio-meta-data.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 30f3317..2ea50a1 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -8,17 +8,17 @@ "inputs": [ { "id": "skillsScope", - "label": "Install to", + "label": "Installation scope", "type": "options", "choices": [ { "value": "local", - "label": "Project (local)", + "label": "Local (project)", "default": true }, { "value": "global", - "label": "User home (global)" + "label": "Global (user home)" } ] }, From 83184cc9917b7a495e8b062a75dc5b1c81fdf21d Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 15:31:07 +0400 Subject: [PATCH 15/36] Fixes --- .studio/studio-meta-data.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 2ea50a1..28f362c 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -24,7 +24,7 @@ }, { "id": "skillsAgents", - "label": "Targets", + "label": "Agents", "type": "options", "multi": true, "choices": [ @@ -59,11 +59,11 @@ }, { "id": "guidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / .junie/guidelines.md) to the project root for the selected agents. Click Skip to leave the project as is.", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents. Click Skip to leave the project as is.", "inputs": [ { "id": "guidelinesAgents", - "label": "Targets", + "label": "Agents", "type": "options", "multi": true, "choices": [ @@ -102,7 +102,7 @@ "inputs": [ { "id": "jetbrainsMcpAgents", - "label": "Targets", + "label": "Agents", "type": "options", "multi": true, "choices": [ @@ -141,7 +141,7 @@ "inputs": [ { "id": "context7Agents", - "label": "Targets", + "label": "Agents", "type": "options", "multi": true, "choices": [ @@ -188,7 +188,7 @@ "inputs": [ { "id": "playwrightAgents", - "label": "Targets", + "label": "Agents", "type": "options", "multi": true, "choices": [ From 8ba78792e1e0e1c7b92e259e8df089cb3e211151 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 19:00:11 +0400 Subject: [PATCH 16/36] skills-manifest.json --- .github/workflows/skills-manifest.yml | 34 +++++++++++ .studio/gen_skills_manifest.py | 87 +++++++++++++++++++++++++++ .studio/skills-manifest.json | 35 +++++++++++ 3 files changed, 156 insertions(+) create mode 100644 .github/workflows/skills-manifest.yml create mode 100644 .studio/gen_skills_manifest.py create mode 100644 .studio/skills-manifest.json diff --git a/.github/workflows/skills-manifest.yml b/.github/workflows/skills-manifest.yml new file mode 100644 index 0000000..7514a12 --- /dev/null +++ b/.github/workflows/skills-manifest.yml @@ -0,0 +1,34 @@ +name: Update skills manifest + +on: + push: + branches: [ main ] + paths: + - 'v*/skills/**' + - '.studio/gen_skills_manifest.py' + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-manifest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Generate manifest + run: python3 .studio/gen_skills_manifest.py + - name: Commit if changed + run: | + if git diff --quiet -- .studio/skills-manifest.json; then + echo "manifest unchanged" + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add .studio/skills-manifest.json + git commit -m "chore: update skills-manifest.json [skip ci]" + git push + fi diff --git a/.studio/gen_skills_manifest.py b/.studio/gen_skills_manifest.py new file mode 100644 index 0000000..0e3bde0 --- /dev/null +++ b/.studio/gen_skills_manifest.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Generate .studio/skills-manifest.json (version -> {skills[], sha256}). + +The aggregate hash is byte-identical to SkillsHasher.kt in jmix-studio: +for each listed skill folder, walk files; entry = relpath(UTF-8) + 0x00 + bytes, +relpath is POSIX-separated and relative to the version's skills/ dir; sort +entries by relpath bytes; SHA-256 over the concatenation; lowercase hex. +""" +import hashlib +import json +import os + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Per-scope agent skill directories (relative to the scope root: the user home +# for "global", the project base for "local"). Mirrors install.sh / install.ps1. +# Studio reads these from the manifest instead of hardcoding paths. +AGENTS_DIRS = { + "global": [ + ".claude/skills", + ".codex/skills", + ".config/opencode/skills", + ".junie/skills", + ], + "local": [ + ".claude/skills", + ".codex/skills", + ".opencode/skills", + ".junie/skills", + ], +} + + +def list_skill_names(skills_dir): + return sorted( + name for name in os.listdir(skills_dir) + if os.path.isdir(os.path.join(skills_dir, name)) + ) + + +def aggregate_hash(skills_dir, skill_names): + entries = [] + for name in sorted(skill_names): + base = os.path.join(skills_dir, name) + if not os.path.isdir(base): + continue + for current, _dirs, files in os.walk(base): + for filename in files: + full = os.path.join(current, filename) + rel = os.path.relpath(full, skills_dir).replace(os.sep, "/") + with open(full, "rb") as f: + entries.append((rel, f.read())) + entries.sort(key=lambda e: e[0].encode("utf-8")) + digest = hashlib.sha256() + for rel, data in entries: + digest.update(rel.encode("utf-8")) + digest.update(b"\x00") + digest.update(data) + return digest.hexdigest() + + +def build_manifest(): + versions = {} + for entry in sorted(os.listdir(REPO_ROOT)): + if not entry.startswith("v"): + continue + skills_dir = os.path.join(REPO_ROOT, entry, "skills") + if not os.path.isdir(skills_dir): + continue + names = list_skill_names(skills_dir) + versions[entry] = {"skills": names, "sha256": aggregate_hash(skills_dir, names)} + return {"schemaVersion": 1, "agents-dirs": AGENTS_DIRS, "versions": versions} + + +def main(): + manifest = build_manifest() + out_dir = os.path.join(REPO_ROOT, ".studio") + os.makedirs(out_dir, exist_ok=True) + out_path = os.path.join(out_dir, "skills-manifest.json") + text = json.dumps(manifest, indent=2, ensure_ascii=False, sort_keys=True) + "\n" + with open(out_path, "w", encoding="utf-8") as f: + f.write(text) + print("wrote " + out_path) + + +if __name__ == "__main__": + main() diff --git a/.studio/skills-manifest.json b/.studio/skills-manifest.json new file mode 100644 index 0000000..44e3c7e --- /dev/null +++ b/.studio/skills-manifest.json @@ -0,0 +1,35 @@ +{ + "agents-dirs": { + "global": [ + ".claude/skills", + ".codex/skills", + ".config/opencode/skills", + ".junie/skills" + ], + "local": [ + ".claude/skills", + ".codex/skills", + ".opencode/skills", + ".junie/skills" + ] + }, + "schemaVersion": 1, + "versions": { + "v2": { + "sha256": "d2d53b6cc53b623c51d05611fc48908342d4cdda2afeefe5289d69eac3c4c2a7", + "skills": [ + "jmix-dto", + "jmix-entities", + "jmix-enums", + "jmix-fetch-plans", + "jmix-fragments", + "jmix-i18n", + "jmix-liquibase", + "jmix-security-roles", + "jmix-services", + "jmix-testing", + "jmix-views" + ] + } + } +} From 74812ae2158a031f5cb2818e9553575ab1cf4d1a Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 19:16:36 +0400 Subject: [PATCH 17/36] Add
to messages --- .studio/studio-meta-data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 28f362c..9cd378e 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,7 +4,7 @@ "steps": [ { "id": "skills", - "message": "Install Jmix skills for the selected agents. Choose whether to install into this project or the user home. Click Skip to opt out.", + "message": "Install Jmix skills for the selected agents. Choose whether to install into this project or the user home.
Click Skip to opt out.", "inputs": [ { "id": "skillsScope", @@ -59,7 +59,7 @@ }, { "id": "guidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents. Click Skip to leave the project as is.", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", "inputs": [ { "id": "guidelinesAgents", @@ -98,7 +98,7 @@ }, { "id": "jetbrainsMcp", - "message": "Register the JetBrains MCP server with the selected agents. Recommended for IntelliJ users. Click Skip to opt out.", + "message": "Register the JetBrains MCP server with the selected agents. Recommended for IntelliJ users.
Click Skip to opt out.", "inputs": [ { "id": "jetbrainsMcpAgents", @@ -137,7 +137,7 @@ }, { "id": "context7", - "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents. Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", + "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", "inputs": [ { "id": "context7Agents", @@ -184,7 +184,7 @@ }, { "id": "playwright", - "message": "Install Playwright testing skills for the selected agents. Requires npm to be installed on PATH. Click Skip to opt out.", + "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", "inputs": [ { "id": "playwrightAgents", From 7b8dc8f8d360996a0c3a4529c64d1bcf48fd430d Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 19:44:55 +0400 Subject: [PATCH 18/36] Improve messages --- .studio/studio-meta-data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 9cd378e..aff4618 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,7 +4,7 @@ "steps": [ { "id": "skills", - "message": "Install Jmix skills for the selected agents. Choose whether to install into this project or the user home.
Click Skip to opt out.", + "message": "Install Jmix skills for the selected agents.
Choose whether to install into this project or the user home.
Click Skip to opt out.", "inputs": [ { "id": "skillsScope", @@ -59,7 +59,7 @@ }, { "id": "guidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", "inputs": [ { "id": "guidelinesAgents", @@ -98,7 +98,7 @@ }, { "id": "jetbrainsMcp", - "message": "Register the JetBrains MCP server with the selected agents. Recommended for IntelliJ users.
Click Skip to opt out.", + "message": "Register the JetBrains MCP server with the selected agents.
Click Skip to opt out.", "inputs": [ { "id": "jetbrainsMcpAgents", @@ -137,7 +137,7 @@ }, { "id": "context7", - "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", + "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", "inputs": [ { "id": "context7Agents", @@ -184,7 +184,7 @@ }, { "id": "playwright", - "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", + "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", "inputs": [ { "id": "playwrightAgents", From 18c24df21f7ec3f00b9bd0c737d11aa1bc77b2c7 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Wed, 20 May 2026 19:48:49 +0400 Subject: [PATCH 19/36] Improve messages --- .studio/studio-meta-data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index aff4618..7142a3f 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,7 +4,7 @@ "steps": [ { "id": "skills", - "message": "Install Jmix skills for the selected agents.
Choose whether to install into this project or the user home.
Click Skip to opt out.", + "message": "Install Jmix skills for the selected agents.
Choose whether to install into this project or the user home.
Click Skip to opt out.", "inputs": [ { "id": "skillsScope", @@ -59,7 +59,7 @@ }, { "id": "guidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", + "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", "inputs": [ { "id": "guidelinesAgents", @@ -98,7 +98,7 @@ }, { "id": "jetbrainsMcp", - "message": "Register the JetBrains MCP server with the selected agents.
Click Skip to opt out.", + "message": "Register the JetBrains MCP server with the selected agents.
Click Skip to opt out.", "inputs": [ { "id": "jetbrainsMcpAgents", @@ -137,7 +137,7 @@ }, { "id": "context7", - "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or click Skip to opt out. The key is sent to the MCP CLI and never stored by Studio.", + "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or Click Skip to opt out.
The key is sent to the MCP CLI and never stored by Studio.", "inputs": [ { "id": "context7Agents", @@ -184,7 +184,7 @@ }, { "id": "playwright", - "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", + "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", "inputs": [ { "id": "playwrightAgents", From be5e94e8cf9c07514ae4defc9a45c50c38761cfe Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 12:42:55 +0400 Subject: [PATCH 20/36] Improve messages --- .studio/studio-meta-data.json | 5 +++++ .studio/studio-meta-data.schema.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 7142a3f..7d75ca2 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -4,6 +4,7 @@ "steps": [ { "id": "skills", + "title": "Skills", "message": "Install Jmix skills for the selected agents.
Choose whether to install into this project or the user home.
Click Skip to opt out.", "inputs": [ { @@ -59,6 +60,7 @@ }, { "id": "guidelines", + "title": "Guidelines", "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", "inputs": [ { @@ -98,6 +100,7 @@ }, { "id": "jetbrainsMcp", + "title": "JetBrains MCP", "message": "Register the JetBrains MCP server with the selected agents.
Click Skip to opt out.", "inputs": [ { @@ -137,6 +140,7 @@ }, { "id": "context7", + "title": "Context7 MCP", "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or Click Skip to opt out.
The key is sent to the MCP CLI and never stored by Studio.", "inputs": [ { @@ -184,6 +188,7 @@ }, { "id": "playwright", + "title": "Playwright", "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", "inputs": [ { diff --git a/.studio/studio-meta-data.schema.json b/.studio/studio-meta-data.schema.json index 62fed77..2becb13 100644 --- a/.studio/studio-meta-data.schema.json +++ b/.studio/studio-meta-data.schema.json @@ -33,6 +33,7 @@ "description": "A single step of the install flow. Steps may collect multiple inputs and optionally execute a shell command.", "additionalProperties": false, "required": [ + "title", "message" ], "properties": { @@ -41,6 +42,10 @@ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "description": "Optional step identifier used for diagnostics and per-step state restoration. Command substitution uses input ids, not step ids." }, + "title": { + "type": "string", + "description": "Main step title shown on the top." + }, "message": { "type": "string", "description": "Header text shown above the inputs." From cb86d7870e2b69a583b825d55a7022c9fd010a70 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 15:28:03 +0400 Subject: [PATCH 21/36] Fix agent skill symlink creation --- .studio/gen_skills_manifest.py | 31 ++------ .studio/skills-manifest.json | 18 +---- README.md | 7 +- install.ps1 | 126 +++++++++++++++++++----------- install.sh | 137 ++++++++++++++++++++------------- 5 files changed, 181 insertions(+), 138 deletions(-) diff --git a/.studio/gen_skills_manifest.py b/.studio/gen_skills_manifest.py index 0e3bde0..18646f0 100644 --- a/.studio/gen_skills_manifest.py +++ b/.studio/gen_skills_manifest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Generate .studio/skills-manifest.json (version -> {skills[], sha256}). -The aggregate hash is byte-identical to SkillsHasher.kt in jmix-studio: +The aggregate hash is byte-identical to jmix-studio logic: for each listed skill folder, walk files; entry = relpath(UTF-8) + 0x00 + bytes, relpath is POSIX-separated and relative to the version's skills/ dir; sort entries by relpath bytes; SHA-256 over the concatenation; lowercase hex. @@ -12,32 +12,20 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -# Per-scope agent skill directories (relative to the scope root: the user home -# for "global", the project base for "local"). Mirrors install.sh / install.ps1. -# Studio reads these from the manifest instead of hardcoding paths. -AGENTS_DIRS = { - "global": [ - ".claude/skills", - ".codex/skills", - ".config/opencode/skills", - ".junie/skills", - ], - "local": [ - ".claude/skills", - ".codex/skills", - ".opencode/skills", - ".junie/skills", - ], +# Canonical per-scope skill store (relative to the scope root: the user home for +# "global", the project base for "local"). The global store appends /v. +# Studio reads this from the manifest; install.sh / install.ps1 mirror it. +STORE = { + "global": ".agents/.jmix/skills", + "local": ".skills", } - def list_skill_names(skills_dir): return sorted( name for name in os.listdir(skills_dir) if os.path.isdir(os.path.join(skills_dir, name)) ) - def aggregate_hash(skills_dir, skill_names): entries = [] for name in sorted(skill_names): @@ -58,7 +46,6 @@ def aggregate_hash(skills_dir, skill_names): digest.update(data) return digest.hexdigest() - def build_manifest(): versions = {} for entry in sorted(os.listdir(REPO_ROOT)): @@ -69,8 +56,7 @@ def build_manifest(): continue names = list_skill_names(skills_dir) versions[entry] = {"skills": names, "sha256": aggregate_hash(skills_dir, names)} - return {"schemaVersion": 1, "agents-dirs": AGENTS_DIRS, "versions": versions} - + return {"schemaVersion": 1, "store": STORE, "versions": versions} def main(): manifest = build_manifest() @@ -82,6 +68,5 @@ def main(): f.write(text) print("wrote " + out_path) - if __name__ == "__main__": main() diff --git a/.studio/skills-manifest.json b/.studio/skills-manifest.json index 44e3c7e..598f813 100644 --- a/.studio/skills-manifest.json +++ b/.studio/skills-manifest.json @@ -1,19 +1,9 @@ { - "agents-dirs": { - "global": [ - ".claude/skills", - ".codex/skills", - ".config/opencode/skills", - ".junie/skills" - ], - "local": [ - ".claude/skills", - ".codex/skills", - ".opencode/skills", - ".junie/skills" - ] - }, "schemaVersion": 1, + "store": { + "global": ".agents/.jmix/skills", + "local": ".skills" + }, "versions": { "v2": { "sha256": "d2d53b6cc53b623c51d05611fc48908342d4cdda2afeefe5289d69eac3c4c2a7", diff --git a/README.md b/README.md index c6b8774..d063efa 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The AI agent will use these resources to understand Jmix-specific patterns, mand - `SKILL.md`: Detailed instructions and rules for the agent regarding a specific Jmix feature. - Optional subdirectories with examples or other materials. -## Automatic Installation +## Quick Install A single command launches an interactive wizard that walks through every setup step: installing global skills for one or more agents, adding the project guideline file (`CLAUDE.md` / `AGENTS.md` / `.junie/guidelines.md`) to the project root, and registering the JetBrains and Context7 MCP servers. @@ -54,7 +54,7 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i | `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | | `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | | `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | -| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs into the per-agent user-home dir; `local` installs into the project (e.g. `./.claude/skills`). | +| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | | `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | | `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | @@ -77,7 +77,8 @@ Copy the `AGENTS.md` file from this repository to the root of your Jmix applicat The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). These should be made available to the agent globally. -Copy or symlink the content of the `skills/` subdirectory to the folder recognized by your agent in your home directory: +- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`), symlinked from `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). +- **Local:** store at `/.skills/`, symlinked from `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. | Agent | Skills Folder Path | |:------------|:-----------------------------| diff --git a/install.ps1 b/install.ps1 index 6465113..5924d83 100644 --- a/install.ps1 +++ b/install.ps1 @@ -12,6 +12,8 @@ Subcommands are available for non-interactive use: install.ps1 skills -Agents CSV [-Scope global|local] [-Version V] [-Ref REF] + Installs skills into a canonical store once, then symlinks each + selected agent's skills dir to that store. install.ps1 agents-md -Agents CSV [-Version V] [-Ref REF] install.ps1 mcp-jetbrains -Agents CSV install.ps1 mcp-context7 -Agents CSV [-Context7Key KEY] @@ -334,63 +336,81 @@ function Resolve-Scope { } } -function Get-SkillsTarget { - param( - [string]$Agent, - [string]$Scope = 'global' - ) - if ($Scope -eq 'local') { - $base = (Get-Location).Path - switch ($Agent) { - 'claude' { Join-Path $base '.claude/skills' } - 'codex' { Join-Path $base '.codex/skills' } - 'opencode' { Join-Path $base '.opencode/skills' } - 'junie' { Join-Path $base '.junie/skills' } - default { throw "unknown agent '$Agent'" } - } - } else { - switch ($Agent) { - 'claude' { Join-Path $HOME '.claude/skills' } - 'codex' { Join-Path $HOME '.codex/skills' } - 'opencode' { Join-Path $HOME '.config/opencode/skills' } - 'junie' { Join-Path $HOME '.junie/skills' } - default { throw "unknown agent '$Agent'" } - } +function Get-AgentSymlinkRel { + param([string]$Agent) + switch ($Agent) { + 'claude' { '.claude/skills' } + 'codex' { '.agents/skills' } + 'opencode' { '.agents/skills' } + 'junie' { '.junie/skills' } + default { throw "unknown agent '$Agent'" } } } -function Install-SkillsForAgent { - param( - [string]$Agent, - [string]$Scope = 'global' - ) - $target = Get-SkillsTarget -Agent $Agent -Scope $Scope - $label = Get-AgentLabel -Agent $Agent - - Write-Info '' - Write-Info "Installing $Scope skills for $label into $target" - if (-not (Test-Path $target)) { - New-Item -ItemType Directory -Path $target -Force | Out-Null +# Creates/refreshes a whole-dir symlink $Link -> $Target. Replaces an existing +# symlink; an existing real dir is backed up (when -BackupExistingFiles) or removed. +# Requires symlink privileges; fails with guidance otherwise. +function New-DirSymlink { + param([string]$Link, [string]$Target) + if (Test-Path $Link) { + $item = Get-Item $Link -Force + if ($item.LinkType) { + Remove-Item $Link -Force + } elseif ($BackupExistingFiles) { + Rename-Item -Path $Link -NewName "$([System.IO.Path]::GetFileName($Link)).bak-$($script:Timestamp)" + } else { + Remove-Item $Link -Recurse -Force + } + } + $parent = Split-Path -Parent $Link + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } + try { + New-Item -ItemType SymbolicLink -Path $Link -Target $Target -ErrorAction Stop | Out-Null + } catch { + Write-ErrAndExit "cannot create symlink $Link -> $Target. Enable Windows Developer Mode or run as Administrator to allow symlinks." } +} - $count = 0 +function Install-SkillsToStore { + param([string]$StoreDir) + Write-Info '' + Write-Info "Installing skills into store $StoreDir" + if (-not (Test-Path $StoreDir)) { New-Item -ItemType Directory -Path $StoreDir -Force | Out-Null } foreach ($skill in Get-ChildItem -Path $script:SourceSkillsDir -Directory) { - $dest = Join-Path $target $skill.Name + $dest = Join-Path $StoreDir $skill.Name Write-Dest -Src $skill.FullName -Dest $dest -Label $skill.Name - $count++ } - Write-Info " $count skill(s) processed for $label" } function Invoke-CmdSkills { $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'skills' $resolvedScope = Resolve-Scope -Scope $Scope Initialize-Tarball + + if ($resolvedScope -eq 'local') { + $root = (Get-Location).Path + $storeDir = Join-Path $root '.skills' + } else { + $root = $HOME + $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) + } + + Install-SkillsToStore -StoreDir $storeDir + + Write-Info '' + Write-Info 'Linking agent skill dirs to the store' + $seen = @{} foreach ($a in $agents) { - Install-SkillsForAgent -Agent $a -Scope $resolvedScope + $rel = Get-AgentSymlinkRel -Agent $a + if ($seen.ContainsKey($rel)) { continue } + $seen[$rel] = $true + $link = Join-Path $root $rel + New-DirSymlink -Link $link -Target $storeDir + Write-Info " Linked: $link -> $storeDir" } + Write-Info '' - Write-Info "Done. Installed $resolvedScope skills for: $($agents -join ', ')" + Write-Info "Done. Installed $resolvedScope skills store at $storeDir and linked: $($agents -join ', ')" } # ================================================================= @@ -627,7 +647,7 @@ function Install-PlaywrightForAgents { foreach ($agent in $Agents) { if ($agent -eq 'claude') { continue } - $target = Get-SkillsTarget -Agent $agent + $target = Join-Path $HOME (Get-AgentSymlinkRel -Agent $agent) if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null } @@ -699,9 +719,27 @@ function Invoke-Wizard { $scopeAnswer = Read-Prompt -Message 'Install scope: (g)lobal user home or (l)ocal project dir' -Default 'g' $resolvedScope = if ($scopeAnswer -match '^(l|local)$') { 'local' } else { 'global' } Initialize-Tarball - foreach ($a in $sel) { - try { Install-SkillsForAgent -Agent $a -Scope $resolvedScope } catch { Write-Info "error: $($_.Exception.Message)" } - } + try { + if ($resolvedScope -eq 'local') { + $wizRoot = (Get-Location).Path + $wizStoreDir = Join-Path $wizRoot '.skills' + } else { + $wizRoot = $HOME + $wizStoreDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) + } + Install-SkillsToStore -StoreDir $wizStoreDir + Write-Info '' + Write-Info 'Linking agent skill dirs to the store' + $wizSeen = @{} + foreach ($a in $sel) { + $rel = Get-AgentSymlinkRel -Agent $a + if ($wizSeen.ContainsKey($rel)) { continue } + $wizSeen[$rel] = $true + $link = Join-Path $wizRoot $rel + New-DirSymlink -Link $link -Target $wizStoreDir + Write-Info " Linked: $link -> $wizStoreDir" + } + } catch { Write-Info "error: $($_.Exception.Message)" } $summaryStrings.skills = "$($sel -join ', ') ($resolvedScope)" } diff --git a/install.sh b/install.sh index 19c6d81..2e1048b 100755 --- a/install.sh +++ b/install.sh @@ -152,7 +152,7 @@ Jmix AI Agent Guidelines installer. Usage: install.sh [--version V] [--ref REF] # interactive wizard - install.sh skills [options] [--scope global|local] # install skills + install.sh skills [--agents CSV] [--scope global|local] # install skills into the canonical store and symlink agent dirs install.sh agents-md [options] # install project guidelines install.sh mcp-jetbrains [options] # register JetBrains MCP install.sh mcp-context7 [options] [--context7-key KEY] # register Context7 MCP @@ -317,65 +317,62 @@ parse_scope() { esac } -# Resolves the skills target dir for an agent. -# $1 - agent -# $2 - scope: "global" (user home, default) or "local" (project working dir) -skills_target_for_agent() { - local agent="$1" - local scope="${2:-global}" - if [ "$scope" = "local" ]; then - local base - base="$(pwd -P)" - case "$agent" in - claude) printf '%s/.claude/skills' "$base" ;; - codex) printf '%s/.codex/skills' "$base" ;; - opencode) printf '%s/.opencode/skills' "$base" ;; - junie) printf '%s/.junie/skills' "$base" ;; - *) die "unknown agent '$agent'" ;; - esac - else - case "$agent" in - claude) printf '%s' "${HOME}/.claude/skills" ;; - codex) printf '%s' "${HOME}/.codex/skills" ;; - opencode) printf '%s' "${HOME}/.config/opencode/skills" ;; - junie) printf '%s' "${HOME}/.junie/skills" ;; - *) die "unknown agent '$agent'" ;; - esac - fi -} - -agent_label() { +# Relative skills dir each agent reads, used as a whole-dir symlink to the store. +# claude -> .claude/skills ; codex & opencode -> .agents/skills (open standard) ; +# junie -> .junie/skills. Rooted at $HOME (global) or the project dir (local). +agent_symlink_rel() { case "$1" in - claude) printf 'Claude Code' ;; - codex) printf 'Codex' ;; - opencode) printf 'OpenCode' ;; - junie) printf 'Junie' ;; - *) printf '%s' "$1" ;; + claude) printf '.claude/skills' ;; + codex|opencode) printf '.agents/skills' ;; + junie) printf '.junie/skills' ;; + *) die "unknown agent '$1'" ;; esac } -install_skills_for_agent() { - local agent="$1" - local scope="${2:-global}" - local target_dir - target_dir="$(skills_target_for_agent "$agent" "$scope")" - local label - label="$(agent_label "$agent")" +# Creates (or refreshes) a whole-dir symlink $1 -> $2. Replaces an existing +# symlink; an existing real dir is backed up (when --backup-existing-files) or +# removed. Requires symlink support; fails otherwise. +create_symlink() { + local link="$1" + local target="$2" + if [ -L "$link" ]; then + rm -f "$link" || die "cannot replace symlink ${link}" + elif [ -e "$link" ]; then + if [ "$BACKUP_EXISTING" -eq 1 ]; then + mv "$link" "${link}.bak-${TIMESTAMP}" || die "cannot back up ${link}" + else + rm -rf "$link" || die "cannot remove ${link}" + fi + fi + mkdir -p "$(dirname "$link")" || die "cannot create parent of ${link}" + ln -s "$target" "$link" \ + || die "cannot create symlink ${link} -> ${target}. Your filesystem/OS may not permit symlinks." +} +# Copies each source skill folder into the canonical store (overwrite or backup +# via write_dest). +install_skills_to_store() { + local store_dir="$1" log "" - log "Installing ${scope} skills for ${label} into ${target_dir}" - mkdir -p "$target_dir" || die "cannot write to ${target_dir}: mkdir failed" - - local count=0 + log "Installing skills into store ${store_dir}" + mkdir -p "$store_dir" || die "cannot create store ${store_dir}" local skill name dest for skill in "$SOURCE_SKILLS_DIR"/*/; do [ -d "$skill" ] || continue name="$(basename "$skill")" - dest="${target_dir}/${name}" + dest="${store_dir}/${name}" write_dest "$skill" "$dest" "$name" - count=$((count + 1)) done - log " ${count} skill(s) processed for ${label}" +} + +agent_label() { + case "$1" in + claude) printf 'Claude Code' ;; + codex) printf 'Codex' ;; + opencode) printf 'OpenCode' ;; + junie) printf 'Junie' ;; + *) printf '%s' "$1" ;; + esac } cmd_skills() { @@ -409,12 +406,33 @@ cmd_skills() { ensure_tarball - local agent + local root store_dir + if [ "$scope" = "local" ]; then + root="$(pwd -P)" + store_dir="${root}/.skills" + else + root="${HOME}" + store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" + fi + + install_skills_to_store "$store_dir" + + log "" + log "Linking agent skill dirs to the store" + local agent rel link seen=" " for agent in $agents; do - install_skills_for_agent "$agent" "$scope" + rel="$(agent_symlink_rel "$agent")" + case "$seen" in + *" ${rel} "*) continue ;; + esac + seen="${seen}${rel} " + link="${root}/${rel}" + create_symlink "$link" "$store_dir" + log " Linked: ${link} -> ${store_dir}" done + log "" - log "Done. Installed ${scope} skills for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" + log "Done. Installed ${scope} skills store at ${store_dir} and linked: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" } # ================================================================= @@ -732,7 +750,7 @@ cmd_playwright() { local agent target dest skill for agent in $agents; do [ "$agent" = "claude" ] && continue - target="$(skills_target_for_agent "$agent")" + target="${HOME}/$(agent_symlink_rel "$agent")" mkdir -p "$target" || die "cannot create ${target}" for skill in $new_skills; do [ -d "${claude_skills}/${skill}" ] || continue @@ -828,9 +846,20 @@ cmd_wizard() { scope_answer="$(prompt 'Install scope: (g)lobal user home or (l)ocal project dir' 'g')" case "$scope_answer" in l|L|local|LOCAL) scope="local" ;; esac ensure_tarball - local agent + local root store_dir + if [ "$scope" = "local" ]; then + root="$(pwd -P)"; store_dir="${root}/.skills" + else + root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" + fi + install_skills_to_store "$store_dir" || true + local agent rel link seen=" " for agent in $sel; do - install_skills_for_agent "$agent" "$scope" || true + rel="$(agent_symlink_rel "$agent")" + case "$seen" in *" ${rel} "*) continue ;; esac + seen="${seen}${rel} " + link="${root}/${rel}" + create_symlink "$link" "$store_dir" || true done summary_skills="$sel (${scope})" fi From cb5d6535d747976d3366c8c597c2effb873ef3ab Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 15:42:36 +0400 Subject: [PATCH 22/36] Update README and schema descriptions --- .studio/studio-meta-data.schema.json | 2 +- README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.studio/studio-meta-data.schema.json b/.studio/studio-meta-data.schema.json index 2becb13..eb65558 100644 --- a/.studio/studio-meta-data.schema.json +++ b/.studio/studio-meta-data.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/.studio/studio-meta-data.schema.json", "title": "Jmix Studio install-flow metadata", - "description": "Metadata consumed by Jmix Studio to drive the 'Configure AI Agents' wizard. Defines a sequence of multi-input steps; each step optionally executes a shell command assembled from collected input values.", + "description": "Metadata consumed by Jmix Studio to drive the AI Agent Toolkit wizard. Defines a sequence of multi-input steps; each step optionally executes a shell command assembled from collected input values.", "type": "object", "additionalProperties": true, "required": [ diff --git a/README.md b/README.md index d063efa..667dd1d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ The AI agent will use these resources to understand Jmix-specific patterns, mand ## Quick Install -A single command launches an interactive wizard that walks through every setup step: installing global skills for one or more agents, adding the project guideline file (`CLAUDE.md` / `AGENTS.md` / `.junie/guidelines.md`) to the project root, and registering the JetBrains and Context7 MCP servers. +A single command launches an interactive wizard that walks through every setup step: +installing skills, adding guidelines registering the recommended MCP servers. **macOS / Linux:** @@ -28,7 +29,7 @@ curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guideline iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex ``` -In Jmix Studio plugin, the same wizard is available from the **Configure AI Agents for Jmix** action. +In Jmix Studio plugin, the same wizard is available from the **Jmix AI Agents Toolkit** action. ### Non-Interactive Subcommands From a1dbba26d5fcf555c0f74d77eef7f7f5ce5771d3 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 15:50:12 +0400 Subject: [PATCH 23/36] Add emojis to messages --- .studio/studio-meta-data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 7d75ca2..591e0d2 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -5,7 +5,7 @@ { "id": "skills", "title": "Skills", - "message": "Install Jmix skills for the selected agents.
Choose whether to install into this project or the user home.
Click Skip to opt out.", + "message": "🛠️ Install Jmix skills for the selected agents in the preferred scope (local/global).
⏭️ Click Skip to opt out.", "inputs": [ { "id": "skillsScope", @@ -61,7 +61,7 @@ { "id": "guidelines", "title": "Guidelines", - "message": "Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
Click Skip to leave the project as is.", + "message": "📘 Add Jmix coding guidelines (CLAUDE.md / AGENTS.md / guidelines.md) to the project root for the selected agents.
⏭️ Click Skip to leave the project as is.", "inputs": [ { "id": "guidelinesAgents", @@ -101,7 +101,7 @@ { "id": "jetbrainsMcp", "title": "JetBrains MCP", - "message": "Register the JetBrains MCP server with the selected agents.
Click Skip to opt out.", + "message": "🔌 Register the JetBrains MCP server with the selected agents.
⏭️ Click Skip to opt out.", "inputs": [ { "id": "jetbrainsMcpAgents", @@ -141,7 +141,7 @@ { "id": "context7", "title": "Context7 MCP", - "message": "Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
Enter your Context7 API key below or Click Skip to opt out.
The key is sent to the MCP CLI and never stored by Studio.", + "message": "\uD83D\uDD0C Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
🔑 Enter your Context7 API key below.
🛡️ The key is sent to the MCP CLI and never stored by Studio.
⏭\uFE0F Click Skip to opt out.", "inputs": [ { "id": "context7Agents", @@ -189,7 +189,7 @@ { "id": "playwright", "title": "Playwright", - "message": "Install Playwright testing skills for the selected agents.
Requires npm to be installed on PATH.
Click Skip to opt out.", + "message": "\uD83D\uDD0C Install Playwright testing skills for the selected agents.
📦 Requires npm to be installed on PATH.
⏭️ Click Skip to opt out.", "inputs": [ { "id": "playwrightAgents", From 77f8d10a2b5c53be3982a999662e501fac326c71 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 15:53:56 +0400 Subject: [PATCH 24/36] Add emojis to labels --- .studio/studio-meta-data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 591e0d2..34787c2 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -25,7 +25,7 @@ }, { "id": "skillsAgents", - "label": "Agents", + "label": "🤖 Agents", "type": "options", "multi": true, "choices": [ @@ -65,7 +65,7 @@ "inputs": [ { "id": "guidelinesAgents", - "label": "Agents", + "label": "\uD83E\uDD16 Agents", "type": "options", "multi": true, "choices": [ @@ -105,7 +105,7 @@ "inputs": [ { "id": "jetbrainsMcpAgents", - "label": "Agents", + "label": "\uD83E\uDD16 Agents", "type": "options", "multi": true, "choices": [ @@ -145,7 +145,7 @@ "inputs": [ { "id": "context7Agents", - "label": "Agents", + "label": "\uD83E\uDD16 Agents", "type": "options", "multi": true, "choices": [ @@ -193,7 +193,7 @@ "inputs": [ { "id": "playwrightAgents", - "label": "Agents", + "label": "\uD83E\uDD16 Agents", "type": "options", "multi": true, "choices": [ From 93bbca79b29d920d7439859355d79db5622ccd79 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 15:59:54 +0400 Subject: [PATCH 25/36] Fixes --- install.ps1 | 10 ++++++---- install.sh | 17 ++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/install.ps1 b/install.ps1 index 5924d83..28b279f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -675,7 +675,8 @@ function Invoke-CmdPlaywright { function Read-AgentChoice { param( [string]$Label, - [string[]]$Options + [string[]]$Options, + [string]$Default = 'skip' ) Write-Info '' Write-Info $Label @@ -687,8 +688,9 @@ function Read-AgentChoice { Write-Output (" {0}) For all agents" -f $i) Write-Output ' s) Skip' - $answer = Read-Prompt -Message 'Choice' -Default 's' + $answer = Read-Prompt -Message 'Choice' -Default $Default if ($answer -match '^(s|skip)$') { return @('skip') } + if ($answer -match '^(a|all)$') { return $Options } if ($answer -notmatch '^\d+$') { Write-Info "Unrecognized choice '$answer'. Skipping." return @('skip') @@ -701,7 +703,7 @@ function Read-AgentChoice { } function Invoke-Wizard { - Write-Info '=== Jmix AI Agent Guidelines - Setup ===' + Write-Info '=== Jmix AI Agents Toolkit ===' if ($Version) { Write-Info "Jmix version: $Version" } Write-Info "Working directory: $((Get-Location).Path)" @@ -714,7 +716,7 @@ function Invoke-Wizard { } # Step 1: skills - $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills?' -Options $script:AllAgents -Default 'all' if ($sel[0] -ne 'skip') { $scopeAnswer = Read-Prompt -Message 'Install scope: (g)lobal user home or (l)ocal project dir' -Default 'g' $resolvedScope = if ($scopeAnswer -match '^(l|local)$') { 'local' } else { 'global' } diff --git a/install.sh b/install.sh index 2e1048b..97d449d 100755 --- a/install.sh +++ b/install.sh @@ -768,6 +768,8 @@ cmd_playwright() { # ================================================================= wizard_pick_agent() { + local default_choice="$1" + shift local prompt_label="$1" shift local options="$*" @@ -787,9 +789,10 @@ wizard_pick_agent() { local total=$((i)) local answer - answer="$(prompt 'Choice' 's')" + answer="$(prompt 'Choice' "$default_choice")" case "$answer" in s|S|skip|SKIP) printf 'skip'; return 0 ;; + a|A|all|ALL) printf '%s' "$options"; return 0 ;; esac if ! printf '%s' "$answer" | grep -Eq '^[0-9]+$'; then log "Unrecognized choice '${answer}'. Skipping." >&2 @@ -828,7 +831,7 @@ cmd_wizard() { esac done - log "=== Jmix AI Agent Guidelines - Setup ===" + log "=== Jmix AI Agents Toolkit ===" [ -n "$VERSION" ] && log "Jmix version: ${VERSION}" log "Working directory: $(pwd -P)" @@ -840,7 +843,7 @@ cmd_wizard() { # Step 1: skills local sel - sel="$(wizard_pick_agent '[1/5] Install Jmix skills?' $ALL_AGENTS)" + sel="$(wizard_pick_agent all '[1/5] Install Jmix skills?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then local scope_answer scope="global" scope_answer="$(prompt 'Install scope: (g)lobal user home or (l)ocal project dir' 'g')" @@ -865,7 +868,7 @@ cmd_wizard() { fi # Step 2: agents-md - sel="$(wizard_pick_agent '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" + sel="$(wizard_pick_agent skip '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then ensure_tarball @@ -880,7 +883,7 @@ cmd_wizard() { fi # Step 3: JetBrains MCP - sel="$(wizard_pick_agent '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" + sel="$(wizard_pick_agent skip '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" if [ "$sel" != "skip" ]; then local agent for agent in $sel; do @@ -890,7 +893,7 @@ cmd_wizard() { fi # Step 4: Context7 MCP - sel="$(wizard_pick_agent '[4/5] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" + sel="$(wizard_pick_agent skip '[4/5] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" if [ "$sel" != "skip" ]; then local key key="$(prompt 'Context7 API key' '')" @@ -907,7 +910,7 @@ cmd_wizard() { fi # Step 5: Playwright - sel="$(wizard_pick_agent '[5/5] Install Playwright? (requires npm)' $ALL_AGENTS)" + sel="$(wizard_pick_agent skip '[5/5] Install Playwright? (requires npm)' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then local pw_csv pw_csv="$(printf '%s' "$sel" | tr ' ' ',' | sed 's/^,//;s/,$//')" From c1bc3b43c7206cc85a1134502cbe8a759c2b898c Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 16:06:51 +0400 Subject: [PATCH 26/36] Fixes --- install.ps1 | 9 ++++----- install.sh | 15 +++++---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/install.ps1 b/install.ps1 index 28b279f..367dbf3 100644 --- a/install.ps1 +++ b/install.ps1 @@ -680,12 +680,12 @@ function Read-AgentChoice { ) Write-Info '' Write-Info $Label + Write-Output ' a) For all agents' $i = 1 foreach ($opt in $Options) { Write-Output (" {0}) {1}" -f $i, (Get-AgentLabel -Agent $opt)) $i++ } - Write-Output (" {0}) For all agents" -f $i) Write-Output ' s) Skip' $answer = Read-Prompt -Message 'Choice' -Default $Default @@ -696,7 +696,6 @@ function Read-AgentChoice { return @('skip') } $num = [int]$answer - if ($num -eq $i) { return $Options } if ($num -ge 1 -and $num -le $Options.Length) { return @($Options[$num - 1]) } Write-Info "Unrecognized choice '$answer'. Skipping." return @('skip') @@ -718,8 +717,8 @@ function Invoke-Wizard { # Step 1: skills $sel = Read-AgentChoice -Label '[1/5] Install Jmix skills?' -Options $script:AllAgents -Default 'all' if ($sel[0] -ne 'skip') { - $scopeAnswer = Read-Prompt -Message 'Install scope: (g)lobal user home or (l)ocal project dir' -Default 'g' - $resolvedScope = if ($scopeAnswer -match '^(l|local)$') { 'local' } else { 'global' } + $scopeAnswer = Read-Prompt -Message 'Install scope: (l)ocal project dir or (g)lobal user home' -Default 'l' + $resolvedScope = if ($scopeAnswer -match '^(g|global)$') { 'global' } else { 'local' } Initialize-Tarball try { if ($resolvedScope -eq 'local') { @@ -746,7 +745,7 @@ function Invoke-Wizard { } # Step 2: agents-md - $sel = Read-AgentChoice -Label '[2/5] Add Jmix coding guidelines to this directory?' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[2/5] Add Jmix coding guidelines to this directory?' -Options $script:AllAgents -Default 'all' if ($sel[0] -ne 'skip') { if (Read-YesNo -Message "Target directory: $((Get-Location).Path). Proceed?" -Default 'y') { Initialize-Tarball diff --git a/install.sh b/install.sh index 97d449d..dfffbe3 100755 --- a/install.sh +++ b/install.sh @@ -777,17 +777,16 @@ wizard_pick_agent() { { log "" log "$prompt_label" + printf ' a) For all agents\n' local i=1 local opt for opt in $options; do printf ' %d) %s\n' "$i" "$(agent_label "$opt")" i=$((i + 1)) done - printf ' %d) For all agents\n' "$i" printf ' s) Skip\n' } >&2 - local total=$((i)) local answer answer="$(prompt 'Choice' "$default_choice")" case "$answer" in @@ -799,10 +798,6 @@ wizard_pick_agent() { printf 'skip' return 0 fi - if [ "$answer" -eq "$total" ]; then - printf '%s' "$options" - return 0 - fi local idx=1 for opt in $options; do if [ "$idx" -eq "$answer" ]; then @@ -845,9 +840,9 @@ cmd_wizard() { local sel sel="$(wizard_pick_agent all '[1/5] Install Jmix skills?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then - local scope_answer scope="global" - scope_answer="$(prompt 'Install scope: (g)lobal user home or (l)ocal project dir' 'g')" - case "$scope_answer" in l|L|local|LOCAL) scope="local" ;; esac + local scope_answer scope="local" + scope_answer="$(prompt 'Install scope: (l)ocal project dir or (g)lobal user home' 'l')" + case "$scope_answer" in g|G|global|GLOBAL) scope="global" ;; esac ensure_tarball local root store_dir if [ "$scope" = "local" ]; then @@ -868,7 +863,7 @@ cmd_wizard() { fi # Step 2: agents-md - sel="$(wizard_pick_agent skip '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" + sel="$(wizard_pick_agent all '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then ensure_tarball From 481b19af0c7cde77bb4dcb931f420475d3b04ef0 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Thu, 21 May 2026 16:12:24 +0400 Subject: [PATCH 27/36] Fixes --- .studio/studio-meta-data.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.studio/studio-meta-data.json b/.studio/studio-meta-data.json index 34787c2..a1c88f7 100644 --- a/.studio/studio-meta-data.json +++ b/.studio/studio-meta-data.json @@ -141,7 +141,7 @@ { "id": "context7", "title": "Context7 MCP", - "message": "\uD83D\uDD0C Register the Context7 MCP server (provides up-to-date library documentation) with the selected agents.
🔑 Enter your Context7 API key below.
🛡️ The key is sent to the MCP CLI and never stored by Studio.
⏭\uFE0F Click Skip to opt out.", + "message": "\uD83D\uDD0C Register the Context7 MCP server with the selected agents.
\uD83C\uDF10 You can get key on context7.com. Your key is sent to the MCP CLI and never stored by Studio.
⏭\uFE0F Click Skip to opt out.", "inputs": [ { "id": "context7Agents", @@ -173,7 +173,7 @@ }, { "id": "context7Key", - "label": "API key", + "label": "\uD83D\uDD11 API key", "type": "userInput", "regex": "^.{8,}$", "errorMessage": "Context7 API key must be at least 8 characters.", From 9be1e23f3032bb10e379f94aa228bf193b3471fb Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 00:42:12 +0400 Subject: [PATCH 28/36] Fix symlinks configuration for agent skills --- README.md | 22 +++++++++++----------- install.ps1 | 27 +++++++++++++++++++-------- install.sh | 36 ++++++++++++++++++++++++++---------- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 667dd1d..b27e5fc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ curl -fsSL https://raw.githubusercontent.com/jmix-framework/jmix-agent-guideline iwr -useb https://raw.githubusercontent.com/jmix-framework/jmix-agent-guidelines/main/install.ps1 | iex ``` -In Jmix Studio plugin, the same wizard is available from the **Jmix AI Agents Toolkit** action. +> In Jmix Studio plugin, the same wizard is available from the **Jmix AI Agents Toolkit** action. ### Non-Interactive Subcommands @@ -50,14 +50,14 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i ### Flags -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:--------------------------|:-----------------------|:--------|:-------------------------------------------------------------------------------------------------------------| -| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | -| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:--------------------------|:-----------------------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | +| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | | `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | -| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | -| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | +| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | +| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | > The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. @@ -76,10 +76,10 @@ Copy the `AGENTS.md` file from this repository to the root of your Jmix applicat ### 2. Agent Skills -The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). These should be made available to the agent globally. +The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). The installer copies them into a canonical store and creates a **per-skill symlink** into each agent's skills directory, so Jmix skills coexist with any other skills already there. -- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`), symlinked from `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). -- **Local:** store at `/.skills/`, symlinked from `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. +- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). +- **Local:** store at `/.skills/`; each `jmix-*` folder symlinked into `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. | Agent | Skills Folder Path | |:------------|:-----------------------------| diff --git a/install.ps1 b/install.ps1 index 367dbf3..2b37d46 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Jmix AI Agent Guidelines installer. + Jmix AI Agents Toolkit installer. .DESCRIPTION Default invocation (no subcommand) launches an interactive wizard that @@ -382,6 +382,17 @@ function Install-SkillsToStore { } } +# Per-skill symlinks: link each store skill folder into the agent skills dir, +# so Jmix skills coexist with other skills already present there. +function New-SkillSymlinks { + param([string]$AgentDir, [string]$StoreDir) + if (-not (Test-Path $AgentDir)) { New-Item -ItemType Directory -Path $AgentDir -Force | Out-Null } + foreach ($skill in Get-ChildItem -Path $StoreDir -Directory) { + $link = Join-Path $AgentDir $skill.Name + New-DirSymlink -Link $link -Target $skill.FullName + } +} + function Invoke-CmdSkills { $agents = Resolve-AgentsCsv -Csv $Agents -Subcommand 'skills' $resolvedScope = Resolve-Scope -Scope $Scope @@ -398,15 +409,15 @@ function Invoke-CmdSkills { Install-SkillsToStore -StoreDir $storeDir Write-Info '' - Write-Info 'Linking agent skill dirs to the store' + Write-Info 'Linking store skills into agent dirs' $seen = @{} foreach ($a in $agents) { $rel = Get-AgentSymlinkRel -Agent $a if ($seen.ContainsKey($rel)) { continue } $seen[$rel] = $true - $link = Join-Path $root $rel - New-DirSymlink -Link $link -Target $storeDir - Write-Info " Linked: $link -> $storeDir" + $agentDir = Join-Path $root $rel + New-SkillSymlinks -AgentDir $agentDir -StoreDir $storeDir + Write-Info " Linked skills into $agentDir" } Write-Info '' @@ -736,9 +747,9 @@ function Invoke-Wizard { $rel = Get-AgentSymlinkRel -Agent $a if ($wizSeen.ContainsKey($rel)) { continue } $wizSeen[$rel] = $true - $link = Join-Path $wizRoot $rel - New-DirSymlink -Link $link -Target $wizStoreDir - Write-Info " Linked: $link -> $wizStoreDir" + $agentDir = Join-Path $wizRoot $rel + New-SkillSymlinks -AgentDir $agentDir -StoreDir $wizStoreDir + Write-Info " Linked skills into $agentDir" } } catch { Write-Info "error: $($_.Exception.Message)" } $summaryStrings.skills = "$($sel -join ', ') ($resolvedScope)" diff --git a/install.sh b/install.sh index dfffbe3..671db55 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Jmix AI Agent Guidelines installer. +# Jmix AI Agents Toolkit installer. # # Default (no subcommand) launches an interactive wizard that guides through: # 1. Installing Jmix skills (globally or into the project) for one or all agents. @@ -148,7 +148,7 @@ prompt_yes_no() { usage() { cat <<'EOF' -Jmix AI Agent Guidelines installer. +Jmix AI Agents Toolkit installer. Usage: install.sh [--version V] [--ref REF] # interactive wizard @@ -365,6 +365,22 @@ install_skills_to_store() { done } +# Per-skill symlinks: link each store skill folder into the agent skills dir, +# so Jmix skills coexist with other skills already present there. +# $1 - agent skills dir (kept as a real dir) +# $2 - store dir holding the skill folders +link_skills_into_dir() { + local agent_dir="$1" + local store_dir="$2" + mkdir -p "$agent_dir" || die "cannot create ${agent_dir}" + local skill name + for skill in "$store_dir"/*/; do + [ -d "$skill" ] || continue + name="$(basename "$skill")" + create_symlink "${agent_dir}/${name}" "${store_dir}/${name}" + done +} + agent_label() { case "$1" in claude) printf 'Claude Code' ;; @@ -418,17 +434,17 @@ cmd_skills() { install_skills_to_store "$store_dir" log "" - log "Linking agent skill dirs to the store" - local agent rel link seen=" " + log "Linking store skills into agent dirs" + local agent rel agent_dir seen=" " for agent in $agents; do rel="$(agent_symlink_rel "$agent")" case "$seen" in *" ${rel} "*) continue ;; esac seen="${seen}${rel} " - link="${root}/${rel}" - create_symlink "$link" "$store_dir" - log " Linked: ${link} -> ${store_dir}" + agent_dir="${root}/${rel}" + link_skills_into_dir "$agent_dir" "$store_dir" + log " Linked skills into ${agent_dir}" done log "" @@ -851,13 +867,13 @@ cmd_wizard() { root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" fi install_skills_to_store "$store_dir" || true - local agent rel link seen=" " + local agent rel agent_dir seen=" " for agent in $sel; do rel="$(agent_symlink_rel "$agent")" case "$seen" in *" ${rel} "*) continue ;; esac seen="${seen}${rel} " - link="${root}/${rel}" - create_symlink "$link" "$store_dir" || true + agent_dir="${root}/${rel}" + link_skills_into_dir "$agent_dir" "$store_dir" || true done summary_skills="$sel (${scope})" fi From 76e5ba06dc55dbc8c8d4edd3f64d6a47253be216 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 16:49:51 +0400 Subject: [PATCH 29/36] Rewrite playwright installation flow to use npx instead of npm (root permissions issue) --- README.md | 13 ++++--------- install.ps1 | 31 +++++++++++++++++-------------- install.sh | 31 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index b27e5fc..e539f48 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ install.sh skills --agents CSV [--scope global|local] [--version V] install.sh agents-md --agents CSV [--version V] install.sh mcp-jetbrains --agents CSV install.sh mcp-context7 --agents CSV [--context7-key KEY] -install.sh playwright --agents CSV # requires npm on PATH +install.sh playwright --agents CSV # requires npx (Node.js) on PATH ``` PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `install.ps1 mcp-context7 -Agents claude -Context7Key KEY`, `install.ps1 playwright -Agents claude,codex`, etc. @@ -59,7 +59,7 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i | `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | | `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | -> The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step shells out to `npm` and `playwright-cli`, so both must be available on PATH. +> The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step runs `@playwright/cli` via `npx`, so `npx` (Node.js) must be available on PATH. ## Manual Installation @@ -215,16 +215,11 @@ Add to your `~/.config/opencode/opencode.json`: To enable Playwright support: -- Install Playwright CLI globally: - ```bash - npm i -g @playwright/cli@latest - ``` - - Install Playwright skills: ```bash - playwright-cli install --skills + npx -y @playwright/cli@latest install --skills ``` - The command above creates Playwrite skills in the `.claude/skills` directory. If you are using a different agent, copy or symlink them to the directory supported by your agent (see [Agent Skills](#2-agent-skills) section). + The command above creates Playwright skills in the `~/.claude/skills` directory. If you are using a different agent, copy or symlink them to the directory supported by your agent (see [Agent Skills](#2-agent-skills) section). Once set up, you can give the agent instructions like: diff --git a/install.ps1 b/install.ps1 index 2b37d46..b7f45bd 100644 --- a/install.ps1 +++ b/install.ps1 @@ -17,7 +17,7 @@ install.ps1 agents-md -Agents CSV [-Version V] [-Ref REF] install.ps1 mcp-jetbrains -Agents CSV install.ps1 mcp-context7 -Agents CSV [-Context7Key KEY] - install.ps1 playwright -Agents CSV # requires npm on PATH + install.ps1 playwright -Agents CSV # requires npx (Node.js) on PATH Add -BackupExistingFiles to any subcommand to rename overwritten files/dirs to .bak- instead of deleting them. @@ -107,6 +107,16 @@ function Test-Tool { } } +# Ensures npx (Node.js) is on PATH. When missing, prints install guidance and +# exits (no automatic runtime install). +function Assert-Npx { + if (Get-Command npx -ErrorAction SilentlyContinue) { return } + Write-Info 'npx (Node.js) is required for the Playwright step but was not found on PATH.' + Write-Info 'Install Node.js (includes npx), then re-run:' + Write-Info ' Windows: winget install OpenJS.NodeJS (or download from https://nodejs.org)' + Write-ErrAndExit 'npx not available on PATH' +} + function Read-Prompt { param( [string]$Message, @@ -616,20 +626,13 @@ function Invoke-CmdMcpContext7 { } # ================================================================= -# Playwright install (npm + playwright-cli) +# Playwright install (npx @playwright/cli) # ================================================================= function Install-PlaywrightForAgents { param([string[]]$Agents) - Test-Tool -Tool 'npm' - Write-Info 'Installing/upgrading @playwright/cli globally via npm...' - & npm i -g '@playwright/cli@latest' - if ($LASTEXITCODE -ne 0) { - Write-ErrAndExit 'npm install of @playwright/cli failed' - } - - Test-Tool -Tool 'playwright-cli' + Assert-Npx $claudeSkills = Join-Path $HOME '.claude/skills' if (-not (Test-Path $claudeSkills)) { @@ -641,10 +644,10 @@ function Install-PlaywrightForAgents { $before = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name } - Write-Info "Running 'playwright-cli install --skills'..." - & playwright-cli install --skills + Write-Info 'Installing Playwright skills via npx (@playwright/cli)...' + & npx -y '@playwright/cli@latest' install --skills if ($LASTEXITCODE -ne 0) { - Write-ErrAndExit 'playwright-cli install --skills failed' + Write-ErrAndExit '@playwright/cli install --skills failed' } $after = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name @@ -794,7 +797,7 @@ function Invoke-Wizard { } # Step 5: Playwright - $sel = Read-AgentChoice -Label '[5/5] Install Playwright? (requires npm)' -Options $script:AllAgents + $sel = Read-AgentChoice -Label '[5/5] Install Playwright? (requires npx)' -Options $script:AllAgents if ($sel[0] -ne 'skip') { try { Install-PlaywrightForAgents -Agents $sel diff --git a/install.sh b/install.sh index 671db55..3e7db9e 100755 --- a/install.sh +++ b/install.sh @@ -53,6 +53,20 @@ require_tool() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." } +# Ensures npx (Node.js) is on PATH. When missing, prints per-OS install guidance +# and exits (no automatic runtime install). +require_npx() { + command -v npx >/dev/null 2>&1 && return 0 + err "npx (Node.js) is required for the Playwright step but was not found on PATH." + err "Install Node.js (includes npx), then re-run:" + case "$(uname -s 2>/dev/null)" in + Darwin) err " macOS: brew install node (or download from https://nodejs.org)" ;; + Linux) err " Linux: install via your package manager (e.g. 'sudo apt install nodejs npm') or download from https://nodejs.org" ;; + *) err " See https://nodejs.org/en/download" ;; + esac + exit 1 +} + # Replaces or installs $dest with a copy of $src. When BACKUP_EXISTING=1, an # existing $dest is moved aside to .bak-; otherwise it is # deleted. Prints a per-item log line. @@ -182,7 +196,7 @@ mcp-context7 options: --context7-key K Context7 API key. Prompted interactively when missing. playwright options: - (uses common --agents flag; requires `npm` on PATH) + (uses common --agents flag; requires `npx` (Node.js) on PATH) EOF } @@ -715,7 +729,7 @@ cmd_mcp_context7() { } # ================================================================= -# Playwright install (npm + playwright-cli) +# Playwright install (npx @playwright/cli) # ================================================================= cmd_playwright() { @@ -735,20 +749,15 @@ cmd_playwright() { local agents agents="$(parse_agents_csv "$agents_csv" "playwright")" - require_tool npm - - log "Installing/upgrading @playwright/cli globally via npm..." - npm i -g @playwright/cli@latest || die "npm install of @playwright/cli failed" - - require_tool playwright-cli + require_npx local claude_skills="${HOME}/.claude/skills" mkdir -p "$claude_skills" || die "cannot create ${claude_skills}" local before before="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" - log "Running 'playwright-cli install --skills'..." - playwright-cli install --skills || die "playwright-cli install --skills failed" + log "Installing Playwright skills via npx (@playwright/cli)..." + npx -y @playwright/cli@latest install --skills || die "@playwright/cli install --skills failed" local after after="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" @@ -921,7 +930,7 @@ cmd_wizard() { fi # Step 5: Playwright - sel="$(wizard_pick_agent skip '[5/5] Install Playwright? (requires npm)' $ALL_AGENTS)" + sel="$(wizard_pick_agent skip '[5/5] Install Playwright? (requires npx)' $ALL_AGENTS)" if [ "$sel" != "skip" ]; then local pw_csv pw_csv="$(printf '%s' "$sel" | tr ' ' ',' | sed 's/^,//;s/,$//')" From 13475889143cc6e41282ff5bb51bbb4f3edbcee2 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 17:31:55 +0400 Subject: [PATCH 30/36] Rewrite playwright installation flow to use npx instead of npm (root permissions issue) --- README.md | 1 + install.ps1 | 50 +++++++++++++++++++++++++++++++++++++------ install.sh | 61 ++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e539f48..6ee1b0b 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i | `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | | `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | | `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | +| `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | > The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step runs `@playwright/cli` via `npx`, so `npx` (Node.js) must be available on PATH. diff --git a/install.ps1 b/install.ps1 index b7f45bd..ab5e5ad 100644 --- a/install.ps1 +++ b/install.ps1 @@ -69,6 +69,7 @@ param( ) $ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest $script:RepoOwner = 'jmix-framework' $script:RepoName = 'jmix-agent-guidelines' @@ -94,6 +95,19 @@ function Write-Info { Write-Output $Message } +# Emits environment + tool versions through Write-Verbose (shown only with -Verbose) +# to help diagnose user problems. +function Write-EnvDiagnostics { + Write-Verbose "os: $([System.Environment]::OSVersion.VersionString)" + Write-Verbose "pwd: $((Get-Location).Path)" + Write-Verbose "HOME: $HOME" + Write-Verbose "PSVersion: $($PSVersionTable.PSVersion)" + foreach ($tool in 'git', 'node', 'npx') { + $cmd = Get-Command $tool -ErrorAction SilentlyContinue + Write-Verbose "${tool}: $(if ($cmd) { $cmd.Source } else { 'not found' })" + } +} + function Write-ErrAndExit { param([string]$Message) [Console]::Error.WriteLine("error: $Message") @@ -295,14 +309,27 @@ function Initialize-Tarball { $archiveUrl = "https://codeload.github.com/$($script:RepoOwner)/$($script:RepoName)/zip/$Ref" $zipPath = Join-Path $script:Staging 'source.zip' + Write-Verbose "staging: $($script:Staging)" + Write-Verbose "archiveUrl: $archiveUrl ; requested version: '$Version', ref: '$Ref'" Write-Info "Downloading $archiveUrl" - try { - Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile $zipPath - } catch { - $status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } - Write-ErrAndExit "failed to download $archiveUrl (HTTP $status)" + $downloaded = $false + for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile $zipPath -TimeoutSec 300 + $downloaded = $true + break + } catch { + $status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } + if ($attempt -lt 3) { + Write-Info "Download attempt $attempt failed (HTTP $status); retrying in 2s..." + Start-Sleep -Seconds 2 + } else { + Write-ErrAndExit "failed to download $archiveUrl after $attempt attempts (HTTP $status)" + } + } } + if (-not $downloaded) { Write-ErrAndExit "failed to download $archiveUrl" } Expand-Archive -Path $zipPath -DestinationPath $script:Staging -Force @@ -323,6 +350,9 @@ function Initialize-Tarball { $script:SourceSkillsDir = $resolved.Path $script:ResolvedVersionDir = Split-Path -Leaf (Split-Path -Parent $script:SourceSkillsDir) $script:SourceAgentsMd = Join-Path (Split-Path -Parent $script:SourceSkillsDir) 'AGENTS.md' + Write-Verbose "extracted dir: $($script:ExtractedDir)" + Write-Verbose "resolved version dir: $($script:ResolvedVersionDir)" + Write-Verbose "source skills dir: $($script:SourceSkillsDir)" if ($resolved.Status -eq 'fallback') { Write-Info "Version '$Version' did not match any folder, falling back to latest available ($($script:ResolvedVersionDir))" @@ -416,6 +446,7 @@ function Invoke-CmdSkills { $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) } + Write-Verbose "scope=$resolvedScope root=$root store=$storeDir" Install-SkillsToStore -StoreDir $storeDir Write-Info '' @@ -645,8 +676,13 @@ function Install-PlaywrightForAgents { } Write-Info 'Installing Playwright skills via npx (@playwright/cli)...' + # playwright-cli installs into /.claude/skills; run from $HOME so the + # skills land globally in ~/.claude/skills, never inside the project. + Push-Location $HOME & npx -y '@playwright/cli@latest' install --skills - if ($LASTEXITCODE -ne 0) { + $playwrightExit = $LASTEXITCODE + Pop-Location + if ($playwrightExit -ne 0) { Write-ErrAndExit '@playwright/cli install --skills failed' } @@ -821,6 +857,8 @@ function Invoke-Wizard { # ================================================================= try { + Write-EnvDiagnostics + switch ($Subcommand) { '' { Invoke-Wizard } 'skills' { Invoke-CmdSkills } diff --git a/install.sh b/install.sh index 3e7db9e..13300b9 100755 --- a/install.sh +++ b/install.sh @@ -25,6 +25,7 @@ TARBALL_READY=0 VERSION="" REF="main" BACKUP_EXISTING=0 +VERBOSE=0 TIMESTAMP="$(date +%Y%m%d-%H%M%S)" @@ -49,6 +50,27 @@ die() { exit 1 } +# Prints a diagnostic line to stderr, only when --verbose/--debug is set. +vlog() { + [ "$VERBOSE" -eq 1 ] && printf '[debug] %s\n' "$*" >&2 + return 0 +} + +# Dumps environment + tool versions (verbose only) to help diagnose user issues. +debug_env() { + [ "$VERBOSE" -eq 1 ] || return 0 + vlog "os: $(uname -a 2>/dev/null)" + vlog "pwd: $(pwd -P 2>/dev/null)" + vlog "HOME: ${HOME:-}" + vlog "PATH: ${PATH:-}" + vlog "bash: ${BASH_VERSION:-?}" + vlog "curl: $(command -v curl 2>/dev/null || echo 'not found')" + vlog "tar: $(command -v tar 2>/dev/null || echo 'not found')" + vlog "git: $(git --version 2>/dev/null || echo 'not found')" + vlog "node: $(node --version 2>/dev/null || echo 'not found')" + vlog "npx: $(npx --version 2>/dev/null || echo 'not found')" +} + require_tool() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it and re-run." } @@ -184,6 +206,8 @@ Common options: --backup-existing-files Rename overwritten files/dirs to .bak- instead of deleting them. Off by default. + --verbose, --debug Print extra diagnostic output (OS, PATH, resolved + paths, tool versions) to help troubleshoot problems. -h, --help Show this help. skills options: @@ -283,14 +307,16 @@ ensure_tarball() { require_tool tar STAGING="$(mktemp -d 2>/dev/null || mktemp -d -t jmix-install)" - trap 'rm -rf "$STAGING"' EXIT + trap 'rm -rf "$STAGING"' INT TERM EXIT local tarball_url="https://codeload.github.com/${REPO_OWNER}/${REPO_NAME}/tar.gz/${REF}" local tarball_path="${STAGING}/source.tar.gz" + vlog "staging dir: ${STAGING}" + vlog "requested version: '${VERSION}', ref: '${REF}'" log "Downloading ${tarball_url}" local http_status - http_status="$(curl -sSL -w '%{http_code}' -o "$tarball_path" "$tarball_url" || echo "000")" + http_status="$(curl -sSL --retry 3 --retry-delay 2 --connect-timeout 30 --max-time 300 -w '%{http_code}' -o "$tarball_path" "$tarball_url" || echo "000")" if [ "$http_status" != "200" ]; then die "failed to download ${tarball_url} (HTTP ${http_status})" fi @@ -309,11 +335,14 @@ ensure_tarball() { RESOLVED_VERSION_DIR="$(basename "$(dirname "$SOURCE_SKILLS_DIR")")" SOURCE_AGENTS_MD="$(dirname "$SOURCE_SKILLS_DIR")/AGENTS.md" + vlog "extracted dir: ${EXTRACTED_DIR}" + vlog "resolved version dir: ${RESOLVED_VERSION_DIR}" + vlog "source skills dir: ${SOURCE_SKILLS_DIR}" if [ "$resolve_status" -eq 2 ]; then log "Version '${VERSION}' did not match any folder, falling back to latest available (${RESOLVED_VERSION_DIR})" fi - log "Using guidelines from ${SOURCE_SKILLS_DIR#${EXTRACTED_DIR}/}" + log "Using guidelines from ${SOURCE_SKILLS_DIR#"${EXTRACTED_DIR}"/}" TARBALL_READY=1 } @@ -445,6 +474,7 @@ cmd_skills() { store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" fi + vlog "scope=${scope} root=${root} store=${store_dir}" install_skills_to_store "$store_dir" log "" @@ -757,7 +787,10 @@ cmd_playwright() { before="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" log "Installing Playwright skills via npx (@playwright/cli)..." - npx -y @playwright/cli@latest install --skills || die "@playwright/cli install --skills failed" + # playwright-cli installs into /.claude/skills; run from $HOME so the + # skills land globally in ~/.claude/skills, never inside the project. + ( cd "$HOME" && npx -y @playwright/cli@latest install --skills ) \ + || die "@playwright/cli install --skills failed" local after after="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" @@ -863,7 +896,7 @@ cmd_wizard() { # Step 1: skills local sel - sel="$(wizard_pick_agent all '[1/5] Install Jmix skills?' $ALL_AGENTS)" + sel="$(wizard_pick_agent all '[1/5] Install Jmix skills?' "$ALL_AGENTS")" if [ "$sel" != "skip" ]; then local scope_answer scope="local" scope_answer="$(prompt 'Install scope: (l)ocal project dir or (g)lobal user home' 'l')" @@ -888,7 +921,7 @@ cmd_wizard() { fi # Step 2: agents-md - sel="$(wizard_pick_agent all '[2/5] Add Jmix coding guidelines to this directory?' $ALL_AGENTS)" + sel="$(wizard_pick_agent all '[2/5] Add Jmix coding guidelines to this directory?' "$ALL_AGENTS")" if [ "$sel" != "skip" ]; then if prompt_yes_no "Target directory: $(pwd -P). Proceed?" "y"; then ensure_tarball @@ -903,6 +936,7 @@ cmd_wizard() { fi # Step 3: JetBrains MCP + # shellcheck disable=SC2086 sel="$(wizard_pick_agent skip '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" if [ "$sel" != "skip" ]; then local agent @@ -913,7 +947,7 @@ cmd_wizard() { fi # Step 4: Context7 MCP - sel="$(wizard_pick_agent skip '[4/5] Connect agent to library docs via Context7 MCP?' $CONTEXT7_AGENTS)" + sel="$(wizard_pick_agent skip '[4/5] Connect agent to library docs via Context7 MCP?' "$CONTEXT7_AGENTS")" if [ "$sel" != "skip" ]; then local key key="$(prompt 'Context7 API key' '')" @@ -930,7 +964,7 @@ cmd_wizard() { fi # Step 5: Playwright - sel="$(wizard_pick_agent skip '[5/5] Install Playwright? (requires npx)' $ALL_AGENTS)" + sel="$(wizard_pick_agent skip '[5/5] Install Playwright? (requires npx)' "$ALL_AGENTS")" if [ "$sel" != "skip" ]; then local pw_csv pw_csv="$(printf '%s' "$sel" | tr ' ' ',' | sed 's/^,//;s/,$//')" @@ -951,6 +985,17 @@ cmd_wizard() { # Main dispatch # ================================================================= +# Pull global --verbose/--debug out of the args so every subcommand benefits. +_args=() +for _a in "$@"; do + case "$_a" in + --verbose|--debug) VERBOSE=1 ;; + *) _args+=("$_a") ;; + esac +done +set -- ${_args[@]+"${_args[@]}"} +debug_env + if [ $# -eq 0 ]; then cmd_wizard exit $? From c2f2966fb1064bde500cd0e55955f797d4472837 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 17:33:41 +0400 Subject: [PATCH 31/36] Fixes --- install.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 13300b9..f9c539e 100755 --- a/install.sh +++ b/install.sh @@ -936,8 +936,7 @@ cmd_wizard() { fi # Step 3: JetBrains MCP - # shellcheck disable=SC2086 - sel="$(wizard_pick_agent skip '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' $JETBRAINS_AGENTS)" + sel="$(wizard_pick_agent skip '[3/5] Connect agent to IntelliJ IDEA via JetBrains MCP?' "$JETBRAINS_AGENTS")" if [ "$sel" != "skip" ]; then local agent for agent in $sel; do From 851a972e47c84e17ef5fc3567a0fcc8200bb53ea Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 17:40:10 +0400 Subject: [PATCH 32/36] Add checks for infinite loop --- install.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/install.sh b/install.sh index f9c539e..6abf270 100755 --- a/install.sh +++ b/install.sh @@ -438,7 +438,10 @@ cmd_skills() { local agents_csv="" local scope="global" + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" @@ -532,7 +535,10 @@ install_agents_md_for() { cmd_agents_md() { local agents_csv="" + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" @@ -626,7 +632,10 @@ install_jetbrains_for() { cmd_mcp_jetbrains() { local agents_csv="" + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" @@ -728,7 +737,10 @@ cmd_mcp_context7() { local agents_csv="" local key="" + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" @@ -764,7 +776,10 @@ cmd_mcp_context7() { cmd_playwright() { local agents_csv="" + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --agents) [ $# -ge 2 ] || die "--agents requires an argument" @@ -869,7 +884,10 @@ wizard_pick_agent() { } cmd_wizard() { + local _argc=-1 while [ $# -gt 0 ]; do + [ "$#" -ne "$_argc" ] || die "argument parser made no progress near: $1" + _argc=$# case "$1" in --version) [ $# -ge 2 ] || die "--version requires an argument" From 1d915d1baacf70b81a03d4b88cfe78d5ebc9387b Mon Sep 17 00:00:00 2001 From: Konstantin Krivopustov Date: Fri, 22 May 2026 17:49:11 +0400 Subject: [PATCH 33/36] Fix silent exit in playwright install when ~/.claude/skills is empty The ls -1d */ snapshot pipeline failed under set -euo pipefail when the directory had no subdirectories. Switch to find for a no-match-safe listing. Co-Authored-By: Claude Opus 4.7 (1M context) --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 6abf270..c508a44 100755 --- a/install.sh +++ b/install.sh @@ -799,7 +799,7 @@ cmd_playwright() { local claude_skills="${HOME}/.claude/skills" mkdir -p "$claude_skills" || die "cannot create ${claude_skills}" local before - before="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" + before="$(cd "$claude_skills" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort -u)" log "Installing Playwright skills via npx (@playwright/cli)..." # playwright-cli installs into /.claude/skills; run from $HOME so the @@ -808,7 +808,7 @@ cmd_playwright() { || die "@playwright/cli install --skills failed" local after - after="$(cd "$claude_skills" && ls -1d */ 2>/dev/null | sed 's:/$::' | sort -u)" + after="$(cd "$claude_skills" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort -u)" local new_skills new_skills="$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after"))" From ab117c62f2a7b5c04f84856e46a5856cb95bd811 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Fri, 22 May 2026 18:58:07 +0400 Subject: [PATCH 34/36] Rewrite playwright scripts installation --- .studio/gen_skills_manifest.py | 2 +- README.md | 20 +++---- install.ps1 | 99 +++++++++++++++++++--------------- install.sh | 74 ++++++++++++++----------- 4 files changed, 111 insertions(+), 84 deletions(-) diff --git a/.studio/gen_skills_manifest.py b/.studio/gen_skills_manifest.py index 18646f0..59a9950 100644 --- a/.studio/gen_skills_manifest.py +++ b/.studio/gen_skills_manifest.py @@ -13,7 +13,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Canonical per-scope skill store (relative to the scope root: the user home for -# "global", the project base for "local"). The global store appends /v. +# "global", the project base for "local"). The global store appends /jmix-v. # Studio reads this from the manifest; install.sh / install.ps1 mirror it. STORE = { "global": ".agents/.jmix/skills", diff --git a/README.md b/README.md index 6ee1b0b..20a8561 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,15 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i ### Flags -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:--------------------------|:-----------------------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | -| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | -| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | -| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | -| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | -| `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:--------------------------|:-----------------------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | +| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/jmix-v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | +| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | +| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | +| `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | > The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step runs `@playwright/cli` via `npx`, so `npx` (Node.js) must be available on PATH. @@ -79,7 +79,7 @@ Copy the `AGENTS.md` file from this repository to the root of your Jmix applicat The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). The installer copies them into a canonical store and creates a **per-skill symlink** into each agent's skills directory, so Jmix skills coexist with any other skills already there. -- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). +- **Global:** store at `~/.agents/.jmix/skills/jmix-v/` (e.g. `jmix-v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). - **Local:** store at `/.skills/`; each `jmix-*` folder symlinked into `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. | Agent | Skills Folder Path | diff --git a/install.ps1 b/install.ps1 index ab5e5ad..a4648ba 100644 --- a/install.ps1 +++ b/install.ps1 @@ -443,7 +443,7 @@ function Invoke-CmdSkills { $storeDir = Join-Path $root '.skills' } else { $root = $HOME - $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) + $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' "jmix-$($script:ResolvedVersionDir)") } Write-Verbose "scope=$resolvedScope root=$root store=$storeDir" @@ -665,52 +665,65 @@ function Install-PlaywrightForAgents { Assert-Npx - $claudeSkills = Join-Path $HOME '.claude/skills' - if (-not (Test-Path $claudeSkills)) { - New-Item -ItemType Directory -Path $claudeSkills -Force | Out-Null - } - - $before = @() - if (Test-Path $claudeSkills) { - $before = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name - } - - Write-Info 'Installing Playwright skills via npx (@playwright/cli)...' - # playwright-cli installs into /.claude/skills; run from $HOME so the - # skills land globally in ~/.claude/skills, never inside the project. - Push-Location $HOME - & npx -y '@playwright/cli@latest' install --skills - $playwrightExit = $LASTEXITCODE - Pop-Location - if ($playwrightExit -ne 0) { - Write-ErrAndExit '@playwright/cli install --skills failed' - } - - $after = Get-ChildItem -Path $claudeSkills -Directory | Select-Object -ExpandProperty Name - $newSkills = $after | Where-Object { $before -notcontains $_ } + # Playwright skills always install globally. Mirror the Jmix model: copy the + # skills into a canonical store, then per-skill symlink them into each agent + # skills dir so they coexist with other skills already present there. + $root = $HOME + $storeDir = Join-Path $HOME '.agents/.playwright/skills' + + # @playwright/cli install --skills writes to /.claude/skills/. + # Run it inside a private staging dir so nothing leaks into the project or a + # real agent dir, then copy the produced skill folders into the store. + $pwStaging = Join-Path ([System.IO.Path]::GetTempPath()) ("jmix-playwright-" + [System.Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $pwStaging -Force | Out-Null + try { + Write-Info 'Installing Playwright skills via npx (@playwright/cli)...' + Push-Location $pwStaging + try { + & npx -y '@playwright/cli@latest' install --skills + $playwrightExit = $LASTEXITCODE + } finally { + Pop-Location + } + if ($playwrightExit -ne 0) { + Write-ErrAndExit '@playwright/cli install --skills failed' + } - if ($newSkills.Count -eq 0) { - Write-Info " No new skill folders detected in $claudeSkills." - } else { - Write-Info " Playwright skill(s) installed in ${claudeSkills}: $($newSkills -join ' ')" - } + $produced = Join-Path $pwStaging '.claude/skills' + if (-not (Test-Path $produced)) { + Write-ErrAndExit "@playwright/cli produced no skills under $produced" + } - foreach ($agent in $Agents) { - if ($agent -eq 'claude') { continue } - $target = Join-Path $HOME (Get-AgentSymlinkRel -Agent $agent) - if (-not (Test-Path $target)) { - New-Item -ItemType Directory -Path $target -Force | Out-Null + Write-Info '' + Write-Info "Installing Playwright skills into store $storeDir" + if (-not (Test-Path $storeDir)) { New-Item -ItemType Directory -Path $storeDir -Force | Out-Null } + $count = 0 + foreach ($skill in Get-ChildItem -Path $produced -Directory) { + $dest = Join-Path $storeDir $skill.Name + Write-Dest -Src $skill.FullName -Dest $dest -Label $skill.Name + $count++ } - foreach ($skill in $newSkills) { - $src = Join-Path $claudeSkills $skill - if (-not (Test-Path $src)) { continue } - $dest = Join-Path $target $skill - Write-Dest -Src $src -Dest $dest -Label $dest + if ($count -eq 0) { + Write-ErrAndExit "no Playwright skill folders found under $produced" } - } - Write-Info '' - Write-Info "Done. Playwright skills installed for: $($Agents -join ', ')" + Write-Info '' + Write-Info 'Linking store skills into agent dirs' + $seen = @{} + foreach ($a in $Agents) { + $rel = Get-AgentSymlinkRel -Agent $a + if ($seen.ContainsKey($rel)) { continue } + $seen[$rel] = $true + $agentDir = Join-Path $root $rel + New-SkillSymlinks -AgentDir $agentDir -StoreDir $storeDir + Write-Info " Linked skills into $agentDir" + } + + Write-Info '' + Write-Info "Done. Installed Playwright skills store at $storeDir and linked: $($Agents -join ', ')" + } finally { + if (Test-Path $pwStaging) { Remove-Item $pwStaging -Recurse -Force -ErrorAction SilentlyContinue } + } } function Invoke-CmdPlaywright { @@ -776,7 +789,7 @@ function Invoke-Wizard { $wizStoreDir = Join-Path $wizRoot '.skills' } else { $wizRoot = $HOME - $wizStoreDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) + $wizStoreDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' "jmix-$($script:ResolvedVersionDir)") } Install-SkillsToStore -StoreDir $wizStoreDir Write-Info '' diff --git a/install.sh b/install.sh index c508a44..a84065a 100755 --- a/install.sh +++ b/install.sh @@ -16,6 +16,9 @@ REPO_NAME="jmix-agent-guidelines" # Global state populated by ensure_tarball() STAGING="" +# Temp dir for the Playwright install; global so the EXIT trap can clean it +# after cmd_playwright() returns (function locals are out of scope by then). +PW_STAGING="" EXTRACTED_DIR="" SOURCE_SKILLS_DIR="" SOURCE_AGENTS_MD="" @@ -474,7 +477,7 @@ cmd_skills() { store_dir="${root}/.skills" else root="${HOME}" - store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" + store_dir="${HOME}/.agents/.jmix/skills/jmix-${RESOLVED_VERSION_DIR}" fi vlog "scope=${scope} root=${root} store=${store_dir}" @@ -796,44 +799,55 @@ cmd_playwright() { require_npx - local claude_skills="${HOME}/.claude/skills" - mkdir -p "$claude_skills" || die "cannot create ${claude_skills}" - local before - before="$(cd "$claude_skills" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort -u)" + # Playwright skills always install globally. Mirror the Jmix model: copy the + # skills into a canonical store, then per-skill symlink them into each agent + # skills dir so they coexist with other skills already present there. + local root="${HOME}" + local store_dir="${HOME}/.agents/.playwright/skills" + + # @playwright/cli install --skills writes to /.claude/skills/. + # Run it inside a private staging dir so nothing leaks into the project or a + # real agent dir, then copy the produced skill folders into the store. + PW_STAGING="$(mktemp -d 2>/dev/null || mktemp -d -t jmix-playwright)" \ + || die "cannot create temp dir for Playwright install" + trap 'rm -rf ${PW_STAGING:+"$PW_STAGING"} ${STAGING:+"$STAGING"}' INT TERM EXIT log "Installing Playwright skills via npx (@playwright/cli)..." - # playwright-cli installs into /.claude/skills; run from $HOME so the - # skills land globally in ~/.claude/skills, never inside the project. - ( cd "$HOME" && npx -y @playwright/cli@latest install --skills ) \ + ( cd "$PW_STAGING" && npx -y @playwright/cli@latest install --skills ) \ || die "@playwright/cli install --skills failed" - local after - after="$(cd "$claude_skills" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort -u)" - local new_skills - new_skills="$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after"))" + local produced="${PW_STAGING}/.claude/skills" + [ -d "$produced" ] || die "@playwright/cli produced no skills under ${produced}" - if [ -z "$new_skills" ]; then - log " No new skill folders detected in ${claude_skills}." - log " Playwright skills appear to be up to date for Claude Code." - else - log " Playwright skill(s) installed in ${claude_skills}: $(printf '%s' "$new_skills" | tr '\n' ' ')" - fi + log "" + log "Installing Playwright skills into store ${store_dir}" + mkdir -p "$store_dir" || die "cannot create store ${store_dir}" + local skill name dest count=0 + for skill in "$produced"/*/; do + [ -d "$skill" ] || continue + name="$(basename "$skill")" + dest="${store_dir}/${name}" + write_dest "$skill" "$dest" "$name" + count=$((count + 1)) + done + [ "$count" -gt 0 ] || die "no Playwright skill folders found under ${produced}" - # Replicate the newly-installed skills to the other selected agents. - local agent target dest skill + log "" + log "Linking store skills into agent dirs" + local agent rel agent_dir seen=" " for agent in $agents; do - [ "$agent" = "claude" ] && continue - target="${HOME}/$(agent_symlink_rel "$agent")" - mkdir -p "$target" || die "cannot create ${target}" - for skill in $new_skills; do - [ -d "${claude_skills}/${skill}" ] || continue - dest="${target}/${skill}" - write_dest "${claude_skills}/${skill}" "$dest" "$dest" - done + rel="$(agent_symlink_rel "$agent")" + case "$seen" in + *" ${rel} "*) continue ;; + esac + seen="${seen}${rel} " + agent_dir="${root}/${rel}" + link_skills_into_dir "$agent_dir" "$store_dir" + log " Linked skills into ${agent_dir}" done log "" - log "Done. Playwright skills installed for: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" + log "Done. Installed Playwright skills store at ${store_dir} and linked: $(printf '%s' "$agents" | tr ' ' ',' | sed 's/,/, /g')" } # ================================================================= @@ -924,7 +938,7 @@ cmd_wizard() { if [ "$scope" = "local" ]; then root="$(pwd -P)"; store_dir="${root}/.skills" else - root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" + root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/jmix-${RESOLVED_VERSION_DIR}" fi install_skills_to_store "$store_dir" || true local agent rel agent_dir seen=" " From 917a8b14a2e86ee8b428d17b704ea5d9adfd2c34 Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Sun, 24 May 2026 02:28:39 +0400 Subject: [PATCH 35/36] Fixes --- .studio/gen_skills_manifest.py | 2 +- README.md | 20 ++++++++++---------- install.ps1 | 24 ++++++++++++++++++++++-- install.sh | 21 +++++++++++++++++++-- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/.studio/gen_skills_manifest.py b/.studio/gen_skills_manifest.py index 59a9950..18646f0 100644 --- a/.studio/gen_skills_manifest.py +++ b/.studio/gen_skills_manifest.py @@ -13,7 +13,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Canonical per-scope skill store (relative to the scope root: the user home for -# "global", the project base for "local"). The global store appends /jmix-v. +# "global", the project base for "local"). The global store appends /v. # Studio reads this from the manifest; install.sh / install.ps1 mirror it. STORE = { "global": ".agents/.jmix/skills", diff --git a/README.md b/README.md index 20a8561..6ee1b0b 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,15 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i ### Flags -| Flag (bash) | Flag (PowerShell) | Default | Meaning | -|:--------------------------|:-----------------------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | -| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | -| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | -| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/jmix-v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | -| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | -| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | -| `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | +| Flag (bash) | Flag (PowerShell) | Default | Meaning | +|:--------------------------|:-----------------------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--version V` | `-Version V` | latest | Jmix version. Picks the best-matching `v*` folder. | +| `--ref REF` | `-Ref REF` | `main` | Git ref (branch or tag) of this repository to download. | +| `--agents CSV` | `-Agents CSV` | - | Comma-separated agents. Required by every subcommand. | +| `--scope global\|local` | `-Scope global\|local` | global | `skills` only. `global` installs the store under `~/.agents/.jmix/skills/v`; `local` installs the store at `/.skills`. Agent dirs are symlinked to the store. | +| `--context7-key K` | `-Context7Key K` | prompt | Context7 API key. Prompted interactively when omitted. | +| `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | +| `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | > The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step runs `@playwright/cli` via `npx`, so `npx` (Node.js) must be available on PATH. @@ -79,7 +79,7 @@ Copy the `AGENTS.md` file from this repository to the root of your Jmix applicat The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). The installer copies them into a canonical store and creates a **per-skill symlink** into each agent's skills directory, so Jmix skills coexist with any other skills already there. -- **Global:** store at `~/.agents/.jmix/skills/jmix-v/` (e.g. `jmix-v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). +- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). - **Local:** store at `/.skills/`; each `jmix-*` folder symlinked into `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. | Agent | Skills Folder Path | diff --git a/install.ps1 b/install.ps1 index a4648ba..8270fd2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -422,10 +422,30 @@ function Install-SkillsToStore { } } +# Removes a path only when it is a dangling (broken) symlink, so directory creation +# does not fail when an agent base/dir (e.g. ~/.junie) points at a missing target. +# A symlink that resolves to an existing directory is left untouched. +function Clear-DanglingSymlink { + param([string]$Path) + $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue + if ($null -eq $item -or -not $item.LinkType) { return } + $target = @($item.Target) | Select-Object -First 1 + if ($target -and (Test-Path -LiteralPath $target)) { return } + if ($BackupExistingFiles) { + Rename-Item -LiteralPath $Path -NewName "$([System.IO.Path]::GetFileName($Path)).bak-$($script:Timestamp)" -ErrorAction SilentlyContinue + } + if (Test-Path -LiteralPath $Path) { + Remove-Item -LiteralPath $Path -Force -Recurse -ErrorAction SilentlyContinue + } +} + # Per-skill symlinks: link each store skill folder into the agent skills dir, # so Jmix skills coexist with other skills already present there. function New-SkillSymlinks { param([string]$AgentDir, [string]$StoreDir) + # Clear a broken-symlink agent base/dir (e.g. ~/.junie -> missing) so creation works. + Clear-DanglingSymlink -Path (Split-Path -Parent $AgentDir) + Clear-DanglingSymlink -Path $AgentDir if (-not (Test-Path $AgentDir)) { New-Item -ItemType Directory -Path $AgentDir -Force | Out-Null } foreach ($skill in Get-ChildItem -Path $StoreDir -Directory) { $link = Join-Path $AgentDir $skill.Name @@ -443,7 +463,7 @@ function Invoke-CmdSkills { $storeDir = Join-Path $root '.skills' } else { $root = $HOME - $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' "jmix-$($script:ResolvedVersionDir)") + $storeDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) } Write-Verbose "scope=$resolvedScope root=$root store=$storeDir" @@ -789,7 +809,7 @@ function Invoke-Wizard { $wizStoreDir = Join-Path $wizRoot '.skills' } else { $wizRoot = $HOME - $wizStoreDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' "jmix-$($script:ResolvedVersionDir)") + $wizStoreDir = Join-Path $HOME (Join-Path '.agents/.jmix/skills' $script:ResolvedVersionDir) } Install-SkillsToStore -StoreDir $wizStoreDir Write-Info '' diff --git a/install.sh b/install.sh index a84065a..018acb6 100755 --- a/install.sh +++ b/install.sh @@ -375,6 +375,20 @@ agent_symlink_rel() { esac } +# Removes a path only when it is a dangling (broken) symlink, so a later +# `mkdir -p` does not fail with ENOENT on macOS/BSD when a path component points +# at a missing target (e.g. a leftover ~/.junie symlink). A symlink that resolves +# to an existing directory is left untouched. +clear_dangling_symlink() { + local p="$1" + [ -L "$p" ] && [ ! -e "$p" ] || return 0 + if [ "$BACKUP_EXISTING" -eq 1 ]; then + mv "$p" "${p}.bak-${TIMESTAMP}" 2>/dev/null || rm -f "$p" + else + rm -f "$p" + fi +} + # Creates (or refreshes) a whole-dir symlink $1 -> $2. Replaces an existing # symlink; an existing real dir is backed up (when --backup-existing-files) or # removed. Requires symlink support; fails otherwise. @@ -418,6 +432,9 @@ install_skills_to_store() { link_skills_into_dir() { local agent_dir="$1" local store_dir="$2" + # Clear a broken-symlink agent base/dir (e.g. ~/.junie -> missing) so mkdir works. + clear_dangling_symlink "$(dirname "$agent_dir")" + clear_dangling_symlink "$agent_dir" mkdir -p "$agent_dir" || die "cannot create ${agent_dir}" local skill name for skill in "$store_dir"/*/; do @@ -477,7 +494,7 @@ cmd_skills() { store_dir="${root}/.skills" else root="${HOME}" - store_dir="${HOME}/.agents/.jmix/skills/jmix-${RESOLVED_VERSION_DIR}" + store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" fi vlog "scope=${scope} root=${root} store=${store_dir}" @@ -938,7 +955,7 @@ cmd_wizard() { if [ "$scope" = "local" ]; then root="$(pwd -P)"; store_dir="${root}/.skills" else - root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/jmix-${RESOLVED_VERSION_DIR}" + root="${HOME}"; store_dir="${HOME}/.agents/.jmix/skills/${RESOLVED_VERSION_DIR}" fi install_skills_to_store "$store_dir" || true local agent rel agent_dir seen=" " From 7bf56832fe2e570c24e4753f3f56b26ee9b8a92e Mon Sep 17 00:00:00 2001 From: Mikhail Fedoseev Date: Mon, 25 May 2026 12:10:50 +0400 Subject: [PATCH 36/36] Fixes --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8f61d4..4576f10 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ PowerShell mirrors the same shape: `install.ps1 skills -Agents claude,codex`, `i | `--backup-existing-files` | `-BackupExistingFiles` | off | Rename overwritten files/dirs to `.bak-` instead of deleting them. Off by default. | | `--verbose`, `--debug` | `-Verbose` | off | Print extra diagnostic output (OS, PATH, resolved paths, tool versions) for troubleshooting. | +**Skills storages:** +- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`); each `jmix-*` folder symlinked into `~/.claude/skills` (Claude Code), `~/.agents/skills` (Codex, OpenCode), `~/.junie/skills` (Junie). +- **Local:** store at `/.skills/`; each `jmix-*` folder symlinked into `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. + > The automatic installer covers skills (installed globally or into the project), project guidelines, MCP server registration, and Playwright testing skills. The Playwright step runs `@playwright/cli` via `npx`, so `npx` (Node.js) must be available on PATH. ## Manual Installation @@ -80,8 +84,6 @@ Copy the `AGENTS.md` file from this repository to the root of your Jmix applicat The `skills/` directory contains specialized knowledge for developing various Jmix features (entities, UI views, data access, etc.). These should be made available to the agent globally or per-project. Before installing the skills, remove previous versions of the skills from the agent's project or user home directory. -- **Global:** store at `~/.agents/.jmix/skills/v/` (e.g. `v2`); each `jmix-*` folder symlinked into `~/.agents/skills` (Codex, OpenCode), `~/.claude/skills` (Claude Code), `~/.junie/skills` (Junie). -- **Local:** store at `/.skills/`; each `jmix-*` folder symlinked into `/.agents/skills`, `/.claude/skills`, `/.junie/skills`. Copy or symlink the content of the `skills/` subdirectory to the folder recognized by your agent in your project or user home directory: