Skip to content

Commit d2a9ff0

Browse files
committed
2 parents 35f501d + 5ec5c28 commit d2a9ff0

19 files changed

Lines changed: 519 additions & 344 deletions

.github/pull_request_template.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Summary
2+
3+
<!-- What does this PR do and why? -->
4+
5+
## Changes
6+
7+
-
8+
9+
## Test plan
10+
11+
- [ ] Unit tests pass (`uv run pytest`)
12+
- [ ] Linter passes (`uv run ruff check .`)
13+
- [ ] Manually tested with `dualentry <command>`

.github/workflows/release.yml

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published, edited]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
build:
12+
strategy:
13+
matrix:
14+
include:
15+
- os: macos-14
16+
target: macos-arm64
17+
- os: macos-15
18+
target: macos-x86_64
19+
- os: ubuntu-latest
20+
target: linux-x86_64
21+
runs-on: ${{ matrix.os }}
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: astral-sh/setup-uv@v4
25+
with:
26+
python-version: "3.12"
27+
- name: Stamp version from tag
28+
run: |
29+
VERSION="${GITHUB_REF_NAME#v}"
30+
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
31+
sed -i.bak "s/^__version__ = .*/__version__ = \"$VERSION\"/" src/dualentry_cli/__init__.py
32+
rm -f pyproject.toml.bak src/dualentry_cli/__init__.py.bak
33+
- run: uv sync --dev
34+
- run: uv run python scripts/build.py
35+
- name: Rename binary
36+
run: mv dist/dualentry dist/dualentry-${{ matrix.target }}
37+
- uses: actions/upload-artifact@v4
38+
with:
39+
name: dualentry-${{ matrix.target }}
40+
path: dist/dualentry-${{ matrix.target }}
41+
42+
upload:
43+
needs: build
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/download-artifact@v4
47+
with:
48+
path: artifacts
49+
merge-multiple: true
50+
- name: Upload binaries to release
51+
env:
52+
GH_TOKEN: ${{ github.token }}
53+
run: |
54+
for f in artifacts/*; do
55+
gh release upload "${{ github.event.release.tag_name }}" "$f" --repo "${{ github.repository }}"
56+
done
57+
58+
update-tap:
59+
needs: upload
60+
runs-on: ubuntu-latest
61+
steps:
62+
- name: Update Homebrew tap
63+
if: ${{ env.TAP_TOKEN != '' }}
64+
env:
65+
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
66+
run: |
67+
VERSION="${GITHUB_REF_NAME#v}"
68+
BASE_URL="https://github.com/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}"
69+
70+
SHA_ARM64=$(curl -sL "$BASE_URL/dualentry-macos-arm64" | shasum -a 256 | cut -d' ' -f1)
71+
SHA_X86_64=$(curl -sL "$BASE_URL/dualentry-macos-x86_64" | shasum -a 256 | cut -d' ' -f1)
72+
SHA_LINUX=$(curl -sL "$BASE_URL/dualentry-linux-x86_64" | shasum -a 256 | cut -d' ' -f1)
73+
74+
cat > /tmp/dualentry.rb <<'FORMULA'
75+
class Dualentry < Formula
76+
desc "DualEntry accounting CLI"
77+
homepage "https://github.com/dualentry/dualentry-cli"
78+
version "VERSION_PLACEHOLDER"
79+
80+
on_macos do
81+
if Hardware::CPU.arm?
82+
url "URL_PLACEHOLDER/dualentry-macos-arm64"
83+
sha256 "SHA_ARM64_PLACEHOLDER"
84+
else
85+
url "URL_PLACEHOLDER/dualentry-macos-x86_64"
86+
sha256 "SHA_X86_64_PLACEHOLDER"
87+
end
88+
end
89+
90+
on_linux do
91+
url "URL_PLACEHOLDER/dualentry-linux-x86_64"
92+
sha256 "SHA_LINUX_PLACEHOLDER"
93+
end
94+
95+
def install
96+
binary = Dir["dualentry-*"].first || "dualentry"
97+
bin.install binary => "dualentry"
98+
end
99+
100+
test do
101+
assert_match "dualentry-cli", shell_output("#{bin}/dualentry --version")
102+
end
103+
end
104+
FORMULA
105+
106+
# Substitute placeholders and strip leading whitespace
107+
sed -i "s|VERSION_PLACEHOLDER|$VERSION|g" /tmp/dualentry.rb
108+
sed -i "s|URL_PLACEHOLDER|$BASE_URL|g" /tmp/dualentry.rb
109+
sed -i "s|SHA_ARM64_PLACEHOLDER|$SHA_ARM64|g" /tmp/dualentry.rb
110+
sed -i "s|SHA_X86_64_PLACEHOLDER|$SHA_X86_64|g" /tmp/dualentry.rb
111+
sed -i "s|SHA_LINUX_PLACEHOLDER|$SHA_LINUX|g" /tmp/dualentry.rb
112+
sed -i 's/^ //' /tmp/dualentry.rb
113+
114+
git clone "https://x-access-token:${TAP_TOKEN}@github.com/dualentry/homebrew-tap.git" /tmp/tap
115+
mkdir -p /tmp/tap/Formula
116+
cp /tmp/dualentry.rb /tmp/tap/Formula/dualentry.rb
117+
cd /tmp/tap
118+
git config user.name "github-actions[bot]"
119+
git config user.email "github-actions[bot]@users.noreply.github.com"
120+
git add Formula/dualentry.rb
121+
git commit -m "Update dualentry to $VERSION"
122+
git push

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dist/
66
build/
77
.eggs/
88
uv.lock
9+
*.spec

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
3+
## [0.1.0] - 2026-03-31
4+
5+
- OAuth browser login with API key storage in system keychain
6+
- List, get, create, and update for all transaction types
7+
- Table and JSON output formats
8+
- Pagination, search, and date/status filtering
9+
- Homebrew tap and install script distribution

README.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
**Automate your accounting workflows from the command line.**
88

9-
The DualEntry CLI brings the full power of the DualEntry API to your terminal. Create invoices, sync transactions, and manage your booksall without leaving your workflow.
9+
The DualEntry CLI brings the full power of the DualEntry API to your terminal. Create invoices, sync transactions, and manage your books: all without leaving your workflow.
1010

1111
## Why DualEntry CLI?
1212

@@ -19,10 +19,22 @@ The DualEntry CLI brings the full power of the DualEntry API to your terminal. C
1919

2020
### Install
2121

22+
```bash
23+
brew install dualentry/tap/dualentry
24+
```
25+
26+
Or with uv:
27+
2228
```bash
2329
uv tool install git+https://github.com/dualentry/dualentry-cli.git
2430
```
2531

32+
Or via the install script:
33+
34+
```bash
35+
curl -fsSL https://raw.githubusercontent.com/dualentry/dualentry-cli/main/install.sh | sh
36+
```
37+
2638
### Authenticate
2739

2840
```bash
@@ -88,14 +100,16 @@ dualentry invoices list --all
88100
## Configuration
89101

90102
```bash
91-
# View current settings
92103
dualentry config show
93-
94-
# Switch environments
95-
dualentry config set-env dev # Development
96-
dualentry config set-env prod # Production
97104
```
98105

106+
**Environment variables** (override config file):
107+
108+
| Variable | Description |
109+
|----------|-------------|
110+
| `DUALENTRY_API_URL` | API base URL (overrides config) |
111+
| `X_API_KEY` | API key (skips OAuth) |
112+
99113
## Requirements
100114

101115
- Python 3.11+
@@ -107,6 +121,34 @@ dualentry config set-env prod # Production
107121
uv tool upgrade dualentry-cli
108122
```
109123

124+
## Development
125+
126+
### Setup
127+
128+
```bash
129+
uv sync --dev
130+
uv run pre-commit install
131+
```
132+
133+
### Linting
134+
135+
```bash
136+
uv run ruff check .
137+
uv run ruff format --check .
138+
```
139+
140+
### Tests
141+
142+
```bash
143+
uv run pytest
144+
```
145+
146+
With coverage:
147+
148+
```bash
149+
uv run pytest --cov=dualentry_cli --cov-report=term-missing
150+
```
151+
110152
## Documentation
111153

112154
- [API Reference](https://docs.dualentry.com/api)

install.sh

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,57 @@
1-
#!/usr/bin/env bash
2-
set -euo pipefail
1+
#!/bin/sh
2+
set -e
33

4-
# DualEntry CLI installer
5-
# Usage: curl -sSL <raw-url>/install.sh | bash
4+
REPO="dualentry/dualentry-cli"
5+
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
66

7-
REPO="git+https://github.com/dualentry/dualentry-cli.git"
8-
TOOL_NAME="dualentry-cli"
7+
get_arch() {
8+
case "$(uname -m)" in
9+
x86_64|amd64) echo "x86_64" ;;
10+
arm64|aarch64) echo "arm64" ;;
11+
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
12+
esac
13+
}
914

10-
echo "Installing DualEntry CLI..."
15+
get_os() {
16+
case "$(uname -s)" in
17+
Darwin) echo "macos" ;;
18+
Linux) echo "linux" ;;
19+
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
20+
esac
21+
}
1122

12-
# Prefer uv, fall back to pipx
13-
if command -v uv &>/dev/null; then
14-
echo "Using uv..."
15-
uv tool install "$REPO"
16-
elif command -v pipx &>/dev/null; then
17-
echo "Using pipx..."
18-
pipx install "$REPO"
23+
OS=$(get_os)
24+
ARCH=$(get_arch)
25+
TARGET="${OS}-${ARCH}"
26+
27+
if [ "$OS" = "linux" ] && [ "$ARCH" = "arm64" ]; then
28+
echo "Linux arm64 is not currently supported." >&2
29+
exit 1
30+
fi
31+
32+
echo "Detecting latest release..."
33+
LATEST=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed 's/.*: "//;s/".*//')
34+
35+
if [ -z "$LATEST" ]; then
36+
echo "Failed to detect latest release." >&2
37+
exit 1
38+
fi
39+
40+
URL="https://github.com/${REPO}/releases/download/${LATEST}/dualentry-${TARGET}"
41+
TMPFILE=$(mktemp)
42+
43+
echo "Downloading dualentry ${LATEST} for ${TARGET}..."
44+
curl -fSL "$URL" -o "$TMPFILE"
45+
46+
chmod +x "$TMPFILE"
47+
mkdir -p "$INSTALL_DIR"
48+
49+
if [ -w "$INSTALL_DIR" ]; then
50+
mv "$TMPFILE" "$INSTALL_DIR/dualentry"
1951
else
20-
echo "Error: requires uv or pipx"
21-
echo ""
22-
echo "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
23-
echo "Install pipx: brew install pipx && pipx ensurepath"
24-
exit 1
52+
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
53+
sudo mv "$TMPFILE" "$INSTALL_DIR/dualentry"
2554
fi
2655

