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
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ jobs:
# Copy config directories (same as package.json "files" for binary distribution)
cp -r .claude config-staging/
cp -r .opencode config-staging/
cp .mcp.json config-staging/
mkdir -p config-staging/.github
cp -r .github/skills config-staging/.github/
cp -r .github/agents config-staging/.github/
cp .github/mcp-config.json config-staging/.github/

# Remove node_modules from .opencode if present
rm -rf config-staging/.opencode/node_modules
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ atomic init

Select your agent. The CLI configures your project automatically.

`atomic init` configures source-control-specific skills in your project (GitHub/Git or Sapling), while Atomic's baseline agents/skills are installed globally under `~/.atomic/.claude`, `~/.atomic/.opencode`, and `~/.atomic/.copilot` during install/update (including `bun install` for editable/package installs).

Then start a chat session and run `/init` to generate `CLAUDE.md` and `AGENTS.md`:

```bash
Expand All @@ -211,7 +213,7 @@ During `atomic init`, you'll be prompted to select your source control system:
| GitHub / Git | `git` | Pull Requests | Most open-source projects |
| Sapling + Phabricator | `sl` | Phabricator Diffs | Meta-style stacked workflows |

The selection is saved to `.atomic.json` in your project root and configures the appropriate commit and code review commands for your workflow.
The selection is saved to `.atomic/settings.json` in your project and configures the appropriate commit and code review commands for your workflow.

#### Sapling + Phabricator Setup

Expand Down Expand Up @@ -507,11 +509,18 @@ atomic chat -a opencode --theme <light/dark>

## Configuration Files

### `.atomic.json`
### `.atomic/settings.json`

Atomic stores project-level configuration in `.atomic/settings.json`. This file is created automatically during `atomic init`.

Configuration resolution for project defaults:

1. Local override: `.atomic/settings.json`
2. Global fallback: `~/.atomic/settings.json`

Atomic stores project-level configuration in `.atomic.json` at the root of your project. This file is created automatically during `atomic init`.
Atomic no longer reads or writes `.atomic.json`.

**Example `.atomic.json`:**
**Example `.atomic/settings.json`:**

```json
{
Expand Down Expand Up @@ -591,6 +600,7 @@ The uninstall command will:

- Remove the Atomic binary from `~/.local/bin/atomic` (or your custom install directory)
- Remove configuration data from `~/.local/share/atomic` (unless `--keep-config` is used)
- Remove Atomic-managed global agent configs from `~/.atomic/.claude`, `~/.atomic/.opencode`, and `~/.atomic/.copilot` (unless `--keep-config` is used)
- Display instructions for removing the PATH entry from your shell configuration

### Native installation (manual)
Expand All @@ -602,6 +612,7 @@ If the CLI command is not available, you can manually remove the files:
```bash
rm -f ~/.local/bin/atomic
rm -rf ~/.local/share/atomic
rm -rf ~/.atomic/.claude ~/.atomic/.opencode ~/.atomic/.copilot
```

If you installed to a custom directory, remove the binary from that location instead.
Expand All @@ -611,6 +622,9 @@ If you installed to a custom directory, remove the binary from that location ins
```powershell
Remove-Item "$env:USERPROFILE\.local\bin\atomic.exe" -Force
Remove-Item "$env:LOCALAPPDATA\atomic" -Recurse -Force
Remove-Item "$env:USERPROFILE\.atomic\.claude" -Recurse -Force
Remove-Item "$env:USERPROFILE\.atomic\.opencode" -Recurse -Force
Remove-Item "$env:USERPROFILE\.atomic\.copilot" -Recurse -Force
```

### bun installation
Expand Down
39 changes: 39 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ $GithubRepo = "flora131/atomic"
$BinaryName = "atomic"
$BinDir = if ($env:ATOMIC_INSTALL_DIR) { $env:ATOMIC_INSTALL_DIR } elseif ($InstallDir) { $InstallDir } else { "${Home}\.local\bin" }
$DataDir = if ($env:LOCALAPPDATA) { "${env:LOCALAPPDATA}\atomic" } else { "${Home}\AppData\Local\atomic" }
$AtomicHome = "${Home}\.atomic"

function Sync-GlobalAgentConfigs {
param([string]$SourceRoot)

$claudeDir = Join-Path $AtomicHome ".claude"
$opencodeDir = Join-Path $AtomicHome ".opencode"
$copilotDir = Join-Path $AtomicHome ".copilot"

$null = New-Item -ItemType Directory -Force -Path $claudeDir
$null = New-Item -ItemType Directory -Force -Path $opencodeDir
$null = New-Item -ItemType Directory -Force -Path $copilotDir

Copy-Item -Path (Join-Path $SourceRoot ".claude\*") -Destination $claudeDir -Recurse -Force
Copy-Item -Path (Join-Path $SourceRoot ".opencode\*") -Destination $opencodeDir -Recurse -Force
Copy-Item -Path (Join-Path $SourceRoot ".github\*") -Destination $copilotDir -Recurse -Force

$mcpConfigSource = Join-Path $SourceRoot ".mcp.json"
if (Test-Path $mcpConfigSource) {
Copy-Item -Path $mcpConfigSource -Destination (Join-Path $AtomicHome ".mcp.json") -Force
}

foreach ($agentDir in @($claudeDir, $opencodeDir, $copilotDir)) {
$skillsDir = Join-Path $agentDir "skills"
if (Test-Path $skillsDir) {
Get-ChildItem -Path $skillsDir -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "gh-*" -or $_.Name -like "sl-*" } |
ForEach-Object { Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue }
}
}

Remove-Item -Recurse -Force (Join-Path $copilotDir "workflows") -ErrorAction SilentlyContinue
Remove-Item -Force (Join-Path $copilotDir "dependabot.yml") -ErrorAction SilentlyContinue
}

