Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
39 changes: 37 additions & 2 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion skills/capabilities-manager/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/cli/commands/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export async function upgradeCommand(): Promise<void> {
async function upgradeWindows(): Promise<void> {
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;
Expand Down
26 changes: 23 additions & 3 deletions src/cli/utils/agents-file.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 ` +
Expand All @@ -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.`
);
}
Expand Down
12 changes: 9 additions & 3 deletions src/types/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Loading