27-
echo ""
28-
echo "Installed! Run: dualentry auth login"
56+
echo "Installed dualentry to ${INSTALL_DIR}/dualentry"
57+
echo "Run 'dualentry --help' to get started."

pyproject.toml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,6 @@ packages = ["src/dualentry_cli"]
2323
[tool.pytest.ini_options]
2424
testpaths = ["tests"]
2525

26-
[project.optional-dependencies]
27-
dev = [
28-
"pytest>=8.0",
29-
"pytest-cov>=6.0",
30-
"pytest-mock>=3.14",
31-
"respx>=0.21",
32-
"ruff>=0.11.11",
33-
"pre-commit>=4.2",
34-
]
35-
3626
[dependency-groups]
3727
dev = [
3828
"pytest>=8.0",
@@ -41,6 +31,7 @@ dev = [
4131
"respx>=0.21",
4232
"ruff>=0.11.11",
4333
"pre-commit>=4.2",
34+
"pyinstaller>=6.0",
4435
]
4536

4637
# ── Ruff ───────────────────────────────────────────────────────────────
@@ -120,12 +111,14 @@ ignore = [
120111
"T201",
121112
"PLW0603",
122113
"PLW2901",
114+
"PLR0912",
123115
"PLR0915",
124116
"F841",
125117
"SIM105",
126118
]
127119

128120
[tool.ruff.lint.per-file-ignores]
121+
"scripts/*" = ["INP001", "S603"]
129122
"tests/*" = [
130123
"S101",
131124
"S105",

0 commit comments

Comments
 (0)