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
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Use recent npm
run: npm install -g npm@latest

- name: Install dependencies
run: npm ci

Expand All @@ -40,4 +43,4 @@ jobs:
run: npm run prepublishOnly

- name: Publish to npm
run: npm publish
run: npm publish --provenance
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ arbella --help
## What it does

```text
push ~/.claude · ~/.codex ──▶ strip · template ──▶ private repo
pull private repo ──▶ install · place ──▶ ~/.claude · ~/.codex
push ~/.claude · ~/.codex · Cursor ──▶ strip · template ──▶ private repo
pull private repo ──▶ install · place ──▶ tools + Cursor User data
```

`push` reads the parts of your setup that matter, strips anything secret, swaps machine-specific paths for placeholders, and pushes the result to your repo. `pull` does the reverse: installs the CLIs you don't have, drops files back with this machine's paths, reinstalls plugins and skills, and wires your shared instructions into both Claude and Codex.
`push` reads the parts of your setup that matter, strips anything secret, swaps machine-specific paths for placeholders, and pushes the result to your repo. `pull` does the reverse: installs the CLIs you don't have, drops files back with this machine's paths, reinstalls plugins and skills, and wires shared Claude/Codex instructions back into `CLAUDE.md` and `AGENTS.md`.

Two things it deliberately won't do:

- **Never commits secrets.** API keys, OAuth tokens, `auth.json`, `.credentials.json` — all excluded, no exceptions. You sign back in after a pull, or carry them yourself with [`arbella secrets`](#arbella-secrets).
- **Doesn't copy what it can reinstall.** Plugins and registry skills are saved as a list and pulled fresh, so the repo stays small and never goes stale.

Supported today: **Claude Code** and **Codex**, plus a **Cursor** adapter that quietly does nothing when Cursor isn't installed.
Supported today: **Claude Code**, **Codex**, and **Cursor**. Cursor support covers global MCP config, user settings, keybindings, snippets, local skills, skills.sh symlinks, and extension IDs; runtime state and credentials stay out.

## Commands

Expand Down
3 changes: 3 additions & 0 deletions src/adapters/adapter.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
TemplaterService,
TemplateVariables,
SourceOfTruth,
EnvVars,
} from "../types.js";

/* Re-export ToolManifest so consumers can import it from the interface module
Expand All @@ -47,6 +48,8 @@ export interface CoreServices {
vars: TemplateVariables;
/** Current OS (from platform/os.ts). */
os: OS;
/** Process environment, injected so path logic can respect platform env vars in tests. */
env: EnvVars;
}

/* -------------------------------------------------------------------------- */
Expand Down
5 changes: 3 additions & 2 deletions src/adapters/claude/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
denylistFor,
matchesDeny,
} from "../../core/sanitizer/denylist.js";
import { normalizeCapturedSymlinkTarget } from "../../utils/symlink.js";
import { listNpmGlobals } from "../../platform/install.js";

import { REPO_PREFIX, FROZEN_PATHS, INSTRUCTIONS_FILE, paths } from "./paths.js";
Expand Down Expand Up @@ -111,7 +112,7 @@ async function walk(
if (kind === "symlink") {
// A symlink anywhere in the frozen tree is preserved as a link. (Skill
// symlinks are special-cased by the caller before recursing into skills/.)
const target = await ctx.fs.readLink(abs);
const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
symlinks.push({ repoPath: repoPathFor(rel), target });
ctx.log.debug(`claude: symlink ${rel} -> ${target}`);
return;
Expand Down Expand Up @@ -225,7 +226,7 @@ async function captureSkills(
const kind = await ctx.fs.statKind(abs);
if (kind === "symlink") {
// skills.sh: ~/.claude/skills/<name> -> ../../.agents/skills/<name>
const target = await ctx.fs.readLink(abs);
const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
symlinks.push({ repoPath: repoPathFor(rel), target });
skills.push({
name,
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/codex/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
} from "../../types.js";
import { denylistFor, matchesDeny } from "../../core/sanitizer/denylist.js";
import { listNpmGlobals } from "../../platform/install.js";
import { normalizeCapturedSymlinkTarget } from "../../utils/symlink.js";
import {
CONFIG_FILE,
FROZEN_PATHS,
Expand Down Expand Up @@ -173,7 +174,7 @@ async function captureSymlink(
): Promise<void> {
let target: string;
try {
target = await ctx.fs.readLink(absPath);
target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(absPath));
} catch {
out.warnings.push(`codex: could not read symlink ${rel}; skipped`);
return;
Expand Down
Loading
Loading