Compare commits
43 Commits
claude/sec
...
react-quer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d45019cf | ||
|
|
59501edb56 | ||
|
|
d4d4cde3c7 | ||
|
|
acd1309752 | ||
|
|
95fd47d255 | ||
|
|
24bd6dff45 | ||
|
|
3f1ba9ce88 | ||
|
|
7ebf0c3693 | ||
|
|
18f7eee81a | ||
|
|
e980c0cef7 | ||
|
|
b891f203d3 | ||
|
|
a950ad9086 | ||
|
|
ac668664b4 | ||
|
|
038b46df9a | ||
|
|
dba2a2a1a8 | ||
|
|
ebd68a38ef | ||
|
|
b126693968 | ||
|
|
9c4df27a8a | ||
|
|
2aff508368 | ||
|
|
5b63efaa10 | ||
|
|
673b5d241f | ||
|
|
2223aa1c96 | ||
|
|
ecc95084f5 | ||
|
|
089ad26419 | ||
|
|
0cd55d5f36 | ||
|
|
dcd0486f9a | ||
|
|
ca7ea8a896 | ||
|
|
a8f2cd6e36 | ||
|
|
8b2e114e99 | ||
|
|
27030adf40 | ||
|
|
98a758c1b4 | ||
|
|
d9a18d03ee | ||
|
|
705b0f5d28 | ||
|
|
cea282f3bc | ||
|
|
c00db8ed95 | ||
|
|
b02386d654 | ||
|
|
27a43d6560 | ||
|
|
9e317b0a71 | ||
|
|
11a4eb65a0 | ||
|
|
e3c178b89a | ||
|
|
478aac731e | ||
|
|
953c9fcf09 | ||
|
|
fc1811c0db |
3
.github/actions/docs-spelling/expect.txt
vendored
@@ -2,7 +2,6 @@ Abanca
|
|||||||
ABNAMRO
|
ABNAMRO
|
||||||
ABNANL
|
ABNANL
|
||||||
Activo
|
Activo
|
||||||
actualrc
|
|
||||||
AESUDEF
|
AESUDEF
|
||||||
ALZEY
|
ALZEY
|
||||||
Anglais
|
Anglais
|
||||||
@@ -111,8 +110,8 @@ KBCBE
|
|||||||
Keycloak
|
Keycloak
|
||||||
Khurozov
|
Khurozov
|
||||||
KORT
|
KORT
|
||||||
Kreditbank
|
|
||||||
KRW
|
KRW
|
||||||
|
Kreditbank
|
||||||
lage
|
lage
|
||||||
LHV
|
LHV
|
||||||
LHVBEE
|
LHVBEE
|
||||||
|
|||||||
8
.github/actions/setup/action.yml
vendored
@@ -15,7 +15,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Install yarn
|
- name: Install yarn
|
||||||
@@ -27,7 +27,7 @@ runs:
|
|||||||
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Cache
|
- name: Cache
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||||
@@ -36,7 +36,7 @@ runs:
|
|||||||
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Cache Lage
|
- name: Cache Lage
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||||
@@ -48,7 +48,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
repository: actualbudget/translations
|
repository: actualbudget/translations
|
||||||
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/autofix.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
autofix:
|
autofix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
download-translations: 'false'
|
download-translations: 'false'
|
||||||
- name: Format code
|
- name: Format code
|
||||||
run: yarn lint:fix
|
run: yarn lint:fix
|
||||||
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
|
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||||
|
|||||||
45
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
api:
|
api:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -34,12 +34,12 @@ jobs:
|
|||||||
- name: Prepare bundle stats artifact
|
- name: Prepare bundle stats artifact
|
||||||
run: cp packages/api/app/stats.json api-stats.json
|
run: cp packages/api/app/stats.json api-stats.json
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-api
|
name: actual-api
|
||||||
path: packages/api/actual-api.tgz
|
path: packages/api/actual-api.tgz
|
||||||
- name: Upload API bundle stats
|
- name: Upload API bundle stats
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: api-build-stats
|
name: api-build-stats
|
||||||
path: api-stats.json
|
path: api-stats.json
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
crdt:
|
crdt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Create package tgz
|
- name: Create package tgz
|
||||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-crdt
|
name: actual-crdt
|
||||||
path: packages/crdt/actual-crdt.tgz
|
path: packages/crdt/actual-crdt.tgz
|
||||||
@@ -65,51 +65,26 @@ jobs:
|
|||||||
web:
|
web:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Build Web
|
- name: Build Web
|
||||||
run: yarn build:browser
|
run: yarn build:browser
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-web
|
name: actual-web
|
||||||
path: packages/desktop-client/build
|
path: packages/desktop-client/build
|
||||||
- name: Upload Build Stats
|
- name: Upload Build Stats
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build-stats
|
name: build-stats
|
||||||
path: packages/desktop-client/build-stats
|
path: packages/desktop-client/build-stats
|
||||||
|
|
||||||
cli:
|
server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Build CLI
|
|
||||||
run: yarn build:cli
|
|
||||||
- name: Create package tgz
|
|
||||||
run: cd packages/cli && yarn pack && mv package.tgz actual-cli.tgz
|
|
||||||
- name: Prepare bundle stats artifact
|
|
||||||
run: cp packages/cli/dist/stats.json cli-stats.json
|
|
||||||
- name: Upload Build
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: actual-cli
|
|
||||||
path: packages/cli/actual-cli.tgz
|
|
||||||
- name: Upload CLI bundle stats
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: cli-build-stats
|
|
||||||
path: cli-stats.json
|
|
||||||
|
|
||||||
server:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -117,7 +92,7 @@ jobs:
|
|||||||
- name: Build Server
|
- name: Build Server
|
||||||
run: yarn workspace @actual-app/sync-server build
|
run: yarn workspace @actual-app/sync-server build
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: sync-server
|
name: sync-server
|
||||||
path: packages/sync-server/build
|
path: packages/sync-server/build
|
||||||
|
|||||||
20
.github/workflows/check.yml
vendored
@@ -12,20 +12,10 @@ concurrency:
|
|||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
constraints:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- name: Set up environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Check dependency version consistency
|
|
||||||
run: yarn constraints
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +25,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -45,7 +35,7 @@ jobs:
|
|||||||
validate-cli:
|
validate-cli:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -57,7 +47,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -69,7 +59,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: '/language:javascript'
|
category: '/language:javascript'
|
||||||
|
|||||||
2
.github/workflows/count-points.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
|
|||||||
16
.github/workflows/docker-edge.yml
vendored
@@ -36,17 +36,17 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu, alpine]
|
os: [ubuntu, alpine]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
with:
|
with:
|
||||||
# Push to both Docker Hub and Github Container Registry
|
# Push to both Docker Hub and Github Container Registry
|
||||||
images: ${{ env.IMAGES }}
|
images: ${{ env.IMAGES }}
|
||||||
@@ -54,14 +54,14 @@ jobs:
|
|||||||
tags: ${{ env.TAGS }}
|
tags: ${{ env.TAGS }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
if: github.event_name != 'pull_request' && !github.event.repository.fork
|
if: github.event_name != 'pull_request' && !github.event.repository.fork
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
run: yarn build:server
|
run: yarn build:server
|
||||||
|
|
||||||
- name: Build image for testing
|
- name: Build image for testing
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
# This will use the cache from the earlier build step and not rebuild the image
|
# This will use the cache from the earlier build step and not rebuild the image
|
||||||
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
||||||
- name: Build and push images
|
- name: Build and push images
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|||||||
18
.github/workflows/docker-release.yml
vendored
@@ -28,17 +28,17 @@ jobs:
|
|||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
with:
|
with:
|
||||||
# Push to both Docker Hub and Github Container Registry
|
# Push to both Docker Hub and Github Container Registry
|
||||||
images: ${{ env.IMAGES }}
|
images: ${{ env.IMAGES }}
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta for Alpine image
|
- name: Docker meta for Alpine image
|
||||||
id: alpine-meta
|
id: alpine-meta
|
||||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.IMAGES }}
|
images: ${{ env.IMAGES }}
|
||||||
# Automatically update :latest
|
# Automatically update :latest
|
||||||
@@ -58,13 +58,13 @@ jobs:
|
|||||||
tags: ${{ env.TAGS }}
|
tags: ${{ env.TAGS }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
run: yarn build:server
|
run: yarn build:server
|
||||||
|
|
||||||
- name: Build and push ubuntu image
|
- name: Build and push ubuntu image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|
||||||
- name: Build and push alpine image
|
- name: Build and push alpine image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
20
.github/workflows/e2e-test.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
- name: Run E2E Tests
|
- name: Run E2E Tests
|
||||||
run: yarn e2e --shard=${{ matrix.shard }}/5
|
run: yarn e2e --shard=${{ matrix.shard }}/5
|
||||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: desktop-client-test-results-shard-${{ matrix.shard }}
|
name: desktop-client-test-results-shard-${{ matrix.shard }}
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
- name: Run Desktop app E2E Tests
|
- name: Run Desktop app E2E Tests
|
||||||
run: |
|
run: |
|
||||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: desktop-app-test-results
|
name: desktop-app-test-results
|
||||||
@@ -83,14 +83,14 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
download-translations: 'false'
|
download-translations: 'false'
|
||||||
- name: Run VRT Tests
|
- name: Run VRT Tests
|
||||||
run: yarn vrt --shard=${{ matrix.shard }}/5
|
run: yarn vrt --shard=${{ matrix.shard }}/5
|
||||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: vrt-blob-report-${{ matrix.shard }}
|
name: vrt-blob-report-${{ matrix.shard }}
|
||||||
@@ -106,11 +106,11 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Download all blob reports
|
- name: Download all blob reports
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
path: packages/desktop-client/all-blob-reports
|
path: packages/desktop-client/all-blob-reports
|
||||||
pattern: vrt-blob-report-*
|
pattern: vrt-blob-report-*
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
- name: Merge reports
|
- name: Merge reports
|
||||||
id: merge-reports
|
id: merge-reports
|
||||||
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
|
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
|
||||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
id: playwright-report-vrt
|
id: playwright-report-vrt
|
||||||
with:
|
with:
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
name: html-report--attempt-${{ github.run_attempt }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
|
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
|
||||||
- name: Upload VRT metadata
|
- name: Upload VRT metadata
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: vrt-comment-metadata
|
name: vrt-comment-metadata
|
||||||
path: vrt-metadata/
|
path: vrt-metadata/
|
||||||
|
|||||||
4
.github/workflows/e2e-vrt-comment.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
if: github.event.workflow_run.event == 'pull_request'
|
if: github.event.workflow_run.event == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: Download VRT metadata
|
- name: Download VRT metadata
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Comment on PR with VRT report link
|
- name: Comment on PR with VRT report link
|
||||||
if: steps.metadata.outputs.should_comment == 'true'
|
if: steps.metadata.outputs.should_comment == 'true'
|
||||||
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
||||||
with:
|
with:
|
||||||
number: ${{ steps.metadata.outputs.pr_number }}
|
number: ${{ steps.metadata.outputs.pr_number }}
|
||||||
header: vrt-comment
|
header: vrt-comment
|
||||||
|
|||||||
10
.github/workflows/electron-master.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- macos-latest
|
- macos-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
run: pip.exe install setuptools
|
run: pip.exe install setuptools
|
||||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||||
run: ./bin/package-electron
|
run: ./bin/package-electron
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-electron-${{ matrix.os }}
|
name: actual-electron-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
@@ -85,13 +85,13 @@ jobs:
|
|||||||
packages/desktop-electron/dist/*.flatpak
|
packages/desktop-electron/dist/*.flatpak
|
||||||
- name: Upload Windows Store Build
|
- name: Upload Windows Store Build
|
||||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-electron-${{ matrix.os }}-appx
|
name: actual-electron-${{ matrix.os }}-appx
|
||||||
path: |
|
path: |
|
||||||
packages/desktop-electron/dist/*.appx
|
packages/desktop-electron/dist/*.appx
|
||||||
- name: Add to new release
|
- name: Add to new release
|
||||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
body: |
|
body: |
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||||
|
|
||||||
- name: Download Microsoft Store artifacts
|
- name: Download Microsoft Store artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-electron-windows-latest-appx
|
name: actual-electron-windows-latest-appx
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/electron-pr.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- macos-latest
|
- macos-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
run: pip.exe install setuptools
|
run: pip.exe install setuptools
|
||||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||||
@@ -42,8 +42,6 @@ jobs:
|
|||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
python3 -m pip install setuptools
|
python3 -m pip install setuptools
|
||||||
- name: Set up environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||||
name: Setup Flatpak dependencies
|
name: Setup Flatpak dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -58,63 +56,65 @@ jobs:
|
|||||||
|
|
||||||
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
|
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
|
||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
|
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
|
||||||
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
|
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
|
||||||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
||||||
|
- name: Set up environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
- name: Build Electron
|
- name: Build Electron
|
||||||
run: ./bin/package-electron
|
run: ./bin/package-electron
|
||||||
|
|
||||||
- name: Upload Linux x64 AppImage
|
- name: Upload Linux x64 AppImage
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-linux-x86_64.AppImage
|
name: Actual-linux-x86_64.AppImage
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
||||||
|
|
||||||
- name: Upload Linux arm64 AppImage
|
- name: Upload Linux arm64 AppImage
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-linux-arm64.AppImage
|
name: Actual-linux-arm64.AppImage
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
||||||
|
|
||||||
- name: Upload Linux x64 flatpak
|
- name: Upload Linux x64 flatpak
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-linux-x86_64.flatpak
|
name: Actual-linux-x86_64.flatpak
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
|
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
|
||||||
|
|
||||||
- name: Upload Windows x32 exe
|
- name: Upload Windows x32 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-ia32.exe
|
name: Actual-windows-ia32.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
||||||
|
|
||||||
- name: Upload Windows x64 exe
|
- name: Upload Windows x64 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-x64.exe
|
name: Actual-windows-x64.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
||||||
|
|
||||||
- name: Upload Windows arm64 exe
|
- name: Upload Windows arm64 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-arm64.exe
|
name: Actual-windows-arm64.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
||||||
|
|
||||||
- name: Upload Mac x64 dmg
|
- name: Upload Mac x64 dmg
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-mac-x64.dmg
|
name: Actual-mac-x64.dmg
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
||||||
|
|
||||||
- name: Upload Mac arm64 dmg
|
- name: Upload Mac arm64 dmg
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-mac-arm64.dmg
|
name: Actual-mac-arm64.dmg
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Windows Store Build
|
- name: Upload Windows Store Build
|
||||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-electron-${{ matrix.os }}-appx
|
name: actual-electron-${{ matrix.os }}-appx
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
2
.github/workflows/fork-pr-welcome.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||||
steps:
|
steps:
|
||||||
- name: Post welcome comment
|
- name: Post welcome comment
|
||||||
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
number: ${{ github.event.pull_request.number }}
|
number: ${{ github.event.pull_request.number }}
|
||||||
|
|||||||
12
.github/workflows/generate-release-pr.yml
vendored
@@ -17,13 +17,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.ref }}
|
ref: ${{ github.event.inputs.ref }}
|
||||||
- name: Set up environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Bump package versions
|
- name: Bump package versions
|
||||||
id: bump_package_versions
|
id: bump_package_versions
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -39,12 +35,12 @@ jobs:
|
|||||||
pkg="${packages[$key]}"
|
pkg="${packages[$key]}"
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||||
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
|
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||||
--package-json "./packages/$pkg/package.json" \
|
--package-json "./packages/$pkg/package.json" \
|
||||||
--version "${{ github.event.inputs.version }}" \
|
--version "${{ github.event.inputs.version }}" \
|
||||||
--update)
|
--update)
|
||||||
else
|
else
|
||||||
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
|
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||||
--package-json "./packages/$pkg/package.json" \
|
--package-json "./packages/$pkg/package.json" \
|
||||||
--type auto \
|
--type auto \
|
||||||
--update)
|
--update)
|
||||||
@@ -55,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
- name: Create PR
|
- name: Create PR
|
||||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
|
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
|
||||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
if: github.repository == 'actualbudget/actual'
|
if: github.repository == 'actualbudget/actual'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out main repository
|
- name: Check out main repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
path: actual
|
path: actual
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
push \
|
push \
|
||||||
actualbudget/actual
|
actualbudget/actual
|
||||||
- name: Check out updated translations
|
- name: Check out updated translations
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
||||||
repository: actualbudget/translations
|
repository: actualbudget/translations
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# This is not a security concern because we have approved & merged the PR
|
# This is not a security concern because we have approved & merged the PR
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Handle feature requests
|
- name: Handle feature requests
|
||||||
|
|||||||
2
.github/workflows/netlify-release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Repository Checkout
|
- name: Repository Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
|
|||||||
4
.github/workflows/publish-flathub.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
|
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Checkout Flathub repo
|
- name: Checkout Flathub repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: flathub/com.actualbudget.actual
|
repository: flathub/com.actualbudget.actual
|
||||||
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
cat com.actualbudget.actual.yml
|
cat com.actualbudget.actual.yml
|
||||||
|
|
||||||
- name: Create PR in Flathub repo
|
- name: Create PR in Flathub repo
|
||||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
||||||
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
|
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
|
||||||
|
|||||||
27
.github/workflows/publish-nightly-electron.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
if: github.event.repository.fork == false
|
if: github.event.repository.fork == false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
run: pip.exe install setuptools
|
run: pip.exe install setuptools
|
||||||
|
|
||||||
@@ -39,9 +39,6 @@ jobs:
|
|||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
python3 -m pip install setuptools
|
python3 -m pip install setuptools
|
||||||
|
|
||||||
- name: Set up environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||||
name: Setup Flatpak dependencies
|
name: Setup Flatpak dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -56,14 +53,16 @@ jobs:
|
|||||||
|
|
||||||
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
|
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
|
||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
|
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
|
||||||
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
|
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
|
||||||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
||||||
|
- name: Set up environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
|
||||||
- name: Update package versions
|
- name: Update package versions
|
||||||
run: |
|
run: |
|
||||||
# Get new nightly version
|
# Get new nightly version
|
||||||
NEW_DESKTOP_APP_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
|
NEW_DESKTOP_APP_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
|
||||||
|
|
||||||
# Set package version
|
# Set package version
|
||||||
npm version $NEW_DESKTOP_APP_VERSION --no-git-tag-version --workspace=desktop-electron --no-workspaces-update
|
npm version $NEW_DESKTOP_APP_VERSION --no-git-tag-version --workspace=desktop-electron --no-workspaces-update
|
||||||
@@ -83,49 +82,49 @@ jobs:
|
|||||||
run: ./bin/package-electron
|
run: ./bin/package-electron
|
||||||
|
|
||||||
- name: Upload Linux x64 AppImage
|
- name: Upload Linux x64 AppImage
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-linux-x86_64.AppImage
|
name: Actual-linux-x86_64.AppImage
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
||||||
|
|
||||||
- name: Upload Linux arm64 AppImage
|
- name: Upload Linux arm64 AppImage
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-linux-arm64.AppImage
|
name: Actual-linux-arm64.AppImage
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
||||||
|
|
||||||
- name: Upload Windows x32 exe
|
- name: Upload Windows x32 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-ia32.exe
|
name: Actual-windows-ia32.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
||||||
|
|
||||||
- name: Upload Windows x64 exe
|
- name: Upload Windows x64 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-x64.exe
|
name: Actual-windows-x64.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
||||||
|
|
||||||
- name: Upload Windows arm64 exe
|
- name: Upload Windows arm64 exe
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-windows-arm64.exe
|
name: Actual-windows-arm64.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
||||||
|
|
||||||
- name: Upload Mac x64 dmg
|
- name: Upload Mac x64 dmg
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-mac-x64.dmg
|
name: Actual-mac-x64.dmg
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
||||||
|
|
||||||
- name: Upload Mac arm64 dmg
|
- name: Upload Mac arm64 dmg
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Actual-mac-arm64.dmg
|
name: Actual-mac-arm64.dmg
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
@@ -133,7 +132,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Windows Store Build
|
- name: Upload Windows Store Build
|
||||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: actual-electron-${{ matrix.os }}-appx
|
name: actual-electron-${{ matrix.os }}-appx
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
name: Build and pack npm packages
|
name: Build and pack npm packages
|
||||||
if: github.event.repository.fork == false
|
if: github.event.repository.fork == false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
@@ -20,18 +20,16 @@ jobs:
|
|||||||
- name: Update package versions
|
- name: Update package versions
|
||||||
run: |
|
run: |
|
||||||
# Get new nightly versions
|
# Get new nightly versions
|
||||||
NEW_CORE_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/loot-core/package.json --type nightly)
|
NEW_CORE_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/loot-core/package.json --type nightly)
|
||||||
NEW_WEB_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-client/package.json --type nightly)
|
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
|
||||||
NEW_SYNC_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/sync-server/package.json --type nightly)
|
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
|
||||||
NEW_API_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/api/package.json --type nightly)
|
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
|
||||||
NEW_CLI_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/cli/package.json --type nightly)
|
|
||||||
|
|
||||||
# Set package versions
|
# Set package versions
|
||||||
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
|
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
|
||||||
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
|
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
|
||||||
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
|
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
|
||||||
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
|
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
|
||||||
npm version $NEW_CLI_VERSION --no-git-tag-version --workspace=@actual-app/cli --no-workspaces-update
|
|
||||||
|
|
||||||
- name: Yarn install
|
- name: Yarn install
|
||||||
run: |
|
run: |
|
||||||
@@ -56,15 +54,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||||
|
|
||||||
- name: Build CLI
|
|
||||||
run: yarn workspace @actual-app/cli build
|
|
||||||
|
|
||||||
- name: Pack the cli package
|
|
||||||
run: |
|
|
||||||
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
|
|
||||||
|
|
||||||
- name: Upload package artifacts
|
- name: Upload package artifacts
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: npm-packages
|
name: npm-packages
|
||||||
path: |
|
path: |
|
||||||
@@ -72,7 +63,6 @@ jobs:
|
|||||||
packages/desktop-client/@actual-app/web.tgz
|
packages/desktop-client/@actual-app/web.tgz
|
||||||
packages/sync-server/@actual-app/sync-server.tgz
|
packages/sync-server/@actual-app/sync-server.tgz
|
||||||
packages/api/@actual-app/api.tgz
|
packages/api/@actual-app/api.tgz
|
||||||
packages/cli/@actual-app/cli.tgz
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -83,12 +73,12 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download the artifacts
|
- name: Download the artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: npm-packages
|
name: npm-packages
|
||||||
|
|
||||||
- name: Setup node and npm registry
|
- name: Setup node and npm registry
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
@@ -116,9 +106,3 @@ jobs:
|
|||||||
npm publish api/@actual-app/api.tgz --access public --tag nightly
|
npm publish api/@actual-app/api.tgz --access public --tag nightly
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Publish CLI
|
|
||||||
run: |
|
|
||||||
npm publish cli/@actual-app/cli.tgz --access public --tag nightly
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
22
.github/workflows/publish-npm-packages.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build and pack npm packages
|
name: Build and pack npm packages
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
@@ -35,15 +35,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||||
|
|
||||||
- name: Build CLI
|
|
||||||
run: yarn workspace @actual-app/cli build
|
|
||||||
|
|
||||||
- name: Pack the cli package
|
|
||||||
run: |
|
|
||||||
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
|
|
||||||
|
|
||||||
- name: Upload package artifacts
|
- name: Upload package artifacts
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: npm-packages
|
name: npm-packages
|
||||||
path: |
|
path: |
|
||||||
@@ -51,7 +44,6 @@ jobs:
|
|||||||
packages/desktop-client/@actual-app/web.tgz
|
packages/desktop-client/@actual-app/web.tgz
|
||||||
packages/sync-server/@actual-app/sync-server.tgz
|
packages/sync-server/@actual-app/sync-server.tgz
|
||||||
packages/api/@actual-app/api.tgz
|
packages/api/@actual-app/api.tgz
|
||||||
packages/cli/@actual-app/cli.tgz
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -62,12 +54,12 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download the artifacts
|
- name: Download the artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: npm-packages
|
name: npm-packages
|
||||||
|
|
||||||
- name: Setup node and npm registry
|
- name: Setup node and npm registry
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
@@ -95,9 +87,3 @@ jobs:
|
|||||||
npm publish api/@actual-app/api.tgz --access public
|
npm publish api/@actual-app/api.tgz --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Publish CLI
|
|
||||||
run: |
|
|
||||||
npm publish cli/@actual-app/cli.tgz --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
2
.github/workflows/release-notes.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
|
|||||||
45
.github/workflows/size-compare.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
@@ -57,13 +57,6 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
checkName: api
|
checkName: api
|
||||||
ref: ${{github.base_ref}}
|
ref: ${{github.base_ref}}
|
||||||
- name: Wait for ${{github.base_ref}} CLI build to succeed
|
|
||||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
|
||||||
id: master-cli-build
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
checkName: cli
|
|
||||||
ref: ${{github.base_ref}}
|
|
||||||
|
|
||||||
- name: Wait for PR build to succeed
|
- name: Wait for PR build to succeed
|
||||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||||
@@ -79,22 +72,15 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
checkName: api
|
checkName: api
|
||||||
ref: ${{github.event.pull_request.head.sha}}
|
ref: ${{github.event.pull_request.head.sha}}
|
||||||
- name: Wait for CLI PR build to succeed
|
|
||||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
|
||||||
id: wait-for-cli-build
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
checkName: cli
|
|
||||||
ref: ${{github.event.pull_request.head.sha}}
|
|
||||||
|
|
||||||
- name: Report build failure
|
- name: Report build failure
|
||||||
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure'
|
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure'
|
||||||
run: |
|
run: |
|
||||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Download web build artifact from ${{github.base_ref}}
|
- name: Download web build artifact from ${{github.base_ref}}
|
||||||
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
id: pr-web-build
|
id: pr-web-build
|
||||||
with:
|
with:
|
||||||
branch: ${{github.base_ref}}
|
branch: ${{github.base_ref}}
|
||||||
@@ -103,7 +89,7 @@ jobs:
|
|||||||
name: build-stats
|
name: build-stats
|
||||||
path: base
|
path: base
|
||||||
- name: Download API build artifact from ${{github.base_ref}}
|
- name: Download API build artifact from ${{github.base_ref}}
|
||||||
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
id: pr-api-build
|
id: pr-api-build
|
||||||
with:
|
with:
|
||||||
branch: ${{github.base_ref}}
|
branch: ${{github.base_ref}}
|
||||||
@@ -112,7 +98,7 @@ jobs:
|
|||||||
name: api-build-stats
|
name: api-build-stats
|
||||||
path: base
|
path: base
|
||||||
- name: Download build stats from PR
|
- name: Download build stats from PR
|
||||||
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
with:
|
with:
|
||||||
pr: ${{github.event.pull_request.number}}
|
pr: ${{github.event.pull_request.number}}
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
@@ -121,7 +107,7 @@ jobs:
|
|||||||
path: head
|
path: head
|
||||||
allow_forks: true
|
allow_forks: true
|
||||||
- name: Download API stats from PR
|
- name: Download API stats from PR
|
||||||
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
with:
|
with:
|
||||||
pr: ${{github.event.pull_request.number}}
|
pr: ${{github.event.pull_request.number}}
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
@@ -129,23 +115,6 @@ jobs:
|
|||||||
name: api-build-stats
|
name: api-build-stats
|
||||||
path: head
|
path: head
|
||||||
allow_forks: true
|
allow_forks: true
|
||||||
- name: Download CLI build artifact from ${{github.base_ref}}
|
|
||||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
|
||||||
with:
|
|
||||||
branch: ${{github.base_ref}}
|
|
||||||
workflow: build.yml
|
|
||||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
|
||||||
name: cli-build-stats
|
|
||||||
path: base
|
|
||||||
- name: Download CLI stats from PR
|
|
||||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
|
||||||
with:
|
|
||||||
pr: ${{github.event.pull_request.number}}
|
|
||||||
workflow: build.yml
|
|
||||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
|
||||||
name: cli-build-stats
|
|
||||||
path: head
|
|
||||||
allow_forks: true
|
|
||||||
- name: Strip content hashes from stats files
|
- name: Strip content hashes from stats files
|
||||||
run: |
|
run: |
|
||||||
if [ -f ./head/web-stats.json ]; then
|
if [ -f ./head/web-stats.json ]; then
|
||||||
@@ -167,11 +136,9 @@ jobs:
|
|||||||
--base desktop-client=./base/web-stats.json \
|
--base desktop-client=./base/web-stats.json \
|
||||||
--base loot-core=./base/loot-core-stats.json \
|
--base loot-core=./base/loot-core-stats.json \
|
||||||
--base api=./base/api-stats.json \
|
--base api=./base/api-stats.json \
|
||||||
--base cli=./base/cli-stats.json \
|
|
||||||
--head desktop-client=./head/web-stats.json \
|
--head desktop-client=./head/web-stats.json \
|
||||||
--head loot-core=./head/loot-core-stats.json \
|
--head loot-core=./head/loot-core-stats.json \
|
||||||
--head api=./head/api-stats.json \
|
--head api=./head/api-stats.json \
|
||||||
--head cli=./head/cli-stats.json \
|
|
||||||
--identifier combined \
|
--identifier combined \
|
||||||
--format pr-body > bundle-stats-comment.md
|
--format pr-body > bundle-stats-comment.md
|
||||||
- name: Post combined bundle stats comment
|
- name: Post combined bundle stats comment
|
||||||
|
|||||||
6
.github/workflows/stale.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
stale-wip:
|
stale-wip:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
||||||
days-before-stale: 7
|
days-before-stale: 7
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
stale-needs-info:
|
stale-needs-info:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
stale-issue-label: 'needs info'
|
stale-issue-label: 'needs info'
|
||||||
days-before-stale: -1
|
days-before-stale: -1
|
||||||
|
|||||||
6
.github/workflows/vrt-update-apply.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download patch artifact
|
- name: Download patch artifact
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
path: /tmp/artifacts
|
path: /tmp/artifacts
|
||||||
|
|
||||||
- name: Download metadata artifact
|
- name: Download metadata artifact
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout fork branch
|
- name: Checkout fork branch
|
||||||
if: steps.metadata.outputs.pr_number != ''
|
if: steps.metadata.outputs.pr_number != ''
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.metadata.outputs.head_repo }}
|
repository: ${{ steps.metadata.outputs.head_repo }}
|
||||||
ref: ${{ steps.metadata.outputs.head_ref }}
|
ref: ${{ steps.metadata.outputs.head_ref }}
|
||||||
|
|||||||
6
.github/workflows/vrt-update-generate.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
|||||||
core.setOutput('head_ref', pr.head.ref);
|
core.setOutput('head_ref', pr.head.ref);
|
||||||
core.setOutput('head_repo', pr.head.repo.full_name);
|
core.setOutput('head_repo', pr.head.repo.full_name);
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.pr.outputs.head_sha }}
|
ref: ${{ steps.pr.outputs.head_sha }}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload patch artifact
|
- name: Upload patch artifact
|
||||||
if: steps.create-patch.outputs.has_changes == 'true'
|
if: steps.create-patch.outputs.has_changes == 'true'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: vrt-patch-${{ github.event.issue.number }}
|
name: vrt-patch-${{ github.event.issue.number }}
|
||||||
path: vrt-update.patch
|
path: vrt-update.patch
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload PR metadata
|
- name: Upload PR metadata
|
||||||
if: steps.create-patch.outputs.has_changes == 'true'
|
if: steps.create-patch.outputs.has_changes == 'true'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: vrt-metadata-${{ github.event.issue.number }}
|
name: vrt-metadata-${{ github.event.issue.number }}
|
||||||
path: pr-metadata/
|
path: pr-metadata/
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -81,10 +81,3 @@ build/
|
|||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
storybook-static
|
||||||
|
|
||||||
# cli config when testing locally
|
|
||||||
.actualrc.json
|
|
||||||
.actualrc
|
|
||||||
.actualrc.yaml
|
|
||||||
.actualrc.yml
|
|
||||||
actual.config.js
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Run yarn install when switching branches (if yarn.lock changed)
|
|
||||||
|
|
||||||
# $3 is 1 for branch checkout, 0 for file checkout
|
|
||||||
if [ "$3" != "1" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if yarn.lock changed between the old and new HEAD
|
|
||||||
if git diff --name-only "$1" "$2" | grep -q "^yarn.lock$"; then
|
|
||||||
echo "yarn.lock changed — running yarn install..."
|
|
||||||
yarn install
|
|
||||||
fi
|
|
||||||
@@ -17,7 +17,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
type: 'npmScript',
|
type: 'npmScript',
|
||||||
dependsOn: ['^build'],
|
|
||||||
cache: true,
|
cache: true,
|
||||||
options: {
|
options: {
|
||||||
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
||||||
|
|||||||
21
package.json
@@ -34,14 +34,12 @@
|
|||||||
"start:browser-backend": "yarn workspace @actual-app/core watch:browser",
|
"start:browser-backend": "yarn workspace @actual-app/core watch:browser",
|
||||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||||
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
||||||
"build": "lage build",
|
|
||||||
"build:browser-backend": "yarn workspace @actual-app/core build:browser",
|
"build:browser-backend": "yarn workspace @actual-app/core build:browser",
|
||||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||||
"build:browser": "./bin/package-browser",
|
"build:browser": "./bin/package-browser",
|
||||||
"build:desktop": "./bin/package-electron",
|
"build:desktop": "./bin/package-electron",
|
||||||
"build:plugins-service": "yarn workspace plugins-service build",
|
"build:plugins-service": "yarn workspace plugins-service build",
|
||||||
"build:api": "yarn workspace @actual-app/api build",
|
"build:api": "yarn workspace @actual-app/api build",
|
||||||
"build:cli": "yarn build --scope=@actual-app/cli",
|
|
||||||
"build:docs": "yarn workspace docs build",
|
"build:docs": "yarn workspace docs build",
|
||||||
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
||||||
"deploy:docs": "yarn workspace docs deploy",
|
"deploy:docs": "yarn workspace docs deploy",
|
||||||
@@ -59,31 +57,29 @@
|
|||||||
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
||||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
||||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||||
"constraints": "yarn constraints",
|
|
||||||
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
|
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
|
||||||
"jq": "./node_modules/node-jq/bin/jq",
|
"jq": "./node_modules/node-jq/bin/jq",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@types/node": "^22.19.15",
|
"@types/node": "^22.19.10",
|
||||||
"@types/prompts": "^2.4.9",
|
"@types/prompts": "^2.4.9",
|
||||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||||
"@yarnpkg/types": "^4.0.1",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"baseline-browser-mapping": "^2.10.0",
|
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-perfectionist": "^5.6.0",
|
"eslint-plugin-perfectionist": "^4.15.1",
|
||||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lage": "^2.14.19",
|
"lage": "^2.14.17",
|
||||||
"lint-staged": "^16.3.2",
|
"lint-staged": "^16.2.7",
|
||||||
"minimatch": "^10.2.4",
|
"minimatch": "^10.1.2",
|
||||||
"node-jq": "^6.3.1",
|
"node-jq": "^6.3.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"oxfmt": "^0.32.0",
|
"oxfmt": "^0.32.0",
|
||||||
"oxlint": "^1.51.0",
|
"oxlint": "^1.47.0",
|
||||||
"oxlint-tsgolint": "^0.13.0",
|
"oxlint-tsgolint": "^0.13.0",
|
||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
@@ -92,7 +88,6 @@
|
|||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
|
|
||||||
"rollup": "4.40.1",
|
"rollup": "4.40.1",
|
||||||
"socks": ">=2.8.3"
|
"socks": ">=2.8.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
class Query {
|
class Query {
|
||||||
/** @type {import('loot-core/shared/query').QueryState} */
|
|
||||||
state;
|
|
||||||
|
|
||||||
constructor(state) {
|
constructor(state) {
|
||||||
this.state = {
|
this.state = {
|
||||||
filterExpressions: state.filterExpressions || [],
|
filterExpressions: state.filterExpressions || [],
|
||||||
|
|||||||
@@ -896,73 +896,6 @@ describe('API CRUD operations', () => {
|
|||||||
);
|
);
|
||||||
expect(transactions[0].notes).toBeNull();
|
expect(transactions[0].notes).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Transactions: reimportDeleted=false prevents reimporting deleted transactions', async () => {
|
|
||||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
|
||||||
|
|
||||||
// Import a transaction
|
|
||||||
const result1 = await api.importTransactions(accountId, [
|
|
||||||
{
|
|
||||||
date: '2023-11-03',
|
|
||||||
imported_id: 'reimport-test-1',
|
|
||||||
amount: 100,
|
|
||||||
account: accountId,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(result1.added).toHaveLength(1);
|
|
||||||
|
|
||||||
// Delete the transaction
|
|
||||||
await api.deleteTransaction(result1.added[0]);
|
|
||||||
|
|
||||||
// Reimport the same transaction with reimportDeleted=false
|
|
||||||
const result2 = await api.importTransactions(
|
|
||||||
accountId,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
date: '2023-11-03',
|
|
||||||
imported_id: 'reimport-test-1',
|
|
||||||
amount: 100,
|
|
||||||
account: accountId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ reimportDeleted: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should match the deleted transaction and not create a new one
|
|
||||||
expect(result2.added).toHaveLength(0);
|
|
||||||
expect(result2.updated).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Transactions: reimportDeleted=true reimports deleted transactions', async () => {
|
|
||||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
|
||||||
|
|
||||||
// Import a transaction
|
|
||||||
const result1 = await api.importTransactions(accountId, [
|
|
||||||
{
|
|
||||||
date: '2023-11-03',
|
|
||||||
imported_id: 'reimport-test-2',
|
|
||||||
amount: 200,
|
|
||||||
account: accountId,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(result1.added).toHaveLength(1);
|
|
||||||
|
|
||||||
// Delete the transaction
|
|
||||||
await api.deleteTransaction(result1.added[0]);
|
|
||||||
|
|
||||||
// Reimport the same transaction relying on reimportDeleted=true default
|
|
||||||
const result2 = await api.importTransactions(accountId, [
|
|
||||||
{
|
|
||||||
date: '2023-11-03',
|
|
||||||
imported_id: 'reimport-test-2',
|
|
||||||
amount: 200,
|
|
||||||
account: accountId,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Should create a new transaction since deleted ones are ignored
|
|
||||||
expect(result2.added).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
|
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
|
||||||
|
|||||||
@@ -9,20 +9,6 @@
|
|||||||
],
|
],
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "@types/index.d.ts",
|
"types": "@types/index.d.ts",
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"development": "./index.ts",
|
|
||||||
"default": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./@types/index.d.ts",
|
|
||||||
"default": "./dist/index.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
@@ -38,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||||
"rollup-plugin-visualizer": "^6.0.11",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"typescript-strict-plugin": "^2.4.4",
|
"typescript-strict-plugin": "^2.4.4",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
|||||||
44
packages/ci-actions/bin/get-next-package-version.ts → packages/ci-actions/bin/get-next-package-version.js
Normal file → Executable file
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
// This script is used in GitHub Actions to get the next version based on the current package.json version.
|
// This script is used in GitHub Actions to get the next version based on the current package.json version.
|
||||||
// It supports three types of versioning: nightly, hotfix, and monthly.
|
// It supports three types of versioning: nightly, hotfix, and monthly.
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { parseArgs } from 'node:util';
|
import { parseArgs } from 'node:util';
|
||||||
|
|
||||||
import {
|
import { getNextVersion } from '../src/versions/get-next-package-version.js';
|
||||||
getNextVersion,
|
|
||||||
isValidVersionType,
|
const args = process.argv;
|
||||||
} from '../src/versions/get-next-package-version';
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
'package-json': {
|
'package-json': {
|
||||||
@@ -28,53 +28,40 @@ const options = {
|
|||||||
short: 'u',
|
short: 'u',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
function fail(message: string): never {
|
|
||||||
console.error(message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
|
args,
|
||||||
options,
|
options,
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const packageJsonPath = values['package-json'];
|
if (!values['package-json']) {
|
||||||
if (!packageJsonPath) {
|
console.error(
|
||||||
fail(
|
|
||||||
'Please specify the path to package.json using --package-json or -p option.',
|
'Please specify the path to package.json using --package-json or -p option.',
|
||||||
);
|
);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const packageJsonPath = values['package-json'];
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
if (!('version' in packageJson) || typeof packageJson.version !== 'string') {
|
|
||||||
fail('The specified package.json does not contain a valid version field.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
const explicitVersion = values.version;
|
const explicitVersion = values.version;
|
||||||
let newVersion;
|
let newVersion;
|
||||||
|
|
||||||
if (explicitVersion) {
|
if (explicitVersion) {
|
||||||
newVersion = explicitVersion;
|
newVersion = explicitVersion;
|
||||||
} else {
|
} else {
|
||||||
const type = values.type;
|
|
||||||
if (!type || !isValidVersionType(type)) {
|
|
||||||
fail('Please specify the release type using --type or -t.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
newVersion = getNextVersion({
|
newVersion = getNextVersion({
|
||||||
currentVersion,
|
currentVersion,
|
||||||
type,
|
type: values.type,
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
fail(error instanceof Error ? error.message : String(error));
|
console.error(e.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,5 +76,6 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fail(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
console.error('Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
cd ../../
|
|
||||||
|
|
||||||
script="$1"
|
|
||||||
shift
|
|
||||||
exec node --import=extensionless/register --experimental-strip-types packages/ci-actions/"$script" "$@"
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsx": "bin/tsx",
|
"tsx": "node --import=extensionless/register --experimental-strip-types",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"typecheck": "tsgo -b"
|
"typecheck": "tsgo -b"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,69 +1,35 @@
|
|||||||
export const versionTypeArray = [
|
function parseVersion(version) {
|
||||||
'auto',
|
|
||||||
'hotfix',
|
|
||||||
'monthly',
|
|
||||||
'nightly',
|
|
||||||
] as const;
|
|
||||||
export type VersionType = (typeof versionTypeArray)[number];
|
|
||||||
|
|
||||||
type ParsedVersion = {
|
|
||||||
versionYear: number;
|
|
||||||
versionMonth: number;
|
|
||||||
versionHotfix: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GetNextVersionOptions = {
|
|
||||||
currentVersion: string;
|
|
||||||
type: VersionType;
|
|
||||||
currentDate?: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseVersion(version: string): ParsedVersion {
|
|
||||||
const [y, m, p] = version.split('.');
|
const [y, m, p] = version.split('.');
|
||||||
return {
|
return {
|
||||||
versionYear: Number.parseInt(y, 10),
|
versionYear: parseInt(y, 10),
|
||||||
versionMonth: Number.parseInt(m, 10),
|
versionMonth: parseInt(m, 10),
|
||||||
versionHotfix: Number.parseInt(p, 10),
|
versionHotfix: parseInt(p, 10),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeNextMonth(versionYear: number, versionMonth: number) {
|
function computeNextMonth(versionYear, versionMonth) {
|
||||||
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1);
|
// Create date and add 1 month
|
||||||
|
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
|
||||||
const nextVersionMonthDate = new Date(
|
const nextVersionMonthDate = new Date(
|
||||||
versionDate.getFullYear(),
|
versionDate.getFullYear(),
|
||||||
versionDate.getMonth() + 1,
|
versionDate.getMonth() + 1,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Format back to YY.M format
|
||||||
const fullYear = nextVersionMonthDate.getFullYear();
|
const fullYear = nextVersionMonthDate.getFullYear();
|
||||||
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
|
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
|
||||||
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1;
|
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
|
||||||
|
|
||||||
return { nextVersionYear, nextVersionMonth };
|
return { nextVersionYear, nextVersionMonth };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidVersionType(value: string): value is VersionType {
|
// Determine logical type from 'auto' based on the current date and version
|
||||||
return versionTypeArray.includes(value as VersionType);
|
function resolveType(type, currentDate, versionYear, versionMonth) {
|
||||||
}
|
if (type !== 'auto') return type;
|
||||||
|
|
||||||
function resolveType(
|
|
||||||
type: VersionType,
|
|
||||||
currentDate: Date,
|
|
||||||
versionYear: number,
|
|
||||||
versionMonth: number,
|
|
||||||
) {
|
|
||||||
if (type !== 'auto') {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inPatchMonth =
|
const inPatchMonth =
|
||||||
currentDate.getFullYear() === 2000 + versionYear &&
|
currentDate.getFullYear() === 2000 + versionYear &&
|
||||||
currentDate.getMonth() + 1 === versionMonth;
|
currentDate.getMonth() + 1 === versionMonth;
|
||||||
|
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
|
||||||
if (inPatchMonth && currentDate.getDate() <= 25) {
|
|
||||||
return 'hotfix';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'monthly';
|
return 'monthly';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +37,7 @@ export function getNextVersion({
|
|||||||
currentVersion,
|
currentVersion,
|
||||||
type,
|
type,
|
||||||
currentDate = new Date(),
|
currentDate = new Date(),
|
||||||
}: GetNextVersionOptions) {
|
}) {
|
||||||
const { versionYear, versionMonth, versionHotfix } =
|
const { versionYear, versionMonth, versionHotfix } =
|
||||||
parseVersion(currentVersion);
|
parseVersion(currentVersion);
|
||||||
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
|
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
|
||||||
@@ -85,10 +51,11 @@ export function getNextVersion({
|
|||||||
versionMonth,
|
versionMonth,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Format date stamp once for nightly
|
||||||
const currentDateString = currentDate
|
const currentDateString = currentDate
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.split('T')[0]
|
.split('T')[0]
|
||||||
.replace(/-/g, '');
|
.replaceAll('-', '');
|
||||||
|
|
||||||
switch (resolvedType) {
|
switch (resolvedType) {
|
||||||
case 'nightly':
|
case 'nightly':
|
||||||
@@ -99,7 +66,7 @@ export function getNextVersion({
|
|||||||
return `${nextVersionYear}.${nextVersionMonth}.0`;
|
return `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid type ${String(resolvedType satisfies never)} specified. Use "auto", "nightly", "hotfix", or "monthly".`,
|
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ describe('getNextVersion (lib)', () => {
|
|||||||
expect(() =>
|
expect(() =>
|
||||||
getNextVersion({
|
getNextVersion({
|
||||||
currentVersion: '25.8.4',
|
currentVersion: '25.8.4',
|
||||||
type: 'unknown' as never,
|
type: 'unknown',
|
||||||
currentDate: new Date('2025-08-10'),
|
currentDate: new Date('2025-08-10'),
|
||||||
}),
|
}),
|
||||||
).toThrow(/Invalid type/);
|
).toThrow(/Invalid type/);
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": [],
|
"lib": [],
|
||||||
"module": "es2022",
|
"module": "nodenext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "nodenext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
|
|||||||
7
packages/cli/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
dist
|
|
||||||
coverage
|
|
||||||
.actualrc.json
|
|
||||||
.actualrc
|
|
||||||
.actualrc.yaml
|
|
||||||
.actualrc.yml
|
|
||||||
actual.config.js
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# @actual-app/cli
|
|
||||||
|
|
||||||
> **WARNING:** This CLI is experimental.
|
|
||||||
|
|
||||||
Command-line interface for [Actual Budget](https://actualbudget.org). Query and modify your budget data from the terminal — accounts, transactions, categories, payees, rules, schedules, and more.
|
|
||||||
|
|
||||||
> **Note:** This CLI connects to a running [Actual sync server](https://actualbudget.org/docs/install/). It does not operate on local budget files directly.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @actual-app/cli
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires Node.js >= 22.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set connection details
|
|
||||||
export ACTUAL_SERVER_URL=http://localhost:5006
|
|
||||||
export ACTUAL_PASSWORD=your-password
|
|
||||||
export ACTUAL_SYNC_ID=your-sync-id # Found in Settings → Advanced → Sync ID
|
|
||||||
|
|
||||||
# List your accounts
|
|
||||||
actual accounts list
|
|
||||||
|
|
||||||
# Check a balance
|
|
||||||
actual accounts balance <account-id>
|
|
||||||
|
|
||||||
# View this month's budget
|
|
||||||
actual budgets month 2026-03
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configuration is resolved in this order (highest priority first):
|
|
||||||
|
|
||||||
1. **CLI flags** (`--server-url`, `--password`, etc.)
|
|
||||||
2. **Environment variables**
|
|
||||||
3. **Config file** (via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig))
|
|
||||||
4. **Defaults** (`dataDir` defaults to `~/.actual-cli/data`)
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
| ---------------------- | --------------------------------------------- |
|
|
||||||
| `ACTUAL_SERVER_URL` | URL of the Actual sync server (required) |
|
|
||||||
| `ACTUAL_PASSWORD` | Server password (required unless using token) |
|
|
||||||
| `ACTUAL_SESSION_TOKEN` | Session token (alternative to password) |
|
|
||||||
| `ACTUAL_SYNC_ID` | Budget Sync ID (required for most commands) |
|
|
||||||
| `ACTUAL_DATA_DIR` | Local directory for cached budget data |
|
|
||||||
|
|
||||||
### Config File
|
|
||||||
|
|
||||||
Create an `.actualrc.json` (or `.actualrc`, `.actualrc.yaml`, `actual.config.js`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"serverUrl": "http://localhost:5006",
|
|
||||||
"password": "your-password",
|
|
||||||
"syncId": "1cfdbb80-6274-49bf-b0c2-737235a4c81f"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Security:** Do not store plaintext passwords in config files (e.g. `.actualrc.json`, `.actualrc`, `.actualrc.yaml`, `actual.config.js`). Add these files to `.gitignore` if they contain secrets. Prefer the `ACTUAL_SESSION_TOKEN` environment variable instead of the `password` field. See [Environment Variables](#environment-variables) for using a session token.
|
|
||||||
|
|
||||||
### Global Flags
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
| ------------------------- | ----------------------------------------------- |
|
|
||||||
| `--server-url <url>` | Server URL |
|
|
||||||
| `--password <pw>` | Server password |
|
|
||||||
| `--session-token <token>` | Session token |
|
|
||||||
| `--sync-id <id>` | Budget Sync ID |
|
|
||||||
| `--data-dir <path>` | Data directory |
|
|
||||||
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
|
|
||||||
| `--verbose` | Show informational messages |
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| ----------------- | ------------------------------ |
|
|
||||||
| `accounts` | Manage accounts |
|
|
||||||
| `budgets` | Manage budgets and allocations |
|
|
||||||
| `categories` | Manage categories |
|
|
||||||
| `category-groups` | Manage category groups |
|
|
||||||
| `transactions` | Manage transactions |
|
|
||||||
| `payees` | Manage payees |
|
|
||||||
| `tags` | Manage tags |
|
|
||||||
| `rules` | Manage transaction rules |
|
|
||||||
| `schedules` | Manage scheduled transactions |
|
|
||||||
| `query` | Run an ActualQL query |
|
|
||||||
| `server` | Server utilities and lookups |
|
|
||||||
|
|
||||||
Run `actual <command> --help` for subcommands and options.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all accounts (as a table)
|
|
||||||
actual accounts list --format table
|
|
||||||
|
|
||||||
# Find an entity ID by name
|
|
||||||
actual server get-id --type accounts --name "Checking"
|
|
||||||
|
|
||||||
# Add a transaction (amount in integer cents: -2500 = -$25.00)
|
|
||||||
actual transactions add --account <id> \
|
|
||||||
--data '[{"date":"2026-03-14","amount":-2500,"payee_name":"Coffee Shop"}]'
|
|
||||||
|
|
||||||
# Export transactions to CSV
|
|
||||||
actual transactions list --account <id> \
|
|
||||||
--start 2026-01-01 --end 2026-12-31 --format csv > transactions.csv
|
|
||||||
|
|
||||||
# Set budget amount ($500 = 50000 cents)
|
|
||||||
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
|
|
||||||
|
|
||||||
# Run an ActualQL query
|
|
||||||
actual query run --table transactions \
|
|
||||||
--select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
|
|
||||||
```
|
|
||||||
|
|
||||||
### Amount Convention
|
|
||||||
|
|
||||||
All monetary amounts are **integer cents**:
|
|
||||||
|
|
||||||
| CLI Value | Dollar Amount |
|
|
||||||
| --------- | ------------- |
|
|
||||||
| `5000` | $50.00 |
|
|
||||||
| `-12350` | -$123.50 |
|
|
||||||
|
|
||||||
## Running Locally (Development)
|
|
||||||
|
|
||||||
If you're working on the CLI within the monorepo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build the CLI
|
|
||||||
yarn build:cli
|
|
||||||
|
|
||||||
# 2. Start a local sync server (in a separate terminal)
|
|
||||||
yarn start:server-dev
|
|
||||||
|
|
||||||
# 3. Open http://localhost:5006 in your browser, create a budget,
|
|
||||||
# then find the Sync ID in Settings → Advanced → Sync ID
|
|
||||||
|
|
||||||
# 4. Run the CLI directly from the build output
|
|
||||||
ACTUAL_SERVER_URL=http://localhost:5006 \
|
|
||||||
ACTUAL_PASSWORD=your-password \
|
|
||||||
ACTUAL_SYNC_ID=your-sync-id \
|
|
||||||
node packages/cli/dist/cli.js accounts list
|
|
||||||
|
|
||||||
# Or use a shorthand alias for convenience
|
|
||||||
alias actual-dev="node $(pwd)/packages/cli/dist/cli.js"
|
|
||||||
actual-dev budgets list
|
|
||||||
```
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@actual-app/cli",
|
|
||||||
"version": "26.3.0",
|
|
||||||
"description": "CLI for Actual Budget",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"actual": "./dist/cli.js",
|
|
||||||
"actual-cli": "./dist/cli.js"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"test": "vitest --run",
|
|
||||||
"typecheck": "tsgo -b"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@actual-app/api": "workspace:*",
|
|
||||||
"cli-table3": "^0.6.5",
|
|
||||||
"commander": "^13.0.0",
|
|
||||||
"cosmiconfig": "^9.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.19.15",
|
|
||||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
|
||||||
"rollup-plugin-visualizer": "^6.0.11",
|
|
||||||
"vite": "^8.0.0",
|
|
||||||
"vitest": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import { Command } from 'commander';
|
|
||||||
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
import { registerAccountsCommand } from './accounts';
|
|
||||||
|
|
||||||
vi.mock('@actual-app/api', () => ({
|
|
||||||
getAccounts: vi.fn().mockResolvedValue([]),
|
|
||||||
createAccount: vi.fn().mockResolvedValue('new-id'),
|
|
||||||
updateAccount: vi.fn().mockResolvedValue(undefined),
|
|
||||||
closeAccount: vi.fn().mockResolvedValue(undefined),
|
|
||||||
reopenAccount: vi.fn().mockResolvedValue(undefined),
|
|
||||||
deleteAccount: vi.fn().mockResolvedValue(undefined),
|
|
||||||
getAccountBalance: vi.fn().mockResolvedValue(10000),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../connection', () => ({
|
|
||||||
withConnection: vi.fn((_opts, fn) => fn()),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../output', () => ({
|
|
||||||
printOutput: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function createProgram(): Command {
|
|
||||||
const program = new Command();
|
|
||||||
program.option('--format <format>');
|
|
||||||
program.option('--server-url <url>');
|
|
||||||
program.option('--password <pw>');
|
|
||||||
program.option('--session-token <token>');
|
|
||||||
program.option('--sync-id <id>');
|
|
||||||
program.option('--data-dir <dir>');
|
|
||||||
program.option('--verbose');
|
|
||||||
program.exitOverride();
|
|
||||||
registerAccountsCommand(program);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(args: string[]) {
|
|
||||||
const program = createProgram();
|
|
||||||
await program.parseAsync(['node', 'test', ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('accounts commands', () => {
|
|
||||||
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
|
||||||
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
stderrSpy = vi
|
|
||||||
.spyOn(process.stderr, 'write')
|
|
||||||
.mockImplementation(() => true);
|
|
||||||
stdoutSpy = vi
|
|
||||||
.spyOn(process.stdout, 'write')
|
|
||||||
.mockImplementation(() => true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
stderrSpy.mockRestore();
|
|
||||||
stdoutSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('list', () => {
|
|
||||||
it('calls api.getAccounts and prints result', async () => {
|
|
||||||
const accounts = [{ id: '1', name: 'Checking' }];
|
|
||||||
vi.mocked(api.getAccounts).mockResolvedValue(accounts);
|
|
||||||
|
|
||||||
await run(['accounts', 'list']);
|
|
||||||
|
|
||||||
expect(api.getAccounts).toHaveBeenCalled();
|
|
||||||
expect(printOutput).toHaveBeenCalledWith(accounts, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes format option to printOutput', async () => {
|
|
||||||
vi.mocked(api.getAccounts).mockResolvedValue([]);
|
|
||||||
|
|
||||||
await run(['--format', 'csv', 'accounts', 'list']);
|
|
||||||
|
|
||||||
expect(printOutput).toHaveBeenCalledWith([], 'csv');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('passes name and defaults to api.createAccount', async () => {
|
|
||||||
await run(['accounts', 'create', '--name', 'Savings']);
|
|
||||||
|
|
||||||
expect(api.createAccount).toHaveBeenCalledWith(
|
|
||||||
{ name: 'Savings', offbudget: false },
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
expect(printOutput).toHaveBeenCalledWith({ id: 'new-id' }, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes offbudget and balance options', async () => {
|
|
||||||
await run([
|
|
||||||
'accounts',
|
|
||||||
'create',
|
|
||||||
'--name',
|
|
||||||
'Investments',
|
|
||||||
'--offbudget',
|
|
||||||
'--balance',
|
|
||||||
'50000',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.createAccount).toHaveBeenCalledWith(
|
|
||||||
{ name: 'Investments', offbudget: true },
|
|
||||||
50000,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('passes fields to api.updateAccount', async () => {
|
|
||||||
await run(['accounts', 'update', 'acct-1', '--name', 'NewName']);
|
|
||||||
|
|
||||||
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
|
|
||||||
name: 'NewName',
|
|
||||||
});
|
|
||||||
expect(printOutput).toHaveBeenCalledWith(
|
|
||||||
{ success: true, id: 'acct-1' },
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes offbudget true', async () => {
|
|
||||||
await run([
|
|
||||||
'accounts',
|
|
||||||
'update',
|
|
||||||
'acct-1',
|
|
||||||
'--name',
|
|
||||||
'X',
|
|
||||||
'--offbudget',
|
|
||||||
'true',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
|
|
||||||
name: 'X',
|
|
||||||
offbudget: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes offbudget false', async () => {
|
|
||||||
await run([
|
|
||||||
'accounts',
|
|
||||||
'update',
|
|
||||||
'acct-1',
|
|
||||||
'--name',
|
|
||||||
'X',
|
|
||||||
'--offbudget',
|
|
||||||
'false',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
|
|
||||||
name: 'X',
|
|
||||||
offbudget: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid offbudget value', async () => {
|
|
||||||
await expect(
|
|
||||||
run(['accounts', 'update', 'acct-1', '--offbudget', 'yes']),
|
|
||||||
).rejects.toThrow(
|
|
||||||
'Invalid --offbudget: "yes". Expected "true" or "false".',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects empty name', async () => {
|
|
||||||
await expect(
|
|
||||||
run(['accounts', 'update', 'acct-1', '--name', ' ']),
|
|
||||||
).rejects.toThrow('Invalid --name: must be a non-empty string.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects update with no fields', async () => {
|
|
||||||
await expect(run(['accounts', 'update', 'acct-1'])).rejects.toThrow(
|
|
||||||
'No update fields provided. Use --name or --offbudget.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('close', () => {
|
|
||||||
it('passes transfer options to api.closeAccount', async () => {
|
|
||||||
await run([
|
|
||||||
'accounts',
|
|
||||||
'close',
|
|
||||||
'acct-1',
|
|
||||||
'--transfer-account',
|
|
||||||
'acct-2',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.closeAccount).toHaveBeenCalledWith(
|
|
||||||
'acct-1',
|
|
||||||
'acct-2',
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes transfer category', async () => {
|
|
||||||
await run([
|
|
||||||
'accounts',
|
|
||||||
'close',
|
|
||||||
'acct-1',
|
|
||||||
'--transfer-category',
|
|
||||||
'cat-1',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.closeAccount).toHaveBeenCalledWith(
|
|
||||||
'acct-1',
|
|
||||||
undefined,
|
|
||||||
'cat-1',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('reopen', () => {
|
|
||||||
it('calls api.reopenAccount', async () => {
|
|
||||||
await run(['accounts', 'reopen', 'acct-1']);
|
|
||||||
|
|
||||||
expect(api.reopenAccount).toHaveBeenCalledWith('acct-1');
|
|
||||||
expect(printOutput).toHaveBeenCalledWith(
|
|
||||||
{ success: true, id: 'acct-1' },
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('calls api.deleteAccount', async () => {
|
|
||||||
await run(['accounts', 'delete', 'acct-1']);
|
|
||||||
|
|
||||||
expect(api.deleteAccount).toHaveBeenCalledWith('acct-1');
|
|
||||||
expect(printOutput).toHaveBeenCalledWith(
|
|
||||||
{ success: true, id: 'acct-1' },
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('balance', () => {
|
|
||||||
it('calls api.getAccountBalance without cutoff', async () => {
|
|
||||||
await run(['accounts', 'balance', 'acct-1']);
|
|
||||||
|
|
||||||
expect(api.getAccountBalance).toHaveBeenCalledWith('acct-1', undefined);
|
|
||||||
expect(printOutput).toHaveBeenCalledWith(
|
|
||||||
{ id: 'acct-1', balance: 10000 },
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.getAccountBalance with cutoff date', async () => {
|
|
||||||
await run(['accounts', 'balance', 'acct-1', '--cutoff', '2025-01-15']);
|
|
||||||
|
|
||||||
expect(api.getAccountBalance).toHaveBeenCalledWith(
|
|
||||||
'acct-1',
|
|
||||||
new Date('2025-01-15'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
import { parseBoolFlag, parseIntFlag } from '../utils';
|
|
||||||
|
|
||||||
export function registerAccountsCommand(program: Command) {
|
|
||||||
const accounts = program.command('accounts').description('Manage accounts');
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('list')
|
|
||||||
.description('List all accounts')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getAccounts();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new account')
|
|
||||||
.requiredOption('--name <name>', 'Account name')
|
|
||||||
.option('--offbudget', 'Create as off-budget account', false)
|
|
||||||
.option('--balance <amount>', 'Initial balance in cents', '0')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const balance = parseIntFlag(cmdOpts.balance, '--balance');
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.createAccount(
|
|
||||||
{ name: cmdOpts.name, offbudget: cmdOpts.offbudget },
|
|
||||||
balance,
|
|
||||||
);
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update an account')
|
|
||||||
.option('--name <name>', 'New account name')
|
|
||||||
.option('--offbudget <bool>', 'Set off-budget status')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
const fields: Record<string, unknown> = {};
|
|
||||||
if (cmdOpts.name !== undefined) {
|
|
||||||
const trimmed = cmdOpts.name.trim();
|
|
||||||
if (trimmed === '') {
|
|
||||||
throw new Error('Invalid --name: must be a non-empty string.');
|
|
||||||
}
|
|
||||||
fields.name = trimmed;
|
|
||||||
}
|
|
||||||
if (cmdOpts.offbudget !== undefined) {
|
|
||||||
fields.offbudget = parseBoolFlag(cmdOpts.offbudget, '--offbudget');
|
|
||||||
}
|
|
||||||
if (Object.keys(fields).length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
'No update fields provided. Use --name or --offbudget.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.updateAccount(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('close <id>')
|
|
||||||
.description('Close an account')
|
|
||||||
.option(
|
|
||||||
'--transfer-account <id>',
|
|
||||||
'Transfer remaining balance to this account',
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
'--transfer-category <id>',
|
|
||||||
'Transfer remaining balance to this category',
|
|
||||||
)
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.closeAccount(
|
|
||||||
id,
|
|
||||||
cmdOpts.transferAccount,
|
|
||||||
cmdOpts.transferCategory,
|
|
||||||
);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('reopen <id>')
|
|
||||||
.description('Reopen a closed account')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.reopenAccount(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete an account')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteAccount(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.command('balance <id>')
|
|
||||||
.description('Get account balance')
|
|
||||||
.option('--cutoff <date>', 'Cutoff date (YYYY-MM-DD)')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
let cutoff: Date | undefined;
|
|
||||||
if (cmdOpts.cutoff) {
|
|
||||||
const cutoffDate = new Date(cmdOpts.cutoff);
|
|
||||||
if (Number.isNaN(cutoffDate.getTime())) {
|
|
||||||
throw new Error(
|
|
||||||
'Invalid cutoff date: expected a valid date (e.g. YYYY-MM-DD).',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
cutoff = cutoffDate;
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const balance = await api.getAccountBalance(id, cutoff);
|
|
||||||
printOutput({ id, balance }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { resolveConfig } from '../config';
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
import { parseBoolFlag, parseIntFlag } from '../utils';
|
|
||||||
|
|
||||||
export function registerBudgetsCommand(program: Command) {
|
|
||||||
const budgets = program.command('budgets').description('Manage budgets');
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('list')
|
|
||||||
.description('List all available budgets')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(
|
|
||||||
opts,
|
|
||||||
async () => {
|
|
||||||
const result = await api.getBudgets();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
},
|
|
||||||
{ loadBudget: false },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('download <syncId>')
|
|
||||||
.description('Download a budget by sync ID')
|
|
||||||
.option('--encryption-password <password>', 'Encryption password')
|
|
||||||
.action(async (syncId: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
const config = await resolveConfig(opts);
|
|
||||||
const password = config.encryptionPassword ?? cmdOpts.encryptionPassword;
|
|
||||||
await withConnection(
|
|
||||||
opts,
|
|
||||||
async () => {
|
|
||||||
await api.downloadBudget(syncId, {
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
printOutput({ success: true, syncId }, opts.format);
|
|
||||||
},
|
|
||||||
{ loadBudget: false },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('sync')
|
|
||||||
.description('Sync the current budget')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.sync();
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('months')
|
|
||||||
.description('List available budget months')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getBudgetMonths();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('month <month>')
|
|
||||||
.description('Get budget data for a specific month (YYYY-MM)')
|
|
||||||
.action(async (month: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getBudgetMonth(month);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('set-amount')
|
|
||||||
.description('Set budget amount for a category in a month')
|
|
||||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
|
||||||
.requiredOption('--category <id>', 'Category ID')
|
|
||||||
.requiredOption('--amount <amount>', 'Amount in cents')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const amount = parseIntFlag(cmdOpts.amount, '--amount');
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.setBudgetAmount(cmdOpts.month, cmdOpts.category, amount);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('set-carryover')
|
|
||||||
.description('Enable/disable carryover for a category')
|
|
||||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
|
||||||
.requiredOption('--category <id>', 'Category ID')
|
|
||||||
.requiredOption('--flag <bool>', 'Enable (true) or disable (false)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const flag = parseBoolFlag(cmdOpts.flag, '--flag');
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.setBudgetCarryover(cmdOpts.month, cmdOpts.category, flag);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('hold-next-month')
|
|
||||||
.description('Hold budget amount for next month')
|
|
||||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
|
||||||
.requiredOption('--amount <amount>', 'Amount in cents')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const parsedAmount = parseIntFlag(cmdOpts.amount, '--amount');
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.holdBudgetForNextMonth(cmdOpts.month, parsedAmount);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
budgets
|
|
||||||
.command('reset-hold')
|
|
||||||
.description('Reset budget hold for a month')
|
|
||||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.resetBudgetHold(cmdOpts.month);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
import { parseBoolFlag } from '../utils';
|
|
||||||
|
|
||||||
export function registerCategoriesCommand(program: Command) {
|
|
||||||
const categories = program
|
|
||||||
.command('categories')
|
|
||||||
.description('Manage categories');
|
|
||||||
|
|
||||||
categories
|
|
||||||
.command('list')
|
|
||||||
.description('List all categories')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getCategories();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
categories
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new category')
|
|
||||||
.requiredOption('--name <name>', 'Category name')
|
|
||||||
.requiredOption('--group-id <id>', 'Category group ID')
|
|
||||||
.option('--is-income', 'Mark as income category', false)
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.createCategory({
|
|
||||||
name: cmdOpts.name,
|
|
||||||
group_id: cmdOpts.groupId,
|
|
||||||
is_income: cmdOpts.isIncome,
|
|
||||||
hidden: false,
|
|
||||||
});
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
categories
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a category')
|
|
||||||
.option('--name <name>', 'New category name')
|
|
||||||
.option('--hidden <bool>', 'Set hidden status')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const fields: Record<string, unknown> = {};
|
|
||||||
if (cmdOpts.name !== undefined) fields.name = cmdOpts.name;
|
|
||||||
if (cmdOpts.hidden !== undefined) {
|
|
||||||
fields.hidden = parseBoolFlag(cmdOpts.hidden, '--hidden');
|
|
||||||
}
|
|
||||||
if (Object.keys(fields).length === 0) {
|
|
||||||
throw new Error('No update fields provided. Use --name or --hidden.');
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.updateCategory(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
categories
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a category')
|
|
||||||
.option('--transfer-to <id>', 'Transfer transactions to this category')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteCategory(id, cmdOpts.transferTo);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
import { parseBoolFlag } from '../utils';
|
|
||||||
|
|
||||||
export function registerCategoryGroupsCommand(program: Command) {
|
|
||||||
const groups = program
|
|
||||||
.command('category-groups')
|
|
||||||
.description('Manage category groups');
|
|
||||||
|
|
||||||
groups
|
|
||||||
.command('list')
|
|
||||||
.description('List all category groups')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getCategoryGroups();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
groups
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new category group')
|
|
||||||
.requiredOption('--name <name>', 'Group name')
|
|
||||||
.option('--is-income', 'Mark as income group', false)
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.createCategoryGroup({
|
|
||||||
name: cmdOpts.name,
|
|
||||||
is_income: cmdOpts.isIncome,
|
|
||||||
hidden: false,
|
|
||||||
});
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
groups
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a category group')
|
|
||||||
.option('--name <name>', 'New group name')
|
|
||||||
.option('--hidden <bool>', 'Set hidden status')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const fields: Record<string, unknown> = {};
|
|
||||||
if (cmdOpts.name !== undefined) fields.name = cmdOpts.name;
|
|
||||||
if (cmdOpts.hidden !== undefined) {
|
|
||||||
fields.hidden = parseBoolFlag(cmdOpts.hidden, '--hidden');
|
|
||||||
}
|
|
||||||
if (Object.keys(fields).length === 0) {
|
|
||||||
throw new Error('No update fields provided. Use --name or --hidden.');
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.updateCategoryGroup(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
groups
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a category group')
|
|
||||||
.option('--transfer-to <id>', 'Transfer transactions to this category ID')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteCategoryGroup(id, cmdOpts.transferTo);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerPayeesCommand(program: Command) {
|
|
||||||
const payees = program.command('payees').description('Manage payees');
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('list')
|
|
||||||
.description('List all payees')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getPayees();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('common')
|
|
||||||
.description('List frequently used payees')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getCommonPayees();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new payee')
|
|
||||||
.requiredOption('--name <name>', 'Payee name')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.createPayee({ name: cmdOpts.name });
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a payee')
|
|
||||||
.option('--name <name>', 'New payee name')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const fields: Record<string, unknown> = {};
|
|
||||||
if (cmdOpts.name) fields.name = cmdOpts.name;
|
|
||||||
if (Object.keys(fields).length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
'No fields to update. Use --name to specify a new name.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.updatePayee(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a payee')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deletePayee(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
payees
|
|
||||||
.command('merge')
|
|
||||||
.description('Merge payees into a target payee')
|
|
||||||
.requiredOption('--target <id>', 'Target payee ID')
|
|
||||||
.requiredOption('--ids <ids>', 'Comma-separated payee IDs to merge')
|
|
||||||
.action(async (cmdOpts: { target: string; ids: string }) => {
|
|
||||||
const mergeIds = cmdOpts.ids
|
|
||||||
.split(',')
|
|
||||||
.map(id => id.trim())
|
|
||||||
.filter(id => id.length > 0);
|
|
||||||
if (mergeIds.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
'No valid payee IDs provided in --ids. Provide comma-separated IDs.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.mergePayees(cmdOpts.target, mergeIds);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { readJsonInput } from '../input';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
import { parseIntFlag } from '../utils';
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildQueryFromFile(
|
|
||||||
parsed: Record<string, unknown>,
|
|
||||||
fallbackTable: string | undefined,
|
|
||||||
) {
|
|
||||||
const table = typeof parsed.table === 'string' ? parsed.table : fallbackTable;
|
|
||||||
if (!table) {
|
|
||||||
throw new Error(
|
|
||||||
'--table is required when the input file lacks a "table" field',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let queryObj = api.q(table);
|
|
||||||
if (Array.isArray(parsed.select)) queryObj = queryObj.select(parsed.select);
|
|
||||||
if (isRecord(parsed.filter)) queryObj = queryObj.filter(parsed.filter);
|
|
||||||
if (Array.isArray(parsed.orderBy)) {
|
|
||||||
queryObj = queryObj.orderBy(parsed.orderBy);
|
|
||||||
}
|
|
||||||
if (typeof parsed.limit === 'number') queryObj = queryObj.limit(parsed.limit);
|
|
||||||
return queryObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildQueryFromFlags(cmdOpts: Record<string, string | undefined>) {
|
|
||||||
if (!cmdOpts.table) {
|
|
||||||
throw new Error('--table is required (or use --file)');
|
|
||||||
}
|
|
||||||
let queryObj = api.q(cmdOpts.table);
|
|
||||||
|
|
||||||
if (cmdOpts.select) {
|
|
||||||
queryObj = queryObj.select(cmdOpts.select.split(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdOpts.filter) {
|
|
||||||
queryObj = queryObj.filter(JSON.parse(cmdOpts.filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdOpts.orderBy) {
|
|
||||||
queryObj = queryObj.orderBy(cmdOpts.orderBy.split(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdOpts.limit) {
|
|
||||||
queryObj = queryObj.limit(parseIntFlag(cmdOpts.limit, '--limit'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerQueryCommand(program: Command) {
|
|
||||||
const query = program
|
|
||||||
.command('query')
|
|
||||||
.description('Run AQL (Actual Query Language) queries');
|
|
||||||
|
|
||||||
query
|
|
||||||
.command('run')
|
|
||||||
.description('Execute an AQL query')
|
|
||||||
.option(
|
|
||||||
'--table <table>',
|
|
||||||
'Table to query (transactions, accounts, categories, payees)',
|
|
||||||
)
|
|
||||||
.option('--select <fields>', 'Comma-separated fields to select')
|
|
||||||
.option('--filter <json>', 'Filter expression as JSON')
|
|
||||||
.option('--order-by <fields>', 'Comma-separated fields to order by')
|
|
||||||
.option('--limit <n>', 'Limit number of results')
|
|
||||||
.option(
|
|
||||||
'--file <path>',
|
|
||||||
'Read full query object from JSON file (use - for stdin)',
|
|
||||||
)
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const parsed = cmdOpts.file ? readJsonInput(cmdOpts) : undefined;
|
|
||||||
if (parsed !== undefined && !isRecord(parsed)) {
|
|
||||||
throw new Error('Query file must contain a JSON object');
|
|
||||||
}
|
|
||||||
const queryObj = parsed
|
|
||||||
? buildQueryFromFile(parsed, cmdOpts.table)
|
|
||||||
: buildQueryFromFlags(cmdOpts);
|
|
||||||
|
|
||||||
const result = await api.aqlQuery(queryObj);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { readJsonInput } from '../input';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerRulesCommand(program: Command) {
|
|
||||||
const rules = program
|
|
||||||
.command('rules')
|
|
||||||
.description('Manage transaction rules');
|
|
||||||
|
|
||||||
rules
|
|
||||||
.command('list')
|
|
||||||
.description('List all rules')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getRules();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rules
|
|
||||||
.command('payee-rules <payeeId>')
|
|
||||||
.description('List rules for a specific payee')
|
|
||||||
.action(async (payeeId: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getPayeeRules(payeeId);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rules
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new rule')
|
|
||||||
.option('--data <json>', 'Rule definition as JSON')
|
|
||||||
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const rule = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.createRule
|
|
||||||
>[0];
|
|
||||||
const id = await api.createRule(rule);
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rules
|
|
||||||
.command('update')
|
|
||||||
.description('Update a rule')
|
|
||||||
.option('--data <json>', 'Rule data as JSON (must include id)')
|
|
||||||
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const rule = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.updateRule
|
|
||||||
>[0];
|
|
||||||
await api.updateRule(rule);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rules
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a rule')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteRule(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { readJsonInput } from '../input';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerSchedulesCommand(program: Command) {
|
|
||||||
const schedules = program
|
|
||||||
.command('schedules')
|
|
||||||
.description('Manage scheduled transactions');
|
|
||||||
|
|
||||||
schedules
|
|
||||||
.command('list')
|
|
||||||
.description('List all schedules')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getSchedules();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
schedules
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new schedule')
|
|
||||||
.option('--data <json>', 'Schedule definition as JSON')
|
|
||||||
.option('--file <path>', 'Read schedule from JSON file (use - for stdin)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const schedule = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.createSchedule
|
|
||||||
>[0];
|
|
||||||
const id = await api.createSchedule(schedule);
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
schedules
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a schedule')
|
|
||||||
.option('--data <json>', 'Fields to update as JSON')
|
|
||||||
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
|
|
||||||
.option('--reset-next-date', 'Reset next occurrence date', false)
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const fields = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.updateSchedule
|
|
||||||
>[1];
|
|
||||||
await api.updateSchedule(id, fields, cmdOpts.resetNextDate);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
schedules
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a schedule')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteSchedule(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import { Option } from 'commander';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerServerCommand(program: Command) {
|
|
||||||
const server = program.command('server').description('Server utilities');
|
|
||||||
|
|
||||||
server
|
|
||||||
.command('version')
|
|
||||||
.description('Get server version')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(
|
|
||||||
opts,
|
|
||||||
async () => {
|
|
||||||
const version = await api.getServerVersion();
|
|
||||||
printOutput({ version }, opts.format);
|
|
||||||
},
|
|
||||||
{ loadBudget: false },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
server
|
|
||||||
.command('get-id')
|
|
||||||
.description('Get entity ID by name')
|
|
||||||
.addOption(
|
|
||||||
new Option('--type <type>', 'Entity type')
|
|
||||||
.choices(['accounts', 'categories', 'payees', 'schedules'])
|
|
||||||
.makeOptionMandatory(),
|
|
||||||
)
|
|
||||||
.requiredOption('--name <name>', 'Entity name')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.getIDByName(cmdOpts.type, cmdOpts.name);
|
|
||||||
printOutput(
|
|
||||||
{ id, type: cmdOpts.type, name: cmdOpts.name },
|
|
||||||
opts.format,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server
|
|
||||||
.command('bank-sync')
|
|
||||||
.description('Run bank synchronization')
|
|
||||||
.option('--account <id>', 'Specific account ID to sync')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const args = cmdOpts.account
|
|
||||||
? { accountId: cmdOpts.account }
|
|
||||||
: undefined;
|
|
||||||
await api.runBankSync(args);
|
|
||||||
printOutput({ success: true }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerTagsCommand(program: Command) {
|
|
||||||
const tags = program.command('tags').description('Manage tags');
|
|
||||||
|
|
||||||
tags
|
|
||||||
.command('list')
|
|
||||||
.description('List all tags')
|
|
||||||
.action(async () => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getTags();
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tags
|
|
||||||
.command('create')
|
|
||||||
.description('Create a new tag')
|
|
||||||
.requiredOption('--tag <tag>', 'Tag name')
|
|
||||||
.option('--color <color>', 'Tag color')
|
|
||||||
.option('--description <description>', 'Tag description')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const id = await api.createTag({
|
|
||||||
tag: cmdOpts.tag,
|
|
||||||
color: cmdOpts.color,
|
|
||||||
description: cmdOpts.description,
|
|
||||||
});
|
|
||||||
printOutput({ id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tags
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a tag')
|
|
||||||
.option('--tag <tag>', 'New tag name')
|
|
||||||
.option('--color <color>', 'New tag color')
|
|
||||||
.option('--description <description>', 'New tag description')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const fields: Record<string, unknown> = {};
|
|
||||||
if (cmdOpts.tag !== undefined) fields.tag = cmdOpts.tag;
|
|
||||||
if (cmdOpts.color !== undefined) fields.color = cmdOpts.color;
|
|
||||||
if (cmdOpts.description !== undefined) {
|
|
||||||
fields.description = cmdOpts.description;
|
|
||||||
}
|
|
||||||
if (Object.keys(fields).length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
'At least one of --tag, --color, or --description is required',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.updateTag(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tags
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a tag')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteTag(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
import type { Command } from 'commander';
|
|
||||||
|
|
||||||
import { withConnection } from '../connection';
|
|
||||||
import { readJsonInput } from '../input';
|
|
||||||
import { printOutput } from '../output';
|
|
||||||
|
|
||||||
export function registerTransactionsCommand(program: Command) {
|
|
||||||
const transactions = program
|
|
||||||
.command('transactions')
|
|
||||||
.description('Manage transactions');
|
|
||||||
|
|
||||||
transactions
|
|
||||||
.command('list')
|
|
||||||
.description('List transactions for an account')
|
|
||||||
.requiredOption('--account <id>', 'Account ID')
|
|
||||||
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
||||||
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const result = await api.getTransactions(
|
|
||||||
cmdOpts.account,
|
|
||||||
cmdOpts.start,
|
|
||||||
cmdOpts.end,
|
|
||||||
);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
transactions
|
|
||||||
.command('add')
|
|
||||||
.description('Add transactions to an account')
|
|
||||||
.requiredOption('--account <id>', 'Account ID')
|
|
||||||
.option('--data <json>', 'Transaction data as JSON array')
|
|
||||||
.option(
|
|
||||||
'--file <path>',
|
|
||||||
'Read transaction data from JSON file (use - for stdin)',
|
|
||||||
)
|
|
||||||
.option('--learn-categories', 'Learn category assignments', false)
|
|
||||||
.option('--run-transfers', 'Process transfers', false)
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const transactions = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.addTransactions
|
|
||||||
>[1];
|
|
||||||
const result = await api.addTransactions(
|
|
||||||
cmdOpts.account,
|
|
||||||
transactions,
|
|
||||||
{
|
|
||||||
learnCategories: cmdOpts.learnCategories,
|
|
||||||
runTransfers: cmdOpts.runTransfers,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
transactions
|
|
||||||
.command('import')
|
|
||||||
.description('Import transactions to an account')
|
|
||||||
.requiredOption('--account <id>', 'Account ID')
|
|
||||||
.option('--data <json>', 'Transaction data as JSON array')
|
|
||||||
.option(
|
|
||||||
'--file <path>',
|
|
||||||
'Read transaction data from JSON file (use - for stdin)',
|
|
||||||
)
|
|
||||||
.option('--dry-run', 'Preview without importing', false)
|
|
||||||
.action(async cmdOpts => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const transactions = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.importTransactions
|
|
||||||
>[1];
|
|
||||||
const result = await api.importTransactions(
|
|
||||||
cmdOpts.account,
|
|
||||||
transactions,
|
|
||||||
{
|
|
||||||
defaultCleared: true,
|
|
||||||
dryRun: cmdOpts.dryRun,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
printOutput(result, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
transactions
|
|
||||||
.command('update <id>')
|
|
||||||
.description('Update a transaction')
|
|
||||||
.option('--data <json>', 'Fields to update as JSON')
|
|
||||||
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
|
|
||||||
.action(async (id: string, cmdOpts) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
const fields = readJsonInput(cmdOpts) as Parameters<
|
|
||||||
typeof api.updateTransaction
|
|
||||||
>[1];
|
|
||||||
await api.updateTransaction(id, fields);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
transactions
|
|
||||||
.command('delete <id>')
|
|
||||||
.description('Delete a transaction')
|
|
||||||
.action(async (id: string) => {
|
|
||||||
const opts = program.opts();
|
|
||||||
await withConnection(opts, async () => {
|
|
||||||
await api.deleteTransaction(id);
|
|
||||||
printOutput({ success: true, id }, opts.format);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { homedir } from 'os';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { resolveConfig } from './config';
|
|
||||||
|
|
||||||
const mockSearch = vi.fn().mockResolvedValue(null);
|
|
||||||
|
|
||||||
vi.mock('cosmiconfig', () => ({
|
|
||||||
cosmiconfig: () => ({
|
|
||||||
search: (...args: unknown[]) => mockSearch(...args),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function mockConfigFile(config: Record<string, unknown> | null) {
|
|
||||||
if (config) {
|
|
||||||
mockSearch.mockResolvedValue({ config, isEmpty: false });
|
|
||||||
} else {
|
|
||||||
mockSearch.mockResolvedValue(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('resolveConfig', () => {
|
|
||||||
const savedEnv: Record<string, string | undefined> = {};
|
|
||||||
const envKeys = [
|
|
||||||
'ACTUAL_SERVER_URL',
|
|
||||||
'ACTUAL_PASSWORD',
|
|
||||||
'ACTUAL_SESSION_TOKEN',
|
|
||||||
'ACTUAL_SYNC_ID',
|
|
||||||
'ACTUAL_DATA_DIR',
|
|
||||||
'ACTUAL_ENCRYPTION_PASSWORD',
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
for (const key of envKeys) {
|
|
||||||
savedEnv[key] = process.env[key];
|
|
||||||
delete process.env[key];
|
|
||||||
}
|
|
||||||
mockConfigFile(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
for (const key of envKeys) {
|
|
||||||
if (savedEnv[key] !== undefined) {
|
|
||||||
process.env[key] = savedEnv[key];
|
|
||||||
} else {
|
|
||||||
delete process.env[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('priority chain', () => {
|
|
||||||
it('CLI opts take highest priority', async () => {
|
|
||||||
process.env.ACTUAL_SERVER_URL = 'http://env';
|
|
||||||
process.env.ACTUAL_PASSWORD = 'envpw';
|
|
||||||
process.env.ACTUAL_ENCRYPTION_PASSWORD = 'env-enc';
|
|
||||||
mockConfigFile({
|
|
||||||
serverUrl: 'http://file',
|
|
||||||
password: 'filepw',
|
|
||||||
encryptionPassword: 'file-enc',
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://cli',
|
|
||||||
password: 'clipw',
|
|
||||||
encryptionPassword: 'cli-enc',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.serverUrl).toBe('http://cli');
|
|
||||||
expect(config.password).toBe('clipw');
|
|
||||||
expect(config.encryptionPassword).toBe('cli-enc');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('env vars override file config', async () => {
|
|
||||||
process.env.ACTUAL_SERVER_URL = 'http://env';
|
|
||||||
process.env.ACTUAL_PASSWORD = 'envpw';
|
|
||||||
process.env.ACTUAL_ENCRYPTION_PASSWORD = 'env-enc';
|
|
||||||
mockConfigFile({
|
|
||||||
serverUrl: 'http://file',
|
|
||||||
password: 'filepw',
|
|
||||||
encryptionPassword: 'file-enc',
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = await resolveConfig({});
|
|
||||||
|
|
||||||
expect(config.serverUrl).toBe('http://env');
|
|
||||||
expect(config.password).toBe('envpw');
|
|
||||||
expect(config.encryptionPassword).toBe('env-enc');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('file config is used when no CLI opts or env vars', async () => {
|
|
||||||
mockConfigFile({
|
|
||||||
serverUrl: 'http://file',
|
|
||||||
password: 'filepw',
|
|
||||||
syncId: 'budget-1',
|
|
||||||
encryptionPassword: 'file-enc',
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = await resolveConfig({});
|
|
||||||
|
|
||||||
expect(config.serverUrl).toBe('http://file');
|
|
||||||
expect(config.password).toBe('filepw');
|
|
||||||
expect(config.syncId).toBe('budget-1');
|
|
||||||
expect(config.encryptionPassword).toBe('file-enc');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('defaults', () => {
|
|
||||||
it('dataDir defaults to ~/.actual-cli/data', async () => {
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.dataDir).toBe(join(homedir(), '.actual-cli', 'data'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('CLI opt overrides default dataDir', async () => {
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
dataDir: '/custom/dir',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.dataDir).toBe('/custom/dir');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('validation', () => {
|
|
||||||
it('throws when serverUrl is missing', async () => {
|
|
||||||
await expect(resolveConfig({ password: 'pw' })).rejects.toThrow(
|
|
||||||
'Server URL is required',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when neither password nor sessionToken provided', async () => {
|
|
||||||
await expect(resolveConfig({ serverUrl: 'http://test' })).rejects.toThrow(
|
|
||||||
'Authentication required',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts sessionToken without password', async () => {
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
sessionToken: 'tok',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.sessionToken).toBe('tok');
|
|
||||||
expect(config.password).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts password without sessionToken', async () => {
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.password).toBe('pw');
|
|
||||||
expect(config.sessionToken).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cosmiconfig handling', () => {
|
|
||||||
it('handles null result (no config file found)', async () => {
|
|
||||||
mockConfigFile(null);
|
|
||||||
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.serverUrl).toBe('http://test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles isEmpty result', async () => {
|
|
||||||
mockSearch.mockResolvedValue({ config: {}, isEmpty: true });
|
|
||||||
|
|
||||||
const config = await resolveConfig({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.serverUrl).toBe('http://test');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { homedir } from 'os';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { cosmiconfig } from 'cosmiconfig';
|
|
||||||
|
|
||||||
export type CliConfig = {
|
|
||||||
serverUrl: string;
|
|
||||||
password?: string;
|
|
||||||
sessionToken?: string;
|
|
||||||
syncId?: string;
|
|
||||||
dataDir: string;
|
|
||||||
encryptionPassword?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CliGlobalOpts = {
|
|
||||||
serverUrl?: string;
|
|
||||||
password?: string;
|
|
||||||
sessionToken?: string;
|
|
||||||
syncId?: string;
|
|
||||||
dataDir?: string;
|
|
||||||
encryptionPassword?: string;
|
|
||||||
format?: 'json' | 'table' | 'csv';
|
|
||||||
verbose?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConfigFileContent = {
|
|
||||||
serverUrl?: string;
|
|
||||||
password?: string;
|
|
||||||
sessionToken?: string;
|
|
||||||
syncId?: string;
|
|
||||||
dataDir?: string;
|
|
||||||
encryptionPassword?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const configFileKeys: readonly string[] = [
|
|
||||||
'serverUrl',
|
|
||||||
'password',
|
|
||||||
'sessionToken',
|
|
||||||
'syncId',
|
|
||||||
'dataDir',
|
|
||||||
'encryptionPassword',
|
|
||||||
];
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateConfigFileContent(value: unknown): ConfigFileContent {
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
throw new Error(
|
|
||||||
'Invalid config file: expected an object with keys: ' +
|
|
||||||
configFileKeys.join(', '),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(value)) {
|
|
||||||
if (!configFileKeys.includes(key)) {
|
|
||||||
throw new Error(`Invalid config file: unknown key "${key}"`);
|
|
||||||
}
|
|
||||||
if (value[key] !== undefined && typeof value[key] !== 'string') {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid config file: key "${key}" must be a string, got ${typeof value[key]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value as ConfigFileContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadConfigFile(): Promise<ConfigFileContent> {
|
|
||||||
const explorer = cosmiconfig('actual', {
|
|
||||||
searchPlaces: [
|
|
||||||
'package.json',
|
|
||||||
'.actualrc',
|
|
||||||
'.actualrc.json',
|
|
||||||
'.actualrc.yaml',
|
|
||||||
'.actualrc.yml',
|
|
||||||
'actual.config.json',
|
|
||||||
'actual.config.yaml',
|
|
||||||
'actual.config.yml',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const result = await explorer.search();
|
|
||||||
if (result && !result.isEmpty) {
|
|
||||||
return validateConfigFileContent(result.config);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveConfig(
|
|
||||||
cliOpts: CliGlobalOpts,
|
|
||||||
): Promise<CliConfig> {
|
|
||||||
const fileConfig = await loadConfigFile();
|
|
||||||
|
|
||||||
const serverUrl =
|
|
||||||
cliOpts.serverUrl ??
|
|
||||||
process.env.ACTUAL_SERVER_URL ??
|
|
||||||
fileConfig.serverUrl ??
|
|
||||||
'';
|
|
||||||
|
|
||||||
const password =
|
|
||||||
cliOpts.password ?? process.env.ACTUAL_PASSWORD ?? fileConfig.password;
|
|
||||||
|
|
||||||
const sessionToken =
|
|
||||||
cliOpts.sessionToken ??
|
|
||||||
process.env.ACTUAL_SESSION_TOKEN ??
|
|
||||||
fileConfig.sessionToken;
|
|
||||||
|
|
||||||
const syncId =
|
|
||||||
cliOpts.syncId ?? process.env.ACTUAL_SYNC_ID ?? fileConfig.syncId;
|
|
||||||
|
|
||||||
const dataDir =
|
|
||||||
cliOpts.dataDir ??
|
|
||||||
process.env.ACTUAL_DATA_DIR ??
|
|
||||||
fileConfig.dataDir ??
|
|
||||||
join(homedir(), '.actual-cli', 'data');
|
|
||||||
|
|
||||||
const encryptionPassword =
|
|
||||||
cliOpts.encryptionPassword ??
|
|
||||||
process.env.ACTUAL_ENCRYPTION_PASSWORD ??
|
|
||||||
fileConfig.encryptionPassword;
|
|
||||||
|
|
||||||
if (!serverUrl) {
|
|
||||||
throw new Error(
|
|
||||||
'Server URL is required. Set --server-url, ACTUAL_SERVER_URL env var, or serverUrl in config file.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password && !sessionToken) {
|
|
||||||
throw new Error(
|
|
||||||
'Authentication required. Set --password/--session-token, ACTUAL_PASSWORD/ACTUAL_SESSION_TOKEN env var, or password/sessionToken in config file.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverUrl,
|
|
||||||
password,
|
|
||||||
sessionToken,
|
|
||||||
syncId,
|
|
||||||
dataDir,
|
|
||||||
encryptionPassword,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import * as api from '@actual-app/api';
|
|
||||||
|
|
||||||
import { resolveConfig } from './config';
|
|
||||||
import { withConnection } from './connection';
|
|
||||||
|
|
||||||
vi.mock('@actual-app/api', () => ({
|
|
||||||
init: vi.fn().mockResolvedValue(undefined),
|
|
||||||
downloadBudget: vi.fn().mockResolvedValue(undefined),
|
|
||||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./config', () => ({
|
|
||||||
resolveConfig: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function setConfig(overrides: Record<string, unknown> = {}) {
|
|
||||||
vi.mocked(resolveConfig).mockResolvedValue({
|
|
||||||
serverUrl: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
dataDir: '/tmp/data',
|
|
||||||
syncId: 'budget-1',
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('withConnection', () => {
|
|
||||||
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
stderrSpy = vi
|
|
||||||
.spyOn(process.stderr, 'write')
|
|
||||||
.mockImplementation(() => true);
|
|
||||||
setConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
stderrSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.init with password when no sessionToken', async () => {
|
|
||||||
setConfig({ password: 'pw', sessionToken: undefined });
|
|
||||||
|
|
||||||
await withConnection({}, async () => 'ok');
|
|
||||||
|
|
||||||
expect(api.init).toHaveBeenCalledWith({
|
|
||||||
serverURL: 'http://test',
|
|
||||||
password: 'pw',
|
|
||||||
dataDir: '/tmp/data',
|
|
||||||
verbose: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.init with sessionToken when present', async () => {
|
|
||||||
setConfig({ sessionToken: 'tok', password: undefined });
|
|
||||||
|
|
||||||
await withConnection({}, async () => 'ok');
|
|
||||||
|
|
||||||
expect(api.init).toHaveBeenCalledWith({
|
|
||||||
serverURL: 'http://test',
|
|
||||||
sessionToken: 'tok',
|
|
||||||
dataDir: '/tmp/data',
|
|
||||||
verbose: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.downloadBudget when syncId is set', async () => {
|
|
||||||
setConfig({ syncId: 'budget-1' });
|
|
||||||
|
|
||||||
await withConnection({}, async () => 'ok');
|
|
||||||
|
|
||||||
expect(api.downloadBudget).toHaveBeenCalledWith('budget-1', {
|
|
||||||
password: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when loadBudget is true but syncId is not set', async () => {
|
|
||||||
setConfig({ syncId: undefined });
|
|
||||||
|
|
||||||
await expect(withConnection({}, async () => 'ok')).rejects.toThrow(
|
|
||||||
'Sync ID is required',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips budget download when loadBudget is false and syncId is not set', async () => {
|
|
||||||
setConfig({ syncId: undefined });
|
|
||||||
|
|
||||||
await withConnection({}, async () => 'ok', { loadBudget: false });
|
|
||||||
|
|
||||||
expect(api.downloadBudget).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call api.downloadBudget when loadBudget is false', async () => {
|
|
||||||
setConfig({ syncId: 'budget-1' });
|
|
||||||
|
|
||||||
await withConnection({}, async () => 'ok', { loadBudget: false });
|
|
||||||
|
|
||||||
expect(api.downloadBudget).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns callback result', async () => {
|
|
||||||
const result = await withConnection({}, async () => 42);
|
|
||||||
expect(result).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.shutdown in finally block on success', async () => {
|
|
||||||
await withConnection({}, async () => 'ok');
|
|
||||||
expect(api.shutdown).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls api.shutdown in finally block on error', async () => {
|
|
||||||
await expect(
|
|
||||||
withConnection({}, async () => {
|
|
||||||
throw new Error('boom');
|
|
||||||
}),
|
|
||||||
).rejects.toThrow('boom');
|
|
||||||
|
|
||||||
expect(api.shutdown).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not write to stderr by default', async () => {
|
|
||||||
await withConnection({}, async () => 'ok');
|
|
||||||
|
|
||||||
expect(stderrSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('writes info to stderr when verbose', async () => {
|
|
||||||
await withConnection({ verbose: true }, async () => 'ok');
|
|
||||||
|
|
||||||
expect(stderrSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('Connecting to'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { mkdirSync } from 'fs';
|
|
||||||
|
|
||||||
import * as api from '@actual-app/api';
|
|
||||||
|
|
||||||
import { resolveConfig } from './config';
|
|
||||||
import type { CliGlobalOpts } from './config';
|
|
||||||
|
|
||||||
function info(message: string, verbose?: boolean) {
|
|
||||||
if (verbose) {
|
|
||||||
process.stderr.write(message + '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionOptions = {
|
|
||||||
loadBudget?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function withConnection<T>(
|
|
||||||
globalOpts: CliGlobalOpts,
|
|
||||||
fn: () => Promise<T>,
|
|
||||||
options: ConnectionOptions = {},
|
|
||||||
): Promise<T> {
|
|
||||||
const { loadBudget = true } = options;
|
|
||||||
const config = await resolveConfig(globalOpts);
|
|
||||||
|
|
||||||
mkdirSync(config.dataDir, { recursive: true });
|
|
||||||
|
|
||||||
info(`Connecting to ${config.serverUrl}...`, globalOpts.verbose);
|
|
||||||
|
|
||||||
if (config.sessionToken) {
|
|
||||||
await api.init({
|
|
||||||
serverURL: config.serverUrl,
|
|
||||||
dataDir: config.dataDir,
|
|
||||||
sessionToken: config.sessionToken,
|
|
||||||
verbose: globalOpts.verbose,
|
|
||||||
});
|
|
||||||
} else if (config.password) {
|
|
||||||
await api.init({
|
|
||||||
serverURL: config.serverUrl,
|
|
||||||
dataDir: config.dataDir,
|
|
||||||
password: config.password,
|
|
||||||
verbose: globalOpts.verbose,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Authentication required. Provide --password or --session-token, or set ACTUAL_PASSWORD / ACTUAL_SESSION_TOKEN.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (loadBudget && config.syncId) {
|
|
||||||
info(`Downloading budget ${config.syncId}...`, globalOpts.verbose);
|
|
||||||
await api.downloadBudget(config.syncId, {
|
|
||||||
password: config.encryptionPassword,
|
|
||||||
});
|
|
||||||
} else if (loadBudget && !config.syncId) {
|
|
||||||
throw new Error(
|
|
||||||
'Sync ID is required for this command. Set --sync-id or ACTUAL_SYNC_ID.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
await api.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Command, Option } from 'commander';
|
|
||||||
|
|
||||||
import { registerAccountsCommand } from './commands/accounts';
|
|
||||||
import { registerBudgetsCommand } from './commands/budgets';
|
|
||||||
import { registerCategoriesCommand } from './commands/categories';
|
|
||||||
import { registerCategoryGroupsCommand } from './commands/category-groups';
|
|
||||||
import { registerPayeesCommand } from './commands/payees';
|
|
||||||
import { registerQueryCommand } from './commands/query';
|
|
||||||
import { registerRulesCommand } from './commands/rules';
|
|
||||||
import { registerSchedulesCommand } from './commands/schedules';
|
|
||||||
import { registerServerCommand } from './commands/server';
|
|
||||||
import { registerTagsCommand } from './commands/tags';
|
|
||||||
import { registerTransactionsCommand } from './commands/transactions';
|
|
||||||
|
|
||||||
declare const __CLI_VERSION__: string;
|
|
||||||
|
|
||||||
const program = new Command();
|
|
||||||
|
|
||||||
program
|
|
||||||
.name('actual')
|
|
||||||
.description('CLI for Actual Budget')
|
|
||||||
.version(__CLI_VERSION__)
|
|
||||||
.option('--server-url <url>', 'Actual server URL (env: ACTUAL_SERVER_URL)')
|
|
||||||
.option('--password <password>', 'Server password (env: ACTUAL_PASSWORD)')
|
|
||||||
.option(
|
|
||||||
'--session-token <token>',
|
|
||||||
'Session token (env: ACTUAL_SESSION_TOKEN)',
|
|
||||||
)
|
|
||||||
.option('--sync-id <id>', 'Budget sync ID (env: ACTUAL_SYNC_ID)')
|
|
||||||
.option('--data-dir <path>', 'Data directory (env: ACTUAL_DATA_DIR)')
|
|
||||||
.option(
|
|
||||||
'--encryption-password <password>',
|
|
||||||
'E2E encryption password (env: ACTUAL_ENCRYPTION_PASSWORD)',
|
|
||||||
)
|
|
||||||
.addOption(
|
|
||||||
new Option('--format <format>', 'Output format: json, table, csv')
|
|
||||||
.choices(['json', 'table', 'csv'] as const)
|
|
||||||
.default('json'),
|
|
||||||
)
|
|
||||||
.option('--verbose', 'Show informational messages', false);
|
|
||||||
|
|
||||||
registerAccountsCommand(program);
|
|
||||||
registerBudgetsCommand(program);
|
|
||||||
registerCategoriesCommand(program);
|
|
||||||
registerCategoryGroupsCommand(program);
|
|
||||||
registerTransactionsCommand(program);
|
|
||||||
registerPayeesCommand(program);
|
|
||||||
registerTagsCommand(program);
|
|
||||||
registerRulesCommand(program);
|
|
||||||
registerSchedulesCommand(program);
|
|
||||||
registerQueryCommand(program);
|
|
||||||
registerServerCommand(program);
|
|
||||||
|
|
||||||
function normalizeThrownMessage(err: unknown): string {
|
|
||||||
if (err instanceof Error) return err.message;
|
|
||||||
if (typeof err === 'object' && err !== null) {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(err);
|
|
||||||
} catch {
|
|
||||||
return '<non-serializable error>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return String(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
||||||
const message = normalizeThrownMessage(err);
|
|
||||||
process.stderr.write(`Error: ${message}\n`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { readFileSync } from 'fs';
|
|
||||||
|
|
||||||
export function readJsonInput(cmdOpts: {
|
|
||||||
data?: string;
|
|
||||||
file?: string;
|
|
||||||
}): unknown {
|
|
||||||
if (cmdOpts.data && cmdOpts.file) {
|
|
||||||
throw new Error('Cannot use both --data and --file');
|
|
||||||
}
|
|
||||||
if (cmdOpts.data) {
|
|
||||||
return JSON.parse(cmdOpts.data);
|
|
||||||
}
|
|
||||||
if (cmdOpts.file) {
|
|
||||||
const content =
|
|
||||||
cmdOpts.file === '-'
|
|
||||||
? readFileSync(0, 'utf-8')
|
|
||||||
: readFileSync(cmdOpts.file, 'utf-8');
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
throw new Error('Either --data or --file is required');
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { formatOutput, printOutput } from './output';
|
|
||||||
|
|
||||||
describe('formatOutput', () => {
|
|
||||||
describe('json (default)', () => {
|
|
||||||
it('pretty-prints with 2-space indent', () => {
|
|
||||||
const data = { a: 1, b: 'two' };
|
|
||||||
expect(formatOutput(data)).toBe(JSON.stringify(data, null, 2));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is the default format', () => {
|
|
||||||
expect(formatOutput({ x: 1 })).toBe(formatOutput({ x: 1 }, 'json'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles arrays', () => {
|
|
||||||
const data = [1, 2, 3];
|
|
||||||
expect(formatOutput(data, 'json')).toBe('[\n 1,\n 2,\n 3\n]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles null', () => {
|
|
||||||
expect(formatOutput(null, 'json')).toBe('null');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('table', () => {
|
|
||||||
it('renders an object as key-value table', () => {
|
|
||||||
const result = formatOutput({ name: 'Alice', age: 30 }, 'table');
|
|
||||||
expect(result).toContain('name');
|
|
||||||
expect(result).toContain('Alice');
|
|
||||||
expect(result).toContain('age');
|
|
||||||
expect(result).toContain('30');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an array of objects as columnar table', () => {
|
|
||||||
const data = [
|
|
||||||
{ id: 1, name: 'a' },
|
|
||||||
{ id: 2, name: 'b' },
|
|
||||||
];
|
|
||||||
const result = formatOutput(data, 'table');
|
|
||||||
expect(result).toContain('id');
|
|
||||||
expect(result).toContain('name');
|
|
||||||
expect(result).toContain('1');
|
|
||||||
expect(result).toContain('a');
|
|
||||||
expect(result).toContain('2');
|
|
||||||
expect(result).toContain('b');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns "(no results)" for empty array', () => {
|
|
||||||
expect(formatOutput([], 'table')).toBe('(no results)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns String(data) for scalar values', () => {
|
|
||||||
expect(formatOutput(42, 'table')).toBe('42');
|
|
||||||
expect(formatOutput('hello', 'table')).toBe('hello');
|
|
||||||
expect(formatOutput(true, 'table')).toBe('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles null/undefined values in objects', () => {
|
|
||||||
const data = [{ a: null, b: undefined }];
|
|
||||||
const result = formatOutput(data, 'table');
|
|
||||||
expect(result).toContain('a');
|
|
||||||
expect(result).toContain('b');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('csv', () => {
|
|
||||||
it('renders array of objects as header + data rows', () => {
|
|
||||||
const data = [
|
|
||||||
{ id: 1, name: 'Alice' },
|
|
||||||
{ id: 2, name: 'Bob' },
|
|
||||||
];
|
|
||||||
const result = formatOutput(data, 'csv');
|
|
||||||
const lines = result.split('\n');
|
|
||||||
expect(lines[0]).toBe('id,name');
|
|
||||||
expect(lines[1]).toBe('1,Alice');
|
|
||||||
expect(lines[2]).toBe('2,Bob');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders single object as header + single row', () => {
|
|
||||||
const result = formatOutput({ x: 10, y: 20 }, 'csv');
|
|
||||||
const lines = result.split('\n');
|
|
||||||
expect(lines[0]).toBe('x,y');
|
|
||||||
expect(lines[1]).toBe('10,20');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns empty string for empty array', () => {
|
|
||||||
expect(formatOutput([], 'csv')).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns String(data) for scalar values', () => {
|
|
||||||
expect(formatOutput(42, 'csv')).toBe('42');
|
|
||||||
expect(formatOutput('hello', 'csv')).toBe('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('escapes commas by quoting', () => {
|
|
||||||
const data = [{ val: 'a,b' }];
|
|
||||||
expect(formatOutput(data, 'csv')).toBe('val\n"a,b"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('escapes double quotes by doubling them', () => {
|
|
||||||
const data = [{ val: 'say "hi"' }];
|
|
||||||
expect(formatOutput(data, 'csv')).toBe('val\n"say ""hi"""');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('escapes newlines by quoting', () => {
|
|
||||||
const data = [{ val: 'line1\nline2' }];
|
|
||||||
expect(formatOutput(data, 'csv')).toBe('val\n"line1\nline2"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles null/undefined values', () => {
|
|
||||||
const data = [{ a: null, b: undefined }];
|
|
||||||
const result = formatOutput(data, 'csv');
|
|
||||||
const lines = result.split('\n');
|
|
||||||
expect(lines[0]).toBe('a,b');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('printOutput', () => {
|
|
||||||
let writeSpy: ReturnType<typeof vi.spyOn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
writeSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('writes formatted output followed by newline', () => {
|
|
||||||
printOutput({ a: 1 }, 'json');
|
|
||||||
expect(writeSpy).toHaveBeenCalledWith(
|
|
||||||
JSON.stringify({ a: 1 }, null, 2) + '\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to json format', () => {
|
|
||||||
printOutput([1, 2]);
|
|
||||||
expect(writeSpy).toHaveBeenCalledWith(
|
|
||||||
JSON.stringify([1, 2], null, 2) + '\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports table format', () => {
|
|
||||||
printOutput([], 'table');
|
|
||||||
expect(writeSpy).toHaveBeenCalledWith('(no results)\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports csv format', () => {
|
|
||||||
printOutput([], 'csv');
|
|
||||||
expect(writeSpy).toHaveBeenCalledWith('\n');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import Table from 'cli-table3';
|
|
||||||
|
|
||||||
export type OutputFormat = 'json' | 'table' | 'csv';
|
|
||||||
|
|
||||||
export function formatOutput(
|
|
||||||
data: unknown,
|
|
||||||
format: OutputFormat = 'json',
|
|
||||||
): string {
|
|
||||||
switch (format) {
|
|
||||||
case 'json':
|
|
||||||
return JSON.stringify(data, null, 2);
|
|
||||||
case 'table':
|
|
||||||
return formatTable(data);
|
|
||||||
case 'csv':
|
|
||||||
return formatCsv(data);
|
|
||||||
default:
|
|
||||||
return JSON.stringify(data, null, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTable(data: unknown): string {
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
const table = new Table();
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
table.push({ [key]: String(value) });
|
|
||||||
}
|
|
||||||
return table.toString();
|
|
||||||
}
|
|
||||||
return String(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return '(no results)';
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(data[0] as Record<string, unknown>);
|
|
||||||
const table = new Table({ head: keys });
|
|
||||||
|
|
||||||
for (const row of data) {
|
|
||||||
const r = row as Record<string, unknown>;
|
|
||||||
table.push(keys.map(k => String(r[k] ?? '')));
|
|
||||||
}
|
|
||||||
|
|
||||||
return table.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCsv(data: unknown): string {
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
const entries = Object.entries(data);
|
|
||||||
const header = entries.map(([k]) => escapeCsv(k)).join(',');
|
|
||||||
const values = entries.map(([, v]) => escapeCsv(String(v))).join(',');
|
|
||||||
return header + '\n' + values;
|
|
||||||
}
|
|
||||||
return String(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(data[0] as Record<string, unknown>);
|
|
||||||
const header = keys.map(k => escapeCsv(k)).join(',');
|
|
||||||
const rows = data.map(row => {
|
|
||||||
const r = row as Record<string, unknown>;
|
|
||||||
return keys.map(k => escapeCsv(String(r[k] ?? ''))).join(',');
|
|
||||||
});
|
|
||||||
|
|
||||||
return [header, ...rows].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeCsv(value: string): string {
|
|
||||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
||||||
return '"' + value.replace(/"/g, '""') + '"';
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function printOutput(data: unknown, format: OutputFormat = 'json') {
|
|
||||||
process.stdout.write(formatOutput(data, format) + '\n');
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { parseBoolFlag, parseIntFlag } from './utils';
|
|
||||||
|
|
||||||
describe('parseBoolFlag', () => {
|
|
||||||
it('parses "true"', () => {
|
|
||||||
expect(parseBoolFlag('true', '--flag')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses "false"', () => {
|
|
||||||
expect(parseBoolFlag('false', '--flag')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects other strings', () => {
|
|
||||||
expect(() => parseBoolFlag('yes', '--flag')).toThrow(
|
|
||||||
'Invalid --flag: "yes". Expected "true" or "false".',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes the flag name in the error message', () => {
|
|
||||||
expect(() => parseBoolFlag('1', '--offbudget')).toThrow(
|
|
||||||
'Invalid --offbudget',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseIntFlag', () => {
|
|
||||||
it('parses a valid integer string', () => {
|
|
||||||
expect(parseIntFlag('42', '--balance')).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses zero', () => {
|
|
||||||
expect(parseIntFlag('0', '--balance')).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses negative integers', () => {
|
|
||||||
expect(parseIntFlag('-10', '--balance')).toBe(-10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects decimal values', () => {
|
|
||||||
expect(() => parseIntFlag('3.5', '--balance')).toThrow(
|
|
||||||
'Invalid --balance: "3.5". Expected an integer.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-numeric strings', () => {
|
|
||||||
expect(() => parseIntFlag('abc', '--balance')).toThrow(
|
|
||||||
'Invalid --balance: "abc". Expected an integer.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects partially numeric strings', () => {
|
|
||||||
expect(() => parseIntFlag('3abc', '--balance')).toThrow(
|
|
||||||
'Invalid --balance: "3abc". Expected an integer.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects empty string', () => {
|
|
||||||
expect(() => parseIntFlag('', '--balance')).toThrow(
|
|
||||||
'Invalid --balance: "". Expected an integer.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes the flag name in the error message', () => {
|
|
||||||
expect(() => parseIntFlag('x', '--amount')).toThrow('Invalid --amount');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
export function parseBoolFlag(value: string, flagName: string): boolean {
|
|
||||||
if (value !== 'true' && value !== 'false') {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid ${flagName}: "${value}". Expected "true" or "false".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIntFlag(value: string, flagName: string): number {
|
|
||||||
const parsed = value.trim() === '' ? NaN : Number(value);
|
|
||||||
if (!Number.isInteger(parsed)) {
|
|
||||||
throw new Error(`Invalid ${flagName}: "${value}". Expected an integer.`);
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"lib": ["ES2021"],
|
|
||||||
"types": ["vitest/globals", "node"],
|
|
||||||
"noEmit": false,
|
|
||||||
"strict": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
|
||||||
},
|
|
||||||
"references": [{ "path": "../api" }],
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "coverage"]
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
|
|
||||||
const pkg = JSON.parse(
|
|
||||||
fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
define: {
|
|
||||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
|
||||||
},
|
|
||||||
ssr: { noExternal: true, external: ['@actual-app/api'] },
|
|
||||||
build: {
|
|
||||||
ssr: true,
|
|
||||||
target: 'node22',
|
|
||||||
outDir: path.resolve(__dirname, 'dist'),
|
|
||||||
emptyOutDir: true,
|
|
||||||
lib: {
|
|
||||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
|
||||||
formats: ['es'],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
entryFileNames: 'cli.js',
|
|
||||||
banner: chunk => (chunk.isEntry ? '#!/usr/bin/env node' : ''),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [visualizer({ template: 'raw-data', filename: 'dist/stats.json' })],
|
|
||||||
test: {
|
|
||||||
globals: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -5,6 +5,7 @@ import type { Preview } from '@storybook/react-vite';
|
|||||||
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
|
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
|
||||||
// TODO: this needs refactoring
|
// TODO: this needs refactoring
|
||||||
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
|
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
|
||||||
|
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
|
||||||
import * as lightTheme from '../../desktop-client/src/style/themes/light';
|
import * as lightTheme from '../../desktop-client/src/style/themes/light';
|
||||||
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
|
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const THEMES = {
|
|||||||
light: lightTheme,
|
light: lightTheme,
|
||||||
dark: darkTheme,
|
dark: darkTheme,
|
||||||
midnight: midnightTheme,
|
midnight: midnightTheme,
|
||||||
|
development: developmentTheme,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ThemeName = keyof typeof THEMES;
|
type ThemeName = keyof typeof THEMES;
|
||||||
@@ -62,6 +64,7 @@ const preview: Preview = {
|
|||||||
{ value: 'light', title: 'Light' },
|
{ value: 'light', title: 'Light' },
|
||||||
{ value: 'dark', title: 'Dark' },
|
{ value: 'dark', title: 'Dark' },
|
||||||
{ value: 'midnight', title: 'Midnight' },
|
{ value: 'midnight', title: 'Midnight' },
|
||||||
|
{ value: 'development', title: 'Development' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,18 +48,18 @@
|
|||||||
"usehooks-ts": "^3.1.1"
|
"usehooks-ts": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^5.0.1",
|
"@chromatic-com/storybook": "^5.0.0",
|
||||||
"@storybook/addon-a11y": "^10.2.16",
|
"@storybook/addon-a11y": "^10.2.7",
|
||||||
"@storybook/addon-docs": "^10.2.16",
|
"@storybook/addon-docs": "^10.2.7",
|
||||||
"@storybook/react-vite": "^10.2.16",
|
"@storybook/react-vite": "^10.2.7",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.5",
|
||||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||||
"@vitejs/plugin-react": "^6.0.0",
|
"@vitejs/plugin-react": "^6.0.0",
|
||||||
"eslint-plugin-storybook": "^10.2.16",
|
"eslint-plugin-storybook": "^10.2.7",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"storybook": "^10.2.16",
|
"storybook": "^10.2.7",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vitest": "^4.1.0"
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
|
|
||||||
import type { CSSProperties } from './styles';
|
import type { CSSProperties } from './styles';
|
||||||
|
|
||||||
type BlockProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
|
type BlockProps = HTMLProps<HTMLDivElement> & {
|
||||||
innerRef?: Ref<HTMLDivElement>;
|
innerRef?: Ref<HTMLDivElement>;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { css } from '@emotion/css';
|
|||||||
|
|
||||||
import type { CSSProperties } from './styles';
|
import type { CSSProperties } from './styles';
|
||||||
|
|
||||||
type ParagraphProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
|
type ParagraphProps = HTMLProps<HTMLDivElement> & {
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
|
|
||||||
import type { CSSProperties } from './styles';
|
import type { CSSProperties } from './styles';
|
||||||
|
|
||||||
type TextProps = Omit<HTMLProps<HTMLSpanElement>, 'style'> & {
|
type TextProps = HTMLProps<HTMLSpanElement> & {
|
||||||
innerRef?: Ref<HTMLSpanElement>;
|
innerRef?: Ref<HTMLSpanElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
|
|
||||||
import type { CSSProperties } from './styles';
|
import type { CSSProperties } from './styles';
|
||||||
|
|
||||||
type ViewProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
|
type ViewProps = HTMLProps<HTMLDivElement> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
nativeStyle?: CSSProperties;
|
nativeStyle?: CSSProperties;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -5,7 +5,6 @@ export class BudgetMenuModal {
|
|||||||
readonly locator: Locator;
|
readonly locator: Locator;
|
||||||
readonly heading: Locator;
|
readonly heading: Locator;
|
||||||
readonly budgetAmountInput: Locator;
|
readonly budgetAmountInput: Locator;
|
||||||
readonly actionsButton: Locator;
|
|
||||||
readonly copyLastMonthBudgetButton: Locator;
|
readonly copyLastMonthBudgetButton: Locator;
|
||||||
readonly setTo3MonthAverageButton: Locator;
|
readonly setTo3MonthAverageButton: Locator;
|
||||||
readonly setTo6MonthAverageButton: Locator;
|
readonly setTo6MonthAverageButton: Locator;
|
||||||
@@ -18,9 +17,6 @@ export class BudgetMenuModal {
|
|||||||
|
|
||||||
this.heading = locator.getByRole('heading');
|
this.heading = locator.getByRole('heading');
|
||||||
this.budgetAmountInput = locator.getByTestId('amount-input');
|
this.budgetAmountInput = locator.getByTestId('amount-input');
|
||||||
this.actionsButton = locator.getByRole('button', {
|
|
||||||
name: 'Actions',
|
|
||||||
});
|
|
||||||
this.copyLastMonthBudgetButton = locator.getByRole('button', {
|
this.copyLastMonthBudgetButton = locator.getByRole('button', {
|
||||||
name: "Copy last month's budget",
|
name: "Copy last month's budget",
|
||||||
});
|
});
|
||||||
@@ -42,10 +38,6 @@ export class BudgetMenuModal {
|
|||||||
await this.heading.getByRole('button', { name: 'Close' }).click();
|
await this.heading.getByRole('button', { name: 'Close' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async showActions() {
|
|
||||||
await this.actionsButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setBudgetAmount(newAmount: string) {
|
async setBudgetAmount(newAmount: string) {
|
||||||
await this.budgetAmountInput.fill(newAmount);
|
await this.budgetAmountInput.fill(newAmount);
|
||||||
await this.budgetAmountInput.blur();
|
await this.budgetAmountInput.blur();
|
||||||
@@ -53,27 +45,22 @@ export class BudgetMenuModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async copyLastMonthBudget() {
|
async copyLastMonthBudget() {
|
||||||
await this.showActions();
|
|
||||||
await this.copyLastMonthBudgetButton.click();
|
await this.copyLastMonthBudgetButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTo3MonthAverage() {
|
async setTo3MonthAverage() {
|
||||||
await this.showActions();
|
|
||||||
await this.setTo3MonthAverageButton.click();
|
await this.setTo3MonthAverageButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTo6MonthAverage() {
|
async setTo6MonthAverage() {
|
||||||
await this.showActions();
|
|
||||||
await this.setTo6MonthAverageButton.click();
|
await this.setTo6MonthAverageButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setToYearlyAverage() {
|
async setToYearlyAverage() {
|
||||||
await this.showActions();
|
|
||||||
await this.setToYearlyAverageButton.click();
|
await this.setToYearlyAverageButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyBudgetTemplate() {
|
async applyBudgetTemplate() {
|
||||||
await this.showActions();
|
|
||||||
await this.applyBudgetTemplateButton.click();
|
await this.applyBudgetTemplateButton.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const NO_PAYEES_FOUND_TEXT = 'No payees found.';
|
||||||
|
|
||||||
export class MobilePayeesPage {
|
export class MobilePayeesPage {
|
||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
readonly searchBox: Locator;
|
readonly searchBox: Locator;
|
||||||
readonly payeesList: Locator;
|
readonly payeesList: Locator;
|
||||||
readonly emptyMessage: Locator;
|
readonly noPayeesFoundText: Locator;
|
||||||
readonly loadingIndicator: Locator;
|
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.searchBox = page.getByPlaceholder('Filter payees…');
|
this.searchBox = page.getByPlaceholder('Filter payees…');
|
||||||
this.payeesList = page.getByRole('grid', { name: 'Payees' });
|
this.payeesList = page.getByRole('grid', { name: 'Payees' });
|
||||||
this.emptyMessage = page.getByText('No payees found.');
|
this.noPayeesFoundText = this.payeesList.getByText(NO_PAYEES_FOUND_TEXT);
|
||||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitFor(options?: {
|
async waitFor(options?: {
|
||||||
@@ -47,7 +47,11 @@ export class MobilePayeesPage {
|
|||||||
* Get all visible payee items
|
* Get all visible payee items
|
||||||
*/
|
*/
|
||||||
getAllPayees() {
|
getAllPayees() {
|
||||||
return this.payeesList.getByRole('gridcell');
|
// `GridList.renderEmptyState` still renders a row with "No payees found" text
|
||||||
|
// when no payees are present, so we need to filter that out to get the actual payee items.
|
||||||
|
return this.payeesList
|
||||||
|
.getByRole('row')
|
||||||
|
.filter({ hasNotText: NO_PAYEES_FOUND_TEXT });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,11 +69,4 @@ export class MobilePayeesPage {
|
|||||||
const payees = this.getAllPayees();
|
const payees = this.getAllPayees();
|
||||||
return await payees.count();
|
return await payees.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for loading to complete
|
|
||||||
*/
|
|
||||||
async waitForLoadingToComplete(timeout: number = 10000) {
|
|
||||||
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const NO_RULES_FOUND_TEXT =
|
||||||
|
'No rules found. Create your first rule to get started!';
|
||||||
|
|
||||||
export class MobileRulesPage {
|
export class MobileRulesPage {
|
||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
readonly searchBox: Locator;
|
readonly searchBox: Locator;
|
||||||
readonly addButton: Locator;
|
readonly addButton: Locator;
|
||||||
readonly rulesList: Locator;
|
readonly rulesList: Locator;
|
||||||
readonly emptyMessage: Locator;
|
readonly noRulesFoundText: Locator;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.searchBox = page.getByPlaceholder('Filter rules…');
|
this.searchBox = page.getByPlaceholder('Filter rules…');
|
||||||
this.addButton = page.getByRole('button', { name: 'Add new rule' });
|
this.addButton = page.getByRole('button', { name: 'Add new rule' });
|
||||||
this.rulesList = page.getByRole('main');
|
this.rulesList = page.getByRole('grid', { name: 'Rules' });
|
||||||
this.emptyMessage = page.getByText('No rules found');
|
this.noRulesFoundText = this.rulesList.getByText(NO_RULES_FOUND_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitFor(options?: {
|
async waitFor(options?: {
|
||||||
@@ -47,7 +50,11 @@ export class MobileRulesPage {
|
|||||||
* Get all visible rule items
|
* Get all visible rule items
|
||||||
*/
|
*/
|
||||||
getAllRules() {
|
getAllRules() {
|
||||||
return this.page.getByRole('grid', { name: 'Rules' }).getByRole('row');
|
// `GridList.renderEmptyState` still renders a row with "No rules found" text
|
||||||
|
// when no rules are present, so we need to filter that out to get the actual rule items.
|
||||||
|
return this.rulesList
|
||||||
|
.getByRole('row')
|
||||||
|
.filter({ hasNotText: NO_RULES_FOUND_TEXT });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const NO_SCHEDULES_FOUND_TEXT =
|
||||||
|
'No schedules found. Create your first schedule to get started!';
|
||||||
|
|
||||||
export class MobileSchedulesPage {
|
export class MobileSchedulesPage {
|
||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
readonly searchBox: Locator;
|
readonly searchBox: Locator;
|
||||||
readonly addButton: Locator;
|
readonly addButton: Locator;
|
||||||
readonly schedulesList: Locator;
|
readonly schedulesList: Locator;
|
||||||
readonly emptyMessage: Locator;
|
readonly noSchedulesFoundText: Locator;
|
||||||
readonly loadingIndicator: Locator;
|
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.searchBox = page.getByPlaceholder('Filter schedules…');
|
this.searchBox = page.getByPlaceholder('Filter schedules…');
|
||||||
this.addButton = page.getByRole('button', { name: 'Add new schedule' });
|
this.addButton = page.getByRole('button', { name: 'Add new schedule' });
|
||||||
this.schedulesList = page.getByRole('grid', { name: 'Schedules' });
|
this.schedulesList = page.getByRole('grid', { name: 'Schedules' });
|
||||||
this.emptyMessage = page.getByText(
|
this.noSchedulesFoundText = this.schedulesList.getByText(
|
||||||
'No schedules found. Create your first schedule to get started!',
|
NO_SCHEDULES_FOUND_TEXT,
|
||||||
);
|
);
|
||||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitFor(options?: {
|
async waitFor(options?: {
|
||||||
@@ -51,7 +52,11 @@ export class MobileSchedulesPage {
|
|||||||
* Get all visible schedule items
|
* Get all visible schedule items
|
||||||
*/
|
*/
|
||||||
getAllSchedules() {
|
getAllSchedules() {
|
||||||
return this.schedulesList.getByRole('gridcell');
|
// `GridList.renderEmptyState` still renders a row with "No schedules found" text
|
||||||
|
// when no schedules are present, so we need to filter that out to get the actual schedule items.
|
||||||
|
return this.schedulesList
|
||||||
|
.getByRole('row')
|
||||||
|
.filter({ hasNotText: NO_SCHEDULES_FOUND_TEXT });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,11 +81,4 @@ export class MobileSchedulesPage {
|
|||||||
const schedules = this.getAllSchedules();
|
const schedules = this.getAllSchedules();
|
||||||
return await schedules.count();
|
return await schedules.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for loading to complete
|
|
||||||
*/
|
|
||||||
async waitForLoadingToComplete(timeout: number = 10000) {
|
|
||||||
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ test.describe('Mobile Payees', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('checks the page visuals', async () => {
|
test('checks the page visuals', async () => {
|
||||||
await payeesPage.waitForLoadingToComplete();
|
|
||||||
|
|
||||||
// Check that the header is present
|
// Check that the header is present
|
||||||
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
|
||||||
|
|
||||||
@@ -63,8 +61,6 @@ test.describe('Mobile Payees', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('clicking on a payee opens payee edit page', async () => {
|
test('clicking on a payee opens payee edit page', async () => {
|
||||||
await payeesPage.waitForLoadingToComplete();
|
|
||||||
|
|
||||||
const payeeCount = await payeesPage.getPayeeCount();
|
const payeeCount = await payeesPage.getPayeeCount();
|
||||||
expect(payeeCount).toBeGreaterThan(0);
|
expect(payeeCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
@@ -89,8 +85,7 @@ test.describe('Mobile Payees', () => {
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// Check that empty message is shown
|
// Check that empty message is shown
|
||||||
const emptyMessage = page.getByText('No payees found.');
|
await expect(payeesPage.noPayeesFoundText).toBeVisible();
|
||||||
await expect(emptyMessage).toBeVisible();
|
|
||||||
|
|
||||||
// Check that no payee items are visible
|
// Check that no payee items are visible
|
||||||
const payees = payeesPage.getAllPayees();
|
const payees = payeesPage.getAllPayees();
|
||||||
@@ -99,8 +94,6 @@ test.describe('Mobile Payees', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('search functionality works correctly', async () => {
|
test('search functionality works correctly', async () => {
|
||||||
await payeesPage.waitForLoadingToComplete();
|
|
||||||
|
|
||||||
// Test searching for a specific payee
|
// Test searching for a specific payee
|
||||||
await payeesPage.searchFor('Fast Internet');
|
await payeesPage.searchFor('Fast Internet');
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |