diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb773d3..a3a53a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,36 +18,42 @@ jobs: # Linux x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-gnu + bun_target: bun-linux-x64 artifact_name: capa asset_name: capa-x86_64-unknown-linux-gnu # Linux aarch64 - os: ubuntu-latest target: aarch64-unknown-linux-gnu + bun_target: bun-linux-arm64 artifact_name: capa asset_name: capa-aarch64-unknown-linux-gnu - # macOS x86_64 + # macOS x86_64 (Intel) — cross-compiled via --target on Apple Silicon runners - os: macos-latest target: x86_64-apple-darwin + bun_target: bun-darwin-x64 artifact_name: capa asset_name: capa-x86_64-apple-darwin # macOS aarch64 (Apple Silicon) - os: macos-latest target: aarch64-apple-darwin + bun_target: bun-darwin-arm64 artifact_name: capa asset_name: capa-aarch64-apple-darwin # Windows x86_64 - os: windows-latest target: x86_64-pc-windows-msvc + bun_target: bun-windows-x64 artifact_name: capa.exe asset_name: capa-x86_64-pc-windows-msvc.exe # Windows aarch64 - os: windows-latest target: aarch64-pc-windows-msvc + bun_target: bun-windows-arm64 artifact_name: capa.exe asset_name: capa-aarch64-pc-windows-msvc.exe @@ -74,11 +80,13 @@ jobs: GITHUB_REF: ${{ github.ref }} - name: Build + env: + BUN_TARGET: ${{ matrix.bun_target }} run: | if [ "$RUNNER_OS" == "Windows" ]; then - bun run build:win + bun build src/cli/index.ts --compile --target=$BUN_TARGET --windows-icon=./logo.ico --outfile dist/capa else - bun run build + bun build src/cli/index.ts --compile --target=$BUN_TARGET --outfile dist/capa --icon logo.ico fi shell: bash diff --git a/install.ps1 b/install.ps1 index 9453fdf..1ece031 100644 --- a/install.ps1 +++ b/install.ps1 @@ -263,20 +263,55 @@ function Install-Capa { $binaryName = "capa-$arch.exe" $downloadUrl = "https://github.com/$GITHUB_REPO/releases/download/v$APP_VERSION/$binaryName" $destPath = Join-Path $installDir "capa.exe" + # Download to temp first so we can replace the running exe when upgrading (fixes #19) + $tempPath = Join-Path $env:TEMP "capa-$APP_VERSION-$arch.exe" Write-Info "Downloading CAPA..." Write-Verbose-Custom "URL: $downloadUrl" try { - # Download binary $ProgressPreference = 'SilentlyContinue' - Invoke-WebRequest -Uri $downloadUrl -OutFile $destPath -UseBasicParsing + Invoke-WebRequest -Uri $downloadUrl -OutFile $tempPath -UseBasicParsing Write-Success "Downloaded CAPA binary" } catch { Write-Error-Custom "Failed to download CAPA from $downloadUrl`n$_" } + # Replace existing binary. If dest is in use (e.g. capa upgrade), defer replace until process exits. + $replaceOk = $false + try { + Move-Item -Path $tempPath -Destination $destPath -Force + $replaceOk = $true + } + catch { + if ($_.Exception.Message -match 'being used by another process') { + $upgradePid = $env:CAPA_UPGRADE_PID + if ($upgradePid) { + Write-Info "Current capa is running; will replace binary after it exits (PID $upgradePid)..." + $deferScript = @" +try { + `$p = Get-Process -Id $upgradePid -ErrorAction SilentlyContinue + if (`$p) { `$p.WaitForExit(120000) } + Move-Item -LiteralPath '$tempPath' -Destination '$destPath' -Force + Remove-Item -LiteralPath '$tempPath' -Force -ErrorAction SilentlyContinue +} catch { } +"@ + Start-Process powershell.exe -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-Command', $deferScript + $replaceOk = $true + Write-Success "Upgrade will complete when the current capa process exits. Restart your terminal and run capa again." + } + } + if (-not $replaceOk) { + Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue + Write-Error-Custom "Could not replace $destPath. Close any process using it and run the installer again, or run: capa upgrade" + } + } + + if (-not $replaceOk) { + exit 1 + } + Write-Success "Installed CAPA to $destPath" # Add to PATH diff --git a/skills/capabilities-manager/SKILL.md b/skills/capabilities-manager/SKILL.md index dd09d77..1576553 100644 --- a/skills/capabilities-manager/SKILL.md +++ b/skills/capabilities-manager/SKILL.md @@ -600,8 +600,9 @@ When no version or SHA is specified capa fetches from `HEAD` (the default branch | Field | Description | |---|---| | `agents.base.ref` | Raw URL of a remote markdown file — used when `type` is `remote` or omitted. Re-running install always re-downloads and refreshes it. | -| `agents.base.type` | `remote` (default when `ref` is set), `github`, or `gitlab`. Use `github`/`gitlab` together with `def.repo`. | +| `agents.base.type` | `remote` (default when `ref` is set), `github`, `gitlab`, or `local`. Use `github`/`gitlab` with `def.repo`; use `local` with `path` for a file relative to the capabilities file. | | `agents.base.def.repo` | Repository + file for `github`/`gitlab` base. Same `owner/repo@filepath` format as snippet `def.repo`. | +| `agents.base.path` | Path to a local markdown file when `type` is `local`. Relative to the directory containing the capabilities file. | | `agents.additional[].id` | Unique identifier used as the capa marker id. Required for `inline`/`remote`; optional for `github`/`gitlab` (derived from the filepath, e.g. `docs_tips_md`). | #### Full example diff --git a/src/cli/commands/upgrade.ts b/src/cli/commands/upgrade.ts index 1f0fe76..d4b94e5 100644 --- a/src/cli/commands/upgrade.ts +++ b/src/cli/commands/upgrade.ts @@ -22,9 +22,12 @@ export async function upgradeCommand(): Promise { async function upgradeWindows(): Promise { console.log('Running Windows installation script...\n'); + // So install.ps1 can defer replacing the exe until this process exits (fixes #19) + const env = { ...process.env, CAPA_UPGRADE_PID: String(process.pid) }; + const proc = Bun.spawn( ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-Command', `irm '${INSTALL_PS1_URL}' | iex`], - { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } + { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit', env } ); const exitCode = await proc.exited; diff --git a/src/cli/utils/agents-file.ts b/src/cli/utils/agents-file.ts index 9765587..68a3f47 100644 --- a/src/cli/utils/agents-file.ts +++ b/src/cli/utils/agents-file.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; -import { join } from 'path'; +import { join, resolve, dirname } from 'path'; import type { AgentFileConfig, SecurityOptions } from '../../types/capabilities'; import { loadBlockedPhrases, @@ -324,7 +324,27 @@ export async function installAgentsFile( let baseContent: string; const baseType = config.base.type ?? (config.base.ref ? 'remote' : undefined); - if (baseType === 'github' || baseType === 'gitlab') { + if (baseType === 'local') { + if (!config.base.path) { + throw new Error( + `agents.base with type 'local' requires a "path" field (e.g. "path: ./docs/AGENTS-base.md").` + ); + } + if (!capabilitiesFilePath) { + throw new Error( + `agents.base type 'local' requires the capabilities file path to resolve relative paths.` + ); + } + const capabilitiesDir = dirname(capabilitiesFilePath); + const resolvedPath = resolve(capabilitiesDir, config.base.path); + if (!existsSync(resolvedPath)) { + throw new Error( + `agents.base local file not found: ${resolvedPath} (resolved from path "${config.base.path}")` + ); + } + console.log(` Using base agents file from ${resolvedPath}`); + baseContent = readFileSync(resolvedPath, 'utf8'); + } else if (baseType === 'github' || baseType === 'gitlab') { if (!config.base.def?.repo) { throw new Error( `agents.base with type '${baseType}' requires a "def.repo" field ` + @@ -340,7 +360,7 @@ export async function installAgentsFile( baseContent = await fetchRemoteContent(config.base.ref); } else { throw new Error( - `agents.base requires either a "ref" URL or a "type: github/gitlab" ` + + `agents.base requires a "ref" URL, "type: local" with "path", or "type: github/gitlab" ` + `with a "def.repo" field.` ); } diff --git a/src/types/capabilities.ts b/src/types/capabilities.ts index 22aea58..d4eb2c5 100644 --- a/src/types/capabilities.ts +++ b/src/types/capabilities.ts @@ -96,23 +96,29 @@ export interface AgentSnippet { */ /** * Source definition for the base agent instructions file. - * Supports the same source types as snippets (remote, github, gitlab). + * Supports the same source types as snippets (remote, github, gitlab) plus local file. * * Examples: * ref: https://raw.githubusercontent.com/org/repo/main/AGENTS.md # remote URL * type: github / def.repo: org/repo@AGENTS.md # GitHub file * type: gitlab / def.repo: group/repo@AGENTS.md:v1.0.0 # GitLab file, pinned + * type: local / path: ./docs/AGENTS-base.md # local file (relative to capabilities file) */ export interface AgentFileBase { /** * Source type. Defaults to 'remote' when `ref` is set and `type` is omitted. - * Use 'github' or 'gitlab' together with `def.repo` for repository-hosted files. + * Use 'github' or 'gitlab' together with `def.repo`, or 'local' with `path`. */ - type?: 'remote' | 'github' | 'gitlab'; + type?: 'remote' | 'github' | 'gitlab' | 'local'; /** Raw URL — used when type is 'remote' (or when type is omitted and ref is present). */ ref?: string; /** Repository + file definition for github/gitlab types. */ def?: AgentSnippetDef; + /** + * Path to a local markdown file. Used when type is 'local'. + * Relative paths are resolved from the directory containing the capabilities file. + */ + path?: string; } /**