# Colors for output
$C_RESET = [char]27 + "[0m"
Expand Down Expand Up @@ -43,6 +77,7 @@ switch ($Arch) {
Write-Info "Detected architecture: $Arch"
Write-Info "Installing to: $BinDir"
Write-Info "Config directory: $DataDir"
Write-Info "Atomic home: $AtomicHome"

# Create install directories
$null = New-Item -ItemType Directory -Force -Path $BinDir
Expand Down Expand Up @@ -142,6 +177,9 @@ try {
$null = New-Item -ItemType Directory -Force -Path $DataDir
Expand-Archive -Path $TempConfig -DestinationPath $DataDir -Force

Write-Info "Syncing global agent configs to ${AtomicHome}..."
Sync-GlobalAgentConfigs -SourceRoot $DataDir

# Verify installation
$VersionOutput = & $BinaryPath --version 2>&1
if ($LASTEXITCODE -ne 0) {
Expand All @@ -150,6 +188,7 @@ try {

Write-Success "Installed ${BinaryName} ${Version} to ${BinaryPath}"
Write-Success "Config files installed to ${DataDir}"
Write-Success "Global agent configs synced to ${AtomicHome}"

# Update PATH
if (-not $NoPathUpdate) {
Expand Down
30 changes: 30 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ GITHUB_REPO="flora131/atomic"
BINARY_NAME="atomic"
BIN_DIR="${ATOMIC_INSTALL_DIR:-$HOME/.local/bin}"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/atomic"
ATOMIC_HOME="$HOME/.atomic"

# Colors
RED='\033[0;31m'
Expand Down Expand Up @@ -138,6 +139,31 @@ verify_checksum() {
info "Checksum verified successfully"
}

# Sync bundled config templates into ~/.atomic for global discovery
# Excludes SCM-specific skills (gh-*, sl-*), which are configured per-project via `atomic init`.
sync_global_agent_configs() {
local source_root="$1"

mkdir -p "$ATOMIC_HOME/.claude" "$ATOMIC_HOME/.opencode" "$ATOMIC_HOME/.copilot"

cp -R "$source_root/.claude/." "$ATOMIC_HOME/.claude/"
cp -R "$source_root/.opencode/." "$ATOMIC_HOME/.opencode/"
cp -R "$source_root/.github/." "$ATOMIC_HOME/.copilot/"

if [[ -f "$source_root/.mcp.json" ]]; then
cp "$source_root/.mcp.json" "$ATOMIC_HOME/.mcp.json"
fi

# Remove SCM-managed skills from global config; these are project-scoped.
rm -rf "$ATOMIC_HOME/.claude/skills/gh-"* "$ATOMIC_HOME/.claude/skills/sl-"* 2>/dev/null || true
rm -rf "$ATOMIC_HOME/.opencode/skills/gh-"* "$ATOMIC_HOME/.opencode/skills/sl-"* 2>/dev/null || true
rm -rf "$ATOMIC_HOME/.copilot/skills/gh-"* "$ATOMIC_HOME/.copilot/skills/sl-"* 2>/dev/null || true

# Keep Copilot global config focused on skills/agents/instructions/MCP.
rm -rf "$ATOMIC_HOME/.copilot/workflows" 2>/dev/null || true
rm -f "$ATOMIC_HOME/.copilot/dependabot.yml" 2>/dev/null || true
}

# Get latest version
get_latest_version() {
curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" |
Expand Down Expand Up @@ -204,12 +230,16 @@ main() {
mkdir -p "$DATA_DIR"
tar -xzf "${tmp_dir}/${BINARY_NAME}-config.tar.gz" -C "$DATA_DIR"

info "Syncing global agent configs to ${ATOMIC_HOME}..."
sync_global_agent_configs "$DATA_DIR"

# Verify installation
"${BIN_DIR}/${BINARY_NAME}" --version >/dev/null 2>&1 ||
error "Installation verification failed"

success "Installed ${BINARY_NAME} ${version} to ${BIN_DIR}/${BINARY_NAME}"
success "Config files installed to ${DATA_DIR}"
success "Global agent configs synced to ${ATOMIC_HOME}"

# Update PATH in shell config
if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
"src",
".claude",
".opencode",
".mcp.json",
".github/skills",
".github/agents"
".github/agents",
".github/mcp-config.json"
],
"scripts": {
"dev": "bun run src/cli.ts",
Expand All @@ -33,7 +35,7 @@
"typecheck": "tsc --noEmit",
"lint": "oxlint --config=oxlint.json src",
"lint:fix": "oxlint --config=oxlint.json --fix src",
"postinstall": "lefthook install"
"postinstall": "lefthook install && bun run src/scripts/postinstall.ts"
},
"devDependencies": {
"@types/bun": "^1.3.9",
Expand Down
50 changes: 50 additions & 0 deletions src/commands/chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { hasProjectScmSkills, shouldAutoInitChat } from "./chat.ts";

async function withTempDir(run: (dir: string) => Promise<void>): Promise<void> {
const dir = await mkdtemp(join(tmpdir(), "atomic-chat-test-"));
try {
await run(dir);
} finally {
await rm(dir, { recursive: true, force: true });
}
}

test("hasProjectScmSkills returns false when skills directory has no managed SCM skills", async () => {
await withTempDir(async (dir) => {
await mkdir(join(dir, ".github", "skills", "init"), { recursive: true });
await writeFile(join(dir, ".github", "skills", "init", "SKILL.md"), "init", "utf-8");

await expect(hasProjectScmSkills("copilot", dir)).resolves.toBe(false);
});
});

test("hasProjectScmSkills returns true when managed SCM skill exists", async () => {
await withTempDir(async (dir) => {
const commitSkillPath = join(dir, ".github", "skills", "gh-commit", "SKILL.md");
await mkdir(join(commitSkillPath, ".."), { recursive: true });
await writeFile(commitSkillPath, "commit skill", "utf-8");

await expect(hasProjectScmSkills("copilot", dir)).resolves.toBe(true);
});
});

test("shouldAutoInitChat returns true when no managed SCM skills are configured", async () => {
await withTempDir(async (dir) => {
await mkdir(join(dir, ".claude"), { recursive: true });
await expect(shouldAutoInitChat("claude", dir)).resolves.toBe(true);
});
});

test("shouldAutoInitChat returns false when managed SCM skills are configured", async () => {
await withTempDir(async (dir) => {
const commitSkillPath = join(dir, ".claude", "skills", "sl-commit", "SKILL.md");
await mkdir(join(commitSkillPath, ".."), { recursive: true });
await writeFile(commitSkillPath, "sapling commit", "utf-8");

await expect(shouldAutoInitChat("claude", dir)).resolves.toBe(false);
});
});
63 changes: 57 additions & 6 deletions src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import { pathExists } from "../utils/copy.ts";
import { AGENT_CONFIG } from "../config.ts";
import { initCommand } from "./init.ts";
import { join } from "path";
import { readdir } from "fs/promises";
import {
ensureAtomicGlobalAgentConfigs,
isManagedScmSkillName,
} from "../utils/atomic-global-config.ts";
import { detectInstallationType, getConfigRoot } from "../utils/config-path.ts";
import { prepareOpenCodeConfigDir } from "../utils/opencode-config.ts";

// SDK client imports
import {
Expand Down Expand Up @@ -77,7 +84,7 @@ function createClientForAgentType(agentType: AgentType): CodingAgentClient {
case "claude":
return createClaudeAgentClient();
case "opencode":
return createOpenCodeClient();
return createOpenCodeClient({ directory: process.cwd() });
case "copilot":
return createCopilotClient();
default:
Expand All @@ -104,6 +111,36 @@ function getTheme(themeName: "dark" | "light"): Theme {
return themeName === "light" ? lightTheme : darkTheme;
}

/**
* Determine whether the selected agent already has project-level SCM skills.
*/
export async function hasProjectScmSkills(
agentType: AgentType,
projectRoot: string
): Promise<boolean> {
const skillsDir = join(projectRoot, AGENT_CONFIG[agentType].folder, "skills");
if (!(await pathExists(skillsDir))) return false;

try {
const entries = await readdir(skillsDir, { withFileTypes: true });
return entries.some(
(entry) => entry.isDirectory() && isManagedScmSkillName(entry.name)
);
} catch {
return false;
}
}

/**
* Determine whether chat should auto-run init for the selected agent.
*/
export async function shouldAutoInitChat(
agentType: AgentType,
projectRoot: string = process.cwd()
): Promise<boolean> {
return !(await hasProjectScmSkills(agentType, projectRoot));
}

// ============================================================================
// Slash Command Handling
// ============================================================================
Expand Down Expand Up @@ -170,14 +207,28 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
const effectiveReasoningEffort = getReasoningEffortPreference(agentType);

const agentName = getAgentDisplayName(agentType);
const projectRoot = process.cwd();

if (detectInstallationType() !== "source") {
await ensureAtomicGlobalAgentConfigs(getConfigRoot());
}

if (agentType === "opencode") {
const mergedConfigDir = await prepareOpenCodeConfigDir({ projectRoot });
if (mergedConfigDir) {
process.env.OPENCODE_CONFIG_DIR = mergedConfigDir;
}
}

// Auto-init when project SCM skills are missing
if (await shouldAutoInitChat(agentType, projectRoot)) {
const configNotFoundMessage =
`Source control skills are not configured for ${agentName}. Starting interactive setup...`;

// Check if config folder exists locally
const configFolder = join(process.cwd(), AGENT_CONFIG[agentType].folder);
if (!(await pathExists(configFolder))) {
await initCommand({
showBanner: false,
preSelectedAgent: agentType,
configNotFoundMessage: `Local configuration not found for ${agentName}. Starting interactive setup...`,
configNotFoundMessage,
});
}

Expand Down Expand Up @@ -221,7 +272,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
version: VERSION,
model: displayModelName,
tier: modelDisplayInfo.tier,
workingDir: process.cwd(),
workingDir: projectRoot,
suggestion: 'Try "fix typecheck errors"',
agentType,
initialPrompt,
Expand Down
Loading
Loading