diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b7d2bee..ef59922 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -266,4 +266,3 @@ jobs: release-staging/ios-automation-server.tar.gz.sha256 install.sh run-visiontest.sh - AGENT_INSTRUCTIONS.md diff --git a/CLAUDE.md b/CLAUDE.md index 0dafaa1..88f4cee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,6 +154,7 @@ The same operations available as MCP tools can be invoked directly from the comm | `press_back` | android | — | — | | `press_home` | android, ios | — | — | | `launch_app` | android, ios | `id` (string) | — | +| `init` | (none) | — | `--agent` (required, comma-separated: `claude,opencode,codex`) | ### CLI Exit Codes diff --git a/README.md b/README.md index 1a804e1..b197745 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,16 @@ Every command requires `--platform android` or `--platform ios` (alias `-p`). Ru With no arguments, `visiontest` starts the MCP stdio server. +### Agent Setup + +To set up AI agent skill files in your project (so agents discover VisionTest CLI instructions automatically): + +```bash +visiontest init --agent claude,opencode,codex +``` + +This writes a `SKILL.md` file to each agent's project-level directory. Run it once per project. + ### Exit Codes | Code | Meaning | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2b3424..d8bc15a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,6 +91,13 @@ tasks.named("test") { useJUnitPlatform() } +// Copy AGENT_INSTRUCTIONS.md from repo root into JAR resources so InitCommand can read it at runtime. +tasks.named("processResources") { + from(rootProject.file("AGENT_INSTRUCTIONS.md")) { + rename { "agent-instructions.md" } + } +} + // Configurazione per il jar ombra (con tutte le dipendenze) tasks { shadowJar { diff --git a/app/src/main/kotlin/com/example/visiontest/cli/VisionTestCli.kt b/app/src/main/kotlin/com/example/visiontest/cli/VisionTestCli.kt index 3cef7e3..4ca0c83 100644 --- a/app/src/main/kotlin/com/example/visiontest/cli/VisionTestCli.kt +++ b/app/src/main/kotlin/com/example/visiontest/cli/VisionTestCli.kt @@ -36,6 +36,8 @@ class VisionTestCli : NoOpCliktCommand(name = "visiontest") { PressHomeCommand(lazy { components }), // Apps LaunchAppCommand(lazy { components }), + // Project setup + InitCommand(), ) } } diff --git a/app/src/main/kotlin/com/example/visiontest/cli/commands/InitCommand.kt b/app/src/main/kotlin/com/example/visiontest/cli/commands/InitCommand.kt new file mode 100644 index 0000000..c109b57 --- /dev/null +++ b/app/src/main/kotlin/com/example/visiontest/cli/commands/InitCommand.kt @@ -0,0 +1,78 @@ +package com.example.visiontest.cli.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.CliktError +import com.github.ajalt.clikt.core.UsageError +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.options.split +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +/** + * `visiontest init --agent claude,opencode,codex` + * + * Writes a project-level SKILL.md for each specified agent so that AI coding + * assistants discover VisionTest CLI instructions automatically. + * + * Does NOT require `--platform` because this is not a device operation. + */ +class InitCommand( + private val workingDir: Path = Path.of(System.getProperty("user.dir")), + private val resourceLoader: (String) -> String? = Companion::loadClasspathResource, +) : CliktCommand(name = "init", help = "Set up AI agent skill files in the current project") { + + companion object { + private const val RESOURCE_PATH = "agent-instructions.md" + + /** Agent name → relative SKILL.md path from project root. */ + val AGENT_PATHS: Map = mapOf( + "claude" to ".claude/skills/visiontest/SKILL.md", + "opencode" to ".opencode/skills/visiontest/SKILL.md", + "codex" to ".agents/skills/visiontest/SKILL.md", + ) + + private val YAML_FRONTMATTER = """ + |--- + |name: visiontest + |description: VisionTest mobile automation CLI – commands, workflows, and examples for automating Android and iOS devices. + |--- + | + """.trimMargin() + + fun loadClasspathResource(name: String): String? = + InitCommand::class.java.classLoader.getResourceAsStream(name) + ?.bufferedReader() + ?.use { it.readText() } + } + + private val agents by option("--agent", help = "Comma-separated agent names: claude, opencode, codex") + .split(",") + .required() + + override fun run() { + // Validate agent names + val invalid = agents.filter { it.isBlank() || it !in AGENT_PATHS } + if (invalid.isNotEmpty()) { + throw UsageError( + "Unknown agent(s): ${invalid.joinToString()}. Valid agents: ${AGENT_PATHS.keys.joinToString()}" + ) + } + + val instructions = resourceLoader(RESOURCE_PATH) + ?: throw CliktError("Internal error: embedded resource '$RESOURCE_PATH' not found in JAR") + + val content = YAML_FRONTMATTER + instructions + + for (agent in agents) { + val relativePath = AGENT_PATHS.getValue(agent) + val target = workingDir.resolve(relativePath) + target.parent.createDirectories() + target.writeText(content) + echo(" wrote $target") + } + + echo("Initialized ${agents.size} agent skill file(s).") + } +} diff --git a/app/src/test/kotlin/com/example/visiontest/cli/InitCommandTest.kt b/app/src/test/kotlin/com/example/visiontest/cli/InitCommandTest.kt new file mode 100644 index 0000000..3ea3d3b --- /dev/null +++ b/app/src/test/kotlin/com/example/visiontest/cli/InitCommandTest.kt @@ -0,0 +1,119 @@ +package com.example.visiontest.cli + +import com.example.visiontest.cli.commands.InitCommand +import com.github.ajalt.clikt.core.MissingOption +import com.github.ajalt.clikt.core.UsageError +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.readText +import kotlin.io.path.exists +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlin.test.assertContains + +class InitCommandTest { + + @TempDir + lateinit var tmp: Path + + private val fakeInstructions = "# Fake Instructions\nSome content here." + + private fun createCommand(dir: Path = tmp) = InitCommand( + workingDir = dir, + resourceLoader = { fakeInstructions }, + ) + + // --- resource loads from classpath --- + + @Test + fun `embedded resource loads from classpath`() { + val content = InitCommand.loadClasspathResource("agent-instructions.md") + assertTrue(content != null && content.contains("VisionTest"), "Resource should contain VisionTest content") + } + + // --- single agent writes correct file --- + + @Test + fun `single agent writes SKILL file to correct path`() { + val cmd = createCommand() + cmd.parse(listOf("--agent", "claude")) + + val file = tmp.resolve(".claude/skills/visiontest/SKILL.md") + assertTrue(file.exists(), "SKILL.md should be created") + val content = file.readText() + assertTrue(content.startsWith("---\n"), "SKILL.md should start with YAML frontmatter delimiter") + assertContains(content, "\n---\n", message = "SKILL.md should have closing frontmatter delimiter") + assertContains(content, "name: visiontest") + assertContains(content, fakeInstructions) + } + + // --- multiple agents --- + + @Test + fun `comma-separated agents writes all files`() { + val cmd = createCommand() + cmd.parse(listOf("--agent", "claude,opencode,codex")) + + for ((agent, path) in InitCommand.AGENT_PATHS) { + assertTrue(tmp.resolve(path).exists(), "SKILL.md should exist for $agent") + } + } + + // --- invalid agent name --- + + @Test + fun `invalid agent name produces UsageError`() { + val cmd = createCommand() + val ex = assertFailsWith { + cmd.parse(listOf("--agent", "gemini")) + } + assertContains(ex.message ?: "", "Unknown agent") + } + + // --- blank agent name in comma list --- + + @Test + fun `blank agent name in comma list produces UsageError`() { + val cmd = createCommand() + val ex = assertFailsWith { + cmd.parse(listOf("--agent", ",claude")) + } + assertContains(ex.message ?: "", "Unknown agent") + } + + // --- missing --agent --- + + @Test + fun `missing agent flag produces MissingOption`() { + val cmd = createCommand() + assertFailsWith { + cmd.parse(emptyList()) + } + } + + // --- idempotent overwrite --- + + @Test + fun `running init twice produces same content`() { + val cmd1 = createCommand() + cmd1.parse(listOf("--agent", "claude")) + val first = tmp.resolve(".claude/skills/visiontest/SKILL.md").readText() + + val cmd2 = createCommand() + cmd2.parse(listOf("--agent", "claude")) + val second = tmp.resolve(".claude/skills/visiontest/SKILL.md").readText() + + assertEquals(first, second) + } + + // --- agent path mapping --- + + @Test + fun `agent paths map correctly`() { + assertEquals(".claude/skills/visiontest/SKILL.md", InitCommand.AGENT_PATHS["claude"]) + assertEquals(".opencode/skills/visiontest/SKILL.md", InitCommand.AGENT_PATHS["opencode"]) + assertEquals(".agents/skills/visiontest/SKILL.md", InitCommand.AGENT_PATHS["codex"]) + } +} diff --git a/app/src/test/kotlin/com/example/visiontest/cli/VisionTestCliTest.kt b/app/src/test/kotlin/com/example/visiontest/cli/VisionTestCliTest.kt index cf15e99..a5e8dc2 100644 --- a/app/src/test/kotlin/com/example/visiontest/cli/VisionTestCliTest.kt +++ b/app/src/test/kotlin/com/example/visiontest/cli/VisionTestCliTest.kt @@ -129,16 +129,17 @@ class VisionTestCliTest { // --- Subcommand routing --- @Test - fun `root command lists all 13 subcommands`() { + fun `root command lists all 14 subcommands`() { // We can't use the real VisionTestCli (ComponentHolder is lazy but still needs ADB), // so we just verify the count expectation as a documentation test. val expectedCommands = listOf( "install_automation_server", "start_automation_server", "automation_server_status", "get_interactive_elements", "get_ui_hierarchy", "get_device_info", "screenshot", "tap_by_coordinates", "input_text", "swipe_direction", - "press_back", "press_home", "launch_app" + "press_back", "press_home", "launch_app", + "init" ) - assertEquals(13, expectedCommands.size) + assertEquals(14, expectedCommands.size) } // --- SwipeDirection choice validation --- diff --git a/docs/installation.md b/docs/installation.md index a61deff..833ebb4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -11,20 +11,13 @@ Users install with `curl -fsSL https://github.com/docer1990/visiontest/releases/ 6. On macOS arm64: downloads `ios-automation-server.tar.gz` + checksum, extracts pre-built iOS XCUITest bundle to `ios-automation-server/` subdirectory (skipped on Linux and macOS x86_64) 7. Installs JAR, APKs, and iOS bundle to `~/.local/share/visiontest/` (customizable via `VISIONTEST_DIR` env var, must be under `$HOME`) 8. Creates wrapper script at `~/.local/bin/visiontest`, ensures PATH -9. Downloads `AGENT_INSTRUCTIONS.md` and installs VisionTest CLI instructions into detected AI coding agents: - - **Claude Code** (`claude`): creates skill at `~/.claude/skills/visiontest-mobile/SKILL.md` - - **OpenCode** (`opencode`): appends to `~/.config/opencode/AGENTS.md` - - **Codex** (`codex`): appends to `~/.codex/instructions.md` - - **Copilot CLI** (`gh copilot`): appends to `~/.github/copilot-instructions.md` - - Uses `` markers for idempotent updates - - Skip with `--skip-agent-setup` flag -10. Does not modify Claude Desktop configuration; use `run-visiontest.sh` or manual setup for Claude integration. +9. Prints instructions to run `visiontest init --agent ` for project-level agent setup **Security hardening:** `umask 077`, explicit `chmod` on all files/dirs, tag validation, checksum verification, install path restricted to `$HOME`. ## Release Workflow (`.github/workflows/release.yaml`) -Triggered by git tags matching `v*`. The workflow runs the test suite, builds the fat JAR via `shadowJar`, Android APKs, and the pre-built iOS XCUITest bundle (on a macOS runner), generates SHA-256 checksums, and creates a GitHub Release with the following assets: `visiontest.jar`, `visiontest.jar.sha256`, `automation-server.apk`, `automation-server.apk.sha256`, `automation-server-test.apk`, `automation-server-test.apk.sha256`, `ios-automation-server.tar.gz`, `ios-automation-server.tar.gz.sha256`, `AGENT_INSTRUCTIONS.md`, `install.sh`, `run-visiontest.sh`. +Triggered by git tags matching `v*`. The workflow runs the test suite, builds the fat JAR via `shadowJar`, Android APKs, and the pre-built iOS XCUITest bundle (on a macOS runner), generates SHA-256 checksums, and creates a GitHub Release with the following assets: `visiontest.jar`, `visiontest.jar.sha256`, `automation-server.apk`, `automation-server.apk.sha256`, `automation-server-test.apk`, `automation-server-test.apk.sha256`, `ios-automation-server.tar.gz`, `ios-automation-server.tar.gz.sha256`, `install.sh`, `run-visiontest.sh`. All GitHub Actions in both workflows are pinned to commit SHAs for supply-chain security. When updating or adding actions, always use SHA-pinned references instead of floating version tags. diff --git a/install.sh b/install.sh index d19bb2c..31c8452 100755 --- a/install.sh +++ b/install.sh @@ -3,7 +3,6 @@ # Usage: curl -fsSL https://github.com/docer1990/visiontest/releases/latest/download/install.sh | bash # # Flags: -# --skip-agent-setup Skip installing AI agent instructions # --local-jar PATH Use a local JAR instead of downloading from GitHub Releases # (also skips APK/iOS bundle download — useful for testing) # @@ -14,11 +13,9 @@ set -eu umask 077 # ---------- parse flags ---------- -SKIP_AGENT_SETUP=false LOCAL_JAR="" for arg in "$@"; do case "$arg" in - --skip-agent-setup) SKIP_AGENT_SETUP=true ;; --local-jar) _EXPECT_JAR_PATH=true ;; *) if [ "${_EXPECT_JAR_PATH:-}" = "true" ]; then @@ -382,15 +379,6 @@ install_local_jar() { printf 'local-dev\n' > "$RESOLVED_VISIONTEST_HOME/version.txt" chmod 600 "$RESOLVED_VISIONTEST_HOME/version.txt" ok "Installed local JAR to $RESOLVED_VISIONTEST_HOME/visiontest.jar" - - # Copy AGENT_INSTRUCTIONS.md from repo if present alongside install.sh - local SCRIPT_DIR - SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) - if [ -f "$SCRIPT_DIR/AGENT_INSTRUCTIONS.md" ]; then - cp -f "$SCRIPT_DIR/AGENT_INSTRUCTIONS.md" "$RESOLVED_VISIONTEST_HOME/AGENT_INSTRUCTIONS.md" - chmod 600 "$RESOLVED_VISIONTEST_HOME/AGENT_INSTRUCTIONS.md" - ok "Copied AGENT_INSTRUCTIONS.md from repo" - fi } # ---------- create wrapper script ---------- @@ -446,129 +434,6 @@ ensure_path() { export PATH="$BIN_DIR:$PATH" } -# ---------- install AI agent instructions ---------- - -MARKER_BEGIN="" -MARKER_END="" - -download_agent_instructions() { - info "Downloading agent instructions..." - local INSTRUCTIONS_URL="https://github.com/$REPO/releases/download/$LATEST_TAG/AGENT_INSTRUCTIONS.md" - local DEST="$RESOLVED_VISIONTEST_HOME/AGENT_INSTRUCTIONS.md" - - if command -v curl >/dev/null 2>&1; then - curl -fsSL -o "$DEST" "$INSTRUCTIONS_URL" - elif command -v wget >/dev/null 2>&1; then - wget -q -O "$DEST" "$INSTRUCTIONS_URL" - else - warn "Neither curl nor wget found; skipping agent instructions download" - return - fi - chmod 600 "$DEST" -} - -# Appends or replaces the VisionTest instruction block in a target file. -# Uses BEGIN/END markers for idempotent updates. -# Usage: append_with_markers -append_with_markers() { - local TARGET="$1" - local CONTENT="$2" - - if [ -f "$TARGET" ] && grep -qF "$MARKER_BEGIN" "$TARGET"; then - # Replace existing block: remove old markers + content, append new - # Use a temp file to avoid sed -i portability issues (GNU vs BSD) - local TMP - TMP=$(mktemp "${TARGET}.XXXXXX") - awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" ' - $0 == begin { skip=1; next } - $0 == end { skip=0; next } - !skip { print } - ' "$TARGET" > "$TMP" - mv -f "$TMP" "$TARGET" - fi - - # Append the block - { - echo "" - echo "$MARKER_BEGIN" - echo "$CONTENT" - echo "$MARKER_END" - } >> "$TARGET" -} - -install_agent_instructions() { - if [ "$SKIP_AGENT_SETUP" = "true" ]; then - info "Skipping AI agent setup (--skip-agent-setup)" - return - fi - - local INSTRUCTIONS_FILE="$RESOLVED_VISIONTEST_HOME/AGENT_INSTRUCTIONS.md" - if [ ! -f "$INSTRUCTIONS_FILE" ]; then - warn "Agent instructions not found, skipping agent setup" - return - fi - - local INSTRUCTIONS - INSTRUCTIONS=$(cat "$INSTRUCTIONS_FILE") - local AGENTS_CONFIGURED="" - - # --- Claude Code --- - if command -v claude >/dev/null 2>&1; then - local CLAUDE_SKILL_DIR="$HOME/.claude/skills/visiontest-mobile" - mkdir -p "$CLAUDE_SKILL_DIR" - cat > "$CLAUDE_SKILL_DIR/SKILL.md" </dev/null 2>&1; then - local OPENCODE_DIR="$HOME/.config/opencode" - mkdir -p "$OPENCODE_DIR" - local OPENCODE_TARGET="$OPENCODE_DIR/AGENTS.md" - append_with_markers "$OPENCODE_TARGET" "$INSTRUCTIONS" - chmod 644 "$OPENCODE_TARGET" - ok "OpenCode: updated $OPENCODE_TARGET" - AGENTS_CONFIGURED="${AGENTS_CONFIGURED}opencode " - fi - - # --- Codex (OpenAI) --- - if command -v codex >/dev/null 2>&1; then - local CODEX_DIR="$HOME/.codex" - mkdir -p "$CODEX_DIR" - local CODEX_TARGET="$CODEX_DIR/instructions.md" - append_with_markers "$CODEX_TARGET" "$INSTRUCTIONS" - chmod 644 "$CODEX_TARGET" - ok "Codex: updated $CODEX_TARGET" - AGENTS_CONFIGURED="${AGENTS_CONFIGURED}codex " - fi - - # --- GitHub Copilot CLI --- - if command -v gh >/dev/null 2>&1 && gh extension list 2>/dev/null | grep -q "copilot"; then - local COPILOT_DIR="$HOME/.github" - mkdir -p "$COPILOT_DIR" - local COPILOT_TARGET="$COPILOT_DIR/copilot-instructions.md" - append_with_markers "$COPILOT_TARGET" "$INSTRUCTIONS" - chmod 644 "$COPILOT_TARGET" - ok "Copilot: updated $COPILOT_TARGET" - AGENTS_CONFIGURED="${AGENTS_CONFIGURED}copilot " - fi - - if [ -z "$AGENTS_CONFIGURED" ]; then - info "No supported AI coding agents detected (checked: claude, opencode, codex, gh copilot)" - info "You can manually copy $INSTRUCTIONS_FILE into your agent's config" - fi -} - # ---------- main ---------- main() { @@ -587,14 +452,12 @@ main() { download_jar download_apks download_ios_bundle - download_agent_instructions # Disarm the cleanup trap since all downloads succeeded trap - EXIT fi create_wrapper ensure_path - install_agent_instructions echo "" ok "VisionTest $LATEST_TAG installed successfully!" @@ -622,8 +485,10 @@ main() { echo " CLI usage (in any project):" echo " visiontest --help" echo "" + echo " Set up AI agent instructions in a project:" + echo " visiontest init --agent claude,opencode,codex" + echo "" echo " To update later, re-run this script." - echo " To skip agent config: install.sh --skip-agent-setup" echo "" } diff --git a/openspec/changes/add-init-command/.openspec.yaml b/openspec/changes/add-init-command/.openspec.yaml new file mode 100644 index 0000000..4b8c565 --- /dev/null +++ b/openspec/changes/add-init-command/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-21 diff --git a/openspec/changes/add-init-command/design.md b/openspec/changes/add-init-command/design.md new file mode 100644 index 0000000..a594886 --- /dev/null +++ b/openspec/changes/add-init-command/design.md @@ -0,0 +1,55 @@ +## Context + +VisionTest is an MCP server + CLI for mobile automation. The CLI is distributed as a fat JAR via `install.sh`. Currently, `install.sh` also installs AI agent instructions into global home-directory config files. This approach can't be shared via version control and mixes two concerns (binary installation vs. project configuration). + +Three AI coding agents are targeted: Claude Code, OpenCode, and Codex. All three support a project-level SKILL.md format with YAML frontmatter, differing only in the directory path. + +## Goals / Non-Goals + +**Goals:** +- Single `visiontest init --agent ` command for project-level agent setup +- Support Claude Code, OpenCode, and Codex with correct project-level paths +- Embedded instructions (JAR resource) -- no network required +- Idempotent: running twice produces the same result +- Remove agent setup from `install.sh` + +**Non-Goals:** +- Interactive agent selection (menus/prompts) +- Global/home-directory agent setup +- Gemini, Copilot, or other agents (future addition) +- MCP server configuration + +## Decisions + +### 1. Embedded JAR resource over network download +**Decision**: Bundle `AGENT_INSTRUCTIONS.md` as `app/src/main/resources/agent-instructions.md`. +**Rationale**: The file is ~90 lines. Embedding avoids network calls, version mismatch, and failure modes. A Gradle `processResources` sync task keeps the repo-root file as the single source of truth. +**Alternative**: Download from GitHub Release at init time -- rejected due to unnecessary complexity. + +### 2. Unified SKILL.md format for all agents +**Decision**: All three agents get the same SKILL.md content with YAML frontmatter. Only the target directory differs. +**Rationale**: Claude Code, OpenCode, and Codex all support the same `SKILL.md` format with `name` and `description` frontmatter fields. This eliminates per-agent formatting logic. + +### 3. Comma-separated `--agent` flag +**Decision**: `--agent claude,opencode,codex` with comma separation. +**Rationale**: Simpler than repeatable flags, familiar pattern (e.g., `git log --format`). Clikt supports `split(",")` natively on options. + +### 4. No platform flag +**Decision**: `InitCommand` does not require `--platform`. It is registered directly in `VisionTestCli`, not through the platform-gated command structure. +**Rationale**: Agent setup is not a device operation. + +### 5. Agent path mapping + +| Agent | Project path | +|-------|-------------| +| `claude` | `.claude/skills/visiontest/SKILL.md` | +| `opencode` | `.opencode/skills/visiontest/SKILL.md` | +| `codex` | `.agents/skills/visiontest/SKILL.md` | + +Sources: Claude Code docs, OpenCode docs (opencode.ai/docs/skills), Codex docs (developers.openai.com/codex/skills). + +## Risks / Trade-offs + +- **[Agent path changes]** If Claude/OpenCode/Codex change their skill discovery paths, init writes to the wrong location. Mitigation: paths are well-documented standards; adding new paths is a one-line map entry. +- **[Stale instructions after upgrade]** After updating VisionTest, the project SKILL.md still has old content until `init` is re-run. Mitigation: acceptable -- same pattern as any config file. Document in help output. +- **[CWD assumption]** Init writes relative to CWD. If run from the wrong directory, files go to the wrong place. Mitigation: print the absolute path in output so the user sees where files were written. diff --git a/openspec/changes/add-init-command/proposal.md b/openspec/changes/add-init-command/proposal.md new file mode 100644 index 0000000..0d30bf1 --- /dev/null +++ b/openspec/changes/add-init-command/proposal.md @@ -0,0 +1,24 @@ +## Why + +Users need a single, predictable way to configure their AI coding agent with VisionTest CLI instructions at the project level. Currently, `install.sh` writes to global home-directory config files, which can't be shared with teammates via version control. A `visiontest init` command writes project-local config that can be committed and shared. + +## What Changes + +- Add `visiontest init --agent ` CLI command that writes SKILL.md files to the correct project-level paths for each specified agent +- Bundle `AGENT_INSTRUCTIONS.md` as a JAR resource so init works offline +- Remove all agent instruction setup from `install.sh` (the installer only handles the CLI binary) +- Remove `AGENT_INSTRUCTIONS.md` from GitHub Release assets (no longer downloaded separately) + +## Capabilities + +### New Capabilities +- `init-command`: The `visiontest init --agent ` CLI command that writes agent-specific SKILL.md files to project directories + +### Modified Capabilities + +## Impact + +- `install.sh`: Agent setup code removed (~120 lines), `--skip-agent-setup` flag removed +- `app/` module: New `InitCommand.kt`, new resource file, Gradle build task to sync resource +- `.github/workflows/release.yaml`: Remove `AGENT_INSTRUCTIONS.md` from release assets +- Documentation: `CLAUDE.md`, `README.md`, `docs/installation.md` updated to reference `init` diff --git a/openspec/changes/add-init-command/specs/init-command/spec.md b/openspec/changes/add-init-command/specs/init-command/spec.md new file mode 100644 index 0000000..dbdb1ba --- /dev/null +++ b/openspec/changes/add-init-command/specs/init-command/spec.md @@ -0,0 +1,81 @@ +## ADDED Requirements + +### Requirement: Init command writes SKILL.md for specified agents +The system SHALL provide a `visiontest init --agent ` command that writes a SKILL.md file to the correct project-level directory for each specified agent. The `--agent` option SHALL accept a comma-separated list of agent names. + +#### Scenario: Single agent initialization +- **WHEN** user runs `visiontest init --agent claude` in a project directory +- **THEN** the system writes `.claude/skills/visiontest/SKILL.md` with YAML frontmatter and VisionTest CLI instructions + +#### Scenario: Multiple agent initialization +- **WHEN** user runs `visiontest init --agent claude,opencode,codex` +- **THEN** the system writes SKILL.md files to `.claude/skills/visiontest/SKILL.md`, `.opencode/skills/visiontest/SKILL.md`, and `.agents/skills/visiontest/SKILL.md` + +#### Scenario: Output confirmation +- **WHEN** initialization completes successfully +- **THEN** the system prints the path of each written file and a summary count + +### Requirement: Supported agent path mapping +The system SHALL map agent names to project-level paths as follows: +- `claude` → `.claude/skills/visiontest/SKILL.md` +- `opencode` → `.opencode/skills/visiontest/SKILL.md` +- `codex` → `.agents/skills/visiontest/SKILL.md` + +#### Scenario: Claude Code path +- **WHEN** agent is `claude` +- **THEN** SKILL.md is written to `.claude/skills/visiontest/SKILL.md` + +#### Scenario: OpenCode path +- **WHEN** agent is `opencode` +- **THEN** SKILL.md is written to `.opencode/skills/visiontest/SKILL.md` + +#### Scenario: Codex path +- **WHEN** agent is `codex` +- **THEN** SKILL.md is written to `.agents/skills/visiontest/SKILL.md` + +### Requirement: SKILL.md format +Each SKILL.md SHALL contain YAML frontmatter with `name: visiontest` and a `description` field, followed by the VisionTest CLI instructions content. + +#### Scenario: File content structure +- **WHEN** a SKILL.md is written for any agent +- **THEN** the file starts with `---` YAML frontmatter containing `name` and `description`, followed by the CLI instructions markdown + +### Requirement: Instructions embedded as JAR resource +The VisionTest CLI instructions SHALL be bundled inside the JAR as a classpath resource. The `init` command SHALL read instructions from the classpath at runtime without network access. + +#### Scenario: Offline initialization +- **WHEN** user runs `visiontest init --agent claude` without network connectivity +- **THEN** initialization succeeds using the embedded resource + +### Requirement: Idempotent initialization +Running `visiontest init` multiple times with the same arguments SHALL produce the same result. Existing SKILL.md files SHALL be overwritten with current content. + +#### Scenario: Re-running init +- **WHEN** user runs `visiontest init --agent claude` twice in the same directory +- **THEN** the SKILL.md file contains the same content as after the first run + +### Requirement: Error handling for invalid agent names +The system SHALL reject unknown agent names and print an error listing valid options. + +#### Scenario: Unknown agent name +- **WHEN** user runs `visiontest init --agent gemini` +- **THEN** the system prints an error with the list of valid agent names and exits with code 2 + +#### Scenario: Missing agent flag +- **WHEN** user runs `visiontest init` without `--agent` +- **THEN** the system prints usage help and exits with code 2 + +### Requirement: No platform flag required +The `init` command SHALL NOT require the `--platform` flag since agent setup is not a device operation. + +#### Scenario: Init without platform +- **WHEN** user runs `visiontest init --agent claude` +- **THEN** the command succeeds without requiring `--platform` + +### Requirement: Remove agent setup from install.sh +The installer script SHALL NOT install agent instructions. All agent instruction functions, markers, and the `--skip-agent-setup` flag SHALL be removed from `install.sh`. The installer summary SHALL reference `visiontest init` for agent setup. + +#### Scenario: Clean install without agent setup +- **WHEN** user runs `install.sh` +- **THEN** no agent config files are created in the home directory or project +- **THEN** the summary output mentions `visiontest init` for agent configuration diff --git a/openspec/changes/add-init-command/tasks.md b/openspec/changes/add-init-command/tasks.md new file mode 100644 index 0000000..812fa6c --- /dev/null +++ b/openspec/changes/add-init-command/tasks.md @@ -0,0 +1,39 @@ +## 1. Embed Instructions as JAR Resource + +- [x] 1.1 Add Gradle `processResources` sync task to copy `AGENT_INSTRUCTIONS.md` from repo root to `app/src/main/resources/agent-instructions.md` +- [x] 1.2 Verify resource loads from classpath in a unit test + +## 2. Implement InitCommand + +- [x] 2.1 Create `InitCommand.kt` Clikt command with `--agent` option (comma-separated, validated against `claude`, `opencode`, `codex`) +- [x] 2.2 Implement agent path mapping (claude → `.claude/skills/visiontest/SKILL.md`, opencode → `.opencode/skills/visiontest/SKILL.md`, codex → `.agents/skills/visiontest/SKILL.md`) +- [x] 2.3 Implement SKILL.md writing with YAML frontmatter (`name`, `description`) + instructions content +- [x] 2.4 Print confirmation output per agent and summary count +- [x] 2.5 Register `InitCommand` in `VisionTestCli` + +## 3. Tests + +- [x] 3.1 Unit test: valid single agent writes correct file to correct path +- [x] 3.2 Unit test: comma-separated multiple agents writes all files +- [x] 3.3 Unit test: invalid agent name produces error with exit code 2 +- [x] 3.4 Unit test: missing `--agent` flag produces error with exit code 2 +- [x] 3.5 Unit test: idempotent overwrite (run twice, same content) +- [x] 3.6 Run full test suite: `./gradlew :app:test` + +## 4. Remove Agent Setup from install.sh + +- [x] 4.1 Remove `download_agent_instructions()`, `install_agent_instructions()`, `append_with_markers()` functions and related constants/variables +- [x] 4.2 Remove `--skip-agent-setup` flag parsing +- [x] 4.3 Remove `download_agent_instructions` and `install_agent_instructions` calls from `main()` +- [x] 4.4 Update summary output to mention `visiontest init --agent ` for agent setup +- [x] 4.5 Syntax-check install.sh: `bash -n install.sh` + +## 5. Remove AGENT_INSTRUCTIONS.md from Release Workflow + +- [x] 5.1 Remove `AGENT_INSTRUCTIONS.md` from `.github/workflows/release.yaml` release assets + +## 6. Update Documentation + +- [x] 6.1 Update `CLAUDE.md` to document `init` command +- [x] 6.2 Update `README.md` to document `init` command +- [x] 6.3 Update `docs/installation.md` to reference `init` instead of install.sh agent setup