Skip to content
Closed
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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: ci

on:
push:
branches: [main]
pull_request:

jobs:
test:
name: test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Lint (tsc --noEmit)
run: npm run lint

- name: Test
run: npm test

- name: Build
run: npm run build
10 changes: 10 additions & 0 deletions .github/workflows/release-binary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ jobs:
- os: macos-latest
target: darwin-x64
node-arch: x64
# Windows bundles are built on ubuntu-latest because esbuild output
# is platform-agnostic JS — the bundle runs under Node on any OS.
# install.ps1 generates the accompanying axme-code.cmd wrapper at
# install time so we ship a single file per Windows arch.
- os: ubuntu-latest
target: windows-x64
node-arch: x64
- os: ubuntu-latest
target: windows-arm64
node-arch: arm64

runs-on: ${{ matrix.os }}
permissions:
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,19 @@ Decisions enforce verification requirements: agent must run tests and show proof

**Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (CLI or VS Code extension).**

**Linux / macOS:**
```bash
curl -fsSL https://raw.githubusercontent.com/AxmeAI/axme-code/main/install.sh | bash
```
Installs to `~/.local/bin/axme-code`. Supports x64 and ARM64.

Installs to `~/.local/bin/axme-code`. Supports Linux and macOS (x64 and ARM64).
**Windows (native):**
```powershell
irm https://raw.githubusercontent.com/AxmeAI/axme-code/main/install.ps1 | iex
```
Installs to `%LOCALAPPDATA%\Programs\axme-code` and adds it to your User PATH. Requires Node.js 20+ on PATH. Supports x64 and ARM64.

**Windows via WSL2** is supported. Install a WSL2 distro (`wsl --install -d Ubuntu-22.04`), then install both Claude Code and axme-code **inside** your WSL distronot on the Windows host. Native Windows is not yet supported.
**Windows via WSL2:** if you already live in WSL2, use the Linux install one-liner inside your distro. Install Claude Code and axme-code **inside** the WSL distro, not on the Windows host.

### Setup

Expand Down
27 changes: 20 additions & 7 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ await build({
define,
});

// Create bin wrapper
// Create bin wrappers — POSIX shebang entry + Windows .cmd wrapper. Shipping
// both means install.sh/install.ps1 can place them side-by-side on any
// platform; the one that matches the shell wins.
import { writeFileSync, chmodSync, mkdirSync } from "fs";
writeFileSync("dist/axme-code.js", '#!/usr/bin/env node\nimport("./cli.mjs");\n');
chmodSync("dist/axme-code.js", 0o755);
// Windows CMD wrapper — forwards all args to node + axme-code.js. %~dp0
// resolves to the directory of the .cmd at runtime so this works regardless
// of cwd or PATH entry style.
writeFileSync("dist/axme-code.cmd", "@echo off\r\nnode \"%~dp0axme-code.js\" %*\r\n");

// --- Plugin bundled builds (self-contained, zero external deps) ---

Expand Down Expand Up @@ -64,13 +70,15 @@ await build({
define,
});

// Plugin bin wrapper — sets NODE_PATH so SDK can be found from CLAUDE_PLUGIN_DATA
// Plugin bin wrappers — POSIX bash script + Windows .cmd. Both forward to
// node + the plugin's bundled cli.mjs, located one directory up from bin/.
mkdirSync("dist/plugin/bin", { recursive: true });
writeFileSync("dist/plugin/bin/axme-code", `#!/bin/bash
PLUGIN_DIR="\$(cd "\$(dirname "\$0")/.." && pwd)"
exec node "\$PLUGIN_DIR/cli.mjs" "\$@"
`);
chmodSync("dist/plugin/bin/axme-code", 0o755);
writeFileSync("dist/plugin/bin/axme-code.cmd", "@echo off\r\nnode \"%~dp0..\\cli.mjs\" %*\r\n");

// Plugin package.json — only SDK for npm install in CLAUDE_PLUGIN_DATA
writeFileSync("dist/plugin/package.json", JSON.stringify({
Expand Down Expand Up @@ -100,36 +108,41 @@ writeFileSync("dist/plugin/.mcp.json", JSON.stringify({
},
}, null, 2) + "\n");

// Plugin hooks — safety enforcement via bundled CLI
// Plugin hooks — safety enforcement via bundled CLI. All commands quote the
// ${CLAUDE_PLUGIN_ROOT} expansion so paths with spaces survive sh -c and
// cmd.exe /c unchanged. The SessionStart hook used to shell out to `test -d
// ... || (cd ... && npm install)` which was POSIX-only; the lazy SDK
// install is now inside the `check-init` subcommand so this command is a
// plain Node invocation and works on Windows natively.
writeFileSync("dist/plugin/hooks/hooks.json", JSON.stringify({
description: "AXME Code safety enforcement and session tracking",
hooks: {
SessionStart: [{
hooks: [{
type: "command",
command: "test -d ${CLAUDE_PLUGIN_ROOT}/node_modules/@anthropic-ai/claude-agent-sdk || (cd ${CLAUDE_PLUGIN_ROOT} && npm install --omit=dev --ignore-scripts 2>/dev/null) ; node ${CLAUDE_PLUGIN_ROOT}/cli.mjs check-init",
command: 'node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" check-init',
timeout: 30,
}],
}],
PreToolUse: [{
hooks: [{
type: "command",
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook pre-tool-use",
command: 'node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" hook pre-tool-use',
timeout: 5,
}],
}],
PostToolUse: [{
matcher: "Edit|Write|NotebookEdit",
hooks: [{
type: "command",
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook post-tool-use",
command: 'node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" hook post-tool-use',
timeout: 10,
}],
}],
SessionEnd: [{
hooks: [{
type: "command",
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook session-end",
command: 'node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" hook session-end',
timeout: 120,
}],
}],
Expand Down
109 changes: 109 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# AXME Code - Windows installer
#
# Downloads the axme-code standalone Node bundle from GitHub Releases,
# places it under %LOCALAPPDATA%\Programs\axme-code\ along with a .cmd
# wrapper, and adds that directory to the User PATH.
#
# Usage:
# iwr -useb https://raw.githubusercontent.com/AxmeAI/axme-code/main/install.ps1 | iex
# Or: irm https://raw.githubusercontent.com/AxmeAI/axme-code/main/install.ps1 | iex
#
# Requires: Windows PowerShell 5.1+ or PowerShell 7+, Node.js 20+ on PATH.

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$Repo = if ($env:AXME_REPO) { $env:AXME_REPO } else { 'AxmeAI/axme-code' }
$InstallDir = if ($env:AXME_INSTALL_DIR) { $env:AXME_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'Programs\axme-code' }

function Get-Arch {
$arch = $env:PROCESSOR_ARCHITECTURE
if ($arch -eq 'ARM64') { return 'arm64' }
if ($arch -eq 'AMD64') { return 'x64' }
if ($arch -eq 'x86') { throw "32-bit Windows is not supported. axme-code requires x64 or arm64." }
throw "Unsupported PROCESSOR_ARCHITECTURE: $arch"
}

function Get-LatestTag {
$url = "https://api.github.com/repos/$Repo/releases/latest"
try {
$release = Invoke-RestMethod -Uri $url -Headers @{ 'User-Agent' = 'axme-code-installer' }
return $release.tag_name
} catch {
throw "Failed to fetch latest release from $url : $($_.Exception.Message)"
}
}

function Test-Node {
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) {
Write-Warning "node.exe not found on PATH. Install Node.js 20+ from https://nodejs.org before running axme-code."
return
}
try {
$versionLine = & node --version 2>$null
if ($versionLine -match 'v(\d+)') {
$major = [int]$Matches[1]
if ($major -lt 20) {
Write-Warning "Found Node $versionLine but axme-code requires Node 20+."
}
}
} catch { }
}

# --- Main -----------------------------------------------------------------

$arch = Get-Arch
$platform = "windows-$arch"

$version = if ($args.Count -ge 1 -and $args[0]) { $args[0] } else { Get-LatestTag }
if (-not $version) { throw 'Could not determine version. Specify as first argument, e.g. install.ps1 v0.2.9' }

Write-Host "Installing axme-code $version ($platform) to $InstallDir..."

$null = New-Item -ItemType Directory -Path $InstallDir -Force

$downloadUrl = "https://github.com/$Repo/releases/download/$version/axme-code-$platform"
$jsTarget = Join-Path $InstallDir 'axme-code.js'
$cmdTarget = Join-Path $InstallDir 'axme-code.cmd'

Write-Host "Downloading $downloadUrl..."
try {
Invoke-WebRequest -Uri $downloadUrl -OutFile $jsTarget -UseBasicParsing
} catch {
throw "Download failed: $($_.Exception.Message). Check that release $version has asset axme-code-$platform."
}

# Generate the .cmd wrapper. %~dp0 expands to the directory of the .cmd at
# runtime (with trailing backslash), so this works regardless of how the
# user put the install dir on PATH.
$cmdContent = "@echo off`r`nnode `"%~dp0axme-code.js`" %*`r`n"
Set-Content -Path $cmdTarget -Value $cmdContent -NoNewline -Encoding ASCII

# Add install dir to User PATH if not already present. [Environment]::SetEnvironmentVariable
# writes to the registry so it persists across sessions; the current session
# gets updated via $env:Path.
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($null -eq $userPath) { $userPath = '' }
$pathEntries = $userPath -split ';' | Where-Object { $_ -ne '' }
if ($pathEntries -notcontains $InstallDir) {
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
$env:Path = "$env:Path;$InstallDir"
Write-Host "Added $InstallDir to User PATH."
} else {
Write-Host "$InstallDir already on User PATH."
}

Test-Node

Write-Host ''
Write-Host "Installed axme-code to $jsTarget"
Write-Host "Wrapper at $cmdTarget"
Write-Host ''
Write-Host 'Get started:'
Write-Host ' cd your-project'
Write-Host ' axme-code setup'
Write-Host ''
Write-Host 'Note: if the axme-code command is not found, open a new terminal so PATH refreshes.'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "node build.mjs",
"start": "node dist/server.js",
"dev": "tsx src/cli.ts",
"test": "tsx --test test/*.test.ts",
"test": "node scripts/run-tests.mjs",
"lint": "tsc --noEmit"
},
"engines": {
Expand Down
37 changes: 37 additions & 0 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
/**
* Cross-platform test runner. Enumerates test/*.test.ts and spawns tsx --test
* with explicit file paths. Replaces the shell-glob pattern in the npm test
* script, which fails on Windows because cmd.exe/PowerShell don't expand *.
*/

import { readdirSync } from "node:fs";
import { spawn } from "node:child_process";
import { join, resolve } from "node:path";

const testDir = resolve("test");
const files = readdirSync(testDir)
.filter((f) => f.endsWith(".test.ts"))
.sort()
.map((f) => join(testDir, f));

if (files.length === 0) {
console.error(`No .test.ts files found in ${testDir}`);
process.exit(1);
}

const isWin = process.platform === "win32";
const tsx = isWin ? "npx.cmd" : "npx";
const child = spawn(tsx, ["tsx", "--test", ...files], {
stdio: "inherit",
shell: isWin,
});

child.on("exit", (code) => {
process.exit(code ?? 0);
});

child.on("error", (err) => {
console.error(`Failed to start tsx: ${err.message}`);
process.exit(1);
});
Loading
Loading