Compare commits

..

3 Commits

Author SHA1 Message Date
Claude
22a5bff977 [AI] Add Capacitor iOS app wrapper for App Store publishing
Wraps the existing PWA mobile web app in a Capacitor iOS shell for
publishing to the iOS App Store. Uses the existing 54+ mobile-optimized
React components and local-first SQLite storage (via absurd-sql/IndexedDB).

Key additions:
- packages/mobile/ - New Capacitor workspace with iOS platform
- Custom ActualBridgeViewController for WKWebView configuration
- CrossOriginIsolationPlugin for SharedArrayBuffer support
- Build scripts: yarn build:mobile, yarn mobile:open, yarn mobile:run:ios
- iOS deployment target set to 16.0 (required for SAB in WKWebView)
- App Transport Security configured for local networking

https://claude.ai/code/session_01GAVpnQ21dPw6KJVf5c4f5D
2026-03-21 10:59:02 +00:00
Matiss Janis Aboltins
23adf06cb0 Add post-merge hook to auto-install dependencies (#7248)
* [AI] Add post-merge hook to auto-install when yarn.lock changes

Mirrors the existing post-checkout hook behavior: after a git pull or
merge, if yarn.lock changed between ORIG_HEAD and HEAD, automatically
runs yarn install to keep dependencies in sync.

https://claude.ai/code/session_01JHoMhGANLTc1q67s1dUHrt

* Add release notes for PR #7248

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-20 22:59:05 +00:00
Matiss Janis Aboltins
a8a2d23e63 [AI] Improve CLI query command with new flags, subcommands, and docs (#7240)
* [AI] Improve CLI query command with new flags, subcommands, and docs

Add --last, --count, --where, --offset, --group-by flags and field:desc
order-by syntax to make the query command more usable for both humans
and AI agents. Add query tables/fields subcommands for schema discovery.
Expand CLI and ActualQL documentation with comprehensive examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix parseOrderBy empty field validation and misleading aggregate docs

Validate that order-by field names are non-empty (e.g. ":desc" now throws
a clear error). Move AVAILABLE_TABLES before first use. Update group-by
examples in CLI help text and docs to use --file with proper aggregate
JSON instead of misleading --select with plain field names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-20 19:01:20 +00:00
33 changed files with 1969 additions and 115 deletions

View File

@@ -50,8 +50,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: web
ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for ${{github.base_ref}} API build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-api-build
@@ -59,8 +57,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for ${{github.base_ref}} CLI build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-cli-build
@@ -68,8 +64,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
@@ -78,8 +72,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: web
ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for API PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-api-build
@@ -87,8 +79,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for CLI PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-cli-build
@@ -96,32 +86,12 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- 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' ||
steps.master-web-build.outputs.conclusion == 'failure' ||
steps.master-api-build.outputs.conclusion == 'failure' ||
steps.master-cli-build.outputs.conclusion == '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'
run: |
echo "Build failed on PR branch or ${{github.base_ref}}"
exit 1
- name: Warn on incomplete builds
if: |
steps.wait-for-web-build.outputs.conclusion != 'success' ||
steps.wait-for-api-build.outputs.conclusion != 'success' ||
steps.wait-for-cli-build.outputs.conclusion != 'success' ||
steps.master-web-build.outputs.conclusion != 'success' ||
steps.master-api-build.outputs.conclusion != 'success' ||
steps.master-cli-build.outputs.conclusion != 'success'
run: |
echo "::warning::Some builds did not complete successfully. Bundle stats may be incomplete."
echo "Base branch - web: ${{ steps.master-web-build.outputs.conclusion }}, api: ${{ steps.master-api-build.outputs.conclusion }}, cli: ${{ steps.master-cli-build.outputs.conclusion }}"
echo "PR - web: ${{ steps.wait-for-web-build.outputs.conclusion }}, api: ${{ steps.wait-for-api-build.outputs.conclusion }}, cli: ${{ steps.wait-for-cli-build.outputs.conclusion }}"
- name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
@@ -132,7 +102,6 @@ jobs:
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: base
if_no_artifact_found: warn
- name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-api-build
@@ -142,46 +111,41 @@ jobs:
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats
path: base
if_no_artifact_found: warn
- name: Download build stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
commit: ${{github.event.pull_request.head.sha}}
pr: ${{github.event.pull_request.number}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: head
allow_forks: true
if_no_artifact_found: warn
- name: Download API stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
commit: ${{github.event.pull_request.head.sha}}
pr: ${{github.event.pull_request.number}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats
path: head
allow_forks: true
if_no_artifact_found: warn
- name: Download CLI build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
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
if_no_artifact_found: warn
- name: Download CLI stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
with:
commit: ${{github.event.pull_request.head.sha}}
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
if_no_artifact_found: warn
- name: Strip content hashes from stats files
run: |
if [ -f ./head/web-stats.json ]; then
@@ -198,31 +162,19 @@ jobs:
fi
done
- name: Generate combined bundle stats comment
if: ${{ !cancelled() }}
id: generate-comment
run: |
ARGS=""
for bundle in "desktop-client=web-stats.json" "loot-core=loot-core-stats.json" "api=api-stats.json" "cli=cli-stats.json"; do
NAME="${bundle%%=*}"
FILE="${bundle#*=}"
if [ -f "./base/$FILE" ] && [ -f "./head/$FILE" ]; then
ARGS="$ARGS --base $NAME=./base/$FILE --head $NAME=./head/$FILE"
else
echo "::warning::Skipping $NAME: base or head stats file missing"
fi
done
if [ -z "$ARGS" ]; then
echo "::warning::No stats files available, skipping comment generation"
echo "has_comment=false" >> "$GITHUB_OUTPUT"
exit 0
fi
node packages/ci-actions/bin/bundle-stats-comment.mjs \
$ARGS \
--base desktop-client=./base/web-stats.json \
--base loot-core=./base/loot-core-stats.json \
--base api=./base/api-stats.json \
--base cli=./base/cli-stats.json \
--head desktop-client=./head/web-stats.json \
--head loot-core=./head/loot-core-stats.json \
--head api=./head/api-stats.json \
--head cli=./head/cli-stats.json \
--identifier combined \
--format pr-body > bundle-stats-comment.md
echo "has_comment=true" >> "$GITHUB_OUTPUT"
- name: Post combined bundle stats comment
if: ${{ !cancelled() && steps.generate-comment.outputs.has_comment == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}

7
.husky/post-merge Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Run yarn install after pulling/merging (if yarn.lock changed)
if git diff --name-only ORIG_HEAD HEAD | grep -q "^yarn.lock$"; then
echo "yarn.lock changed — running yarn install..."
yarn install
fi

23
bin/package-mobile Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash -e
ROOT=`dirname $0`
cd "$ROOT/.."
echo "Building web assets for mobile..."
# Build the browser version (same assets used by the Capacitor wrapper)
./bin/package-browser
echo "Syncing Capacitor iOS project..."
cd packages/mobile
npx cap sync ios
echo ""
echo "iOS project ready at: packages/mobile/ios/App/App.xcworkspace"
echo ""
echo "To build for the App Store:"
echo " 1. Open the project: yarn mobile:open"
echo " 2. Select your signing team in Xcode"
echo " 3. Archive: Product > Archive"
echo " 4. Distribute via App Store Connect"

View File

@@ -39,6 +39,9 @@
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:mobile": "yarn build:browser && yarn workspace @actual-app/mobile sync",
"mobile:open": "yarn workspace @actual-app/mobile open",
"mobile:run:ios": "yarn workspace @actual-app/mobile run:ios",
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:cli": "yarn build --scope=@actual-app/cli",

View File

@@ -5,7 +5,7 @@
* Heavily inspired by https://github.com/twk3/rollup-size-compare-action (MIT).
*/
import { access, readFile } from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
@@ -179,19 +179,8 @@ function parseArgs(argv) {
}
async function loadStats(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
// Check if the file exists before trying to read it
try {
await access(absolutePath);
} catch {
console.error(
`[bundle-stats] Stats file not found: "${filePath}" — skipping`,
);
return null;
}
try {
const absolutePath = path.resolve(process.cwd(), filePath);
const fileContents = await readFile(absolutePath, 'utf8');
const parsed = JSON.parse(fileContents);
@@ -207,7 +196,7 @@ async function loadStats(filePath) {
? error.message
: 'Unknown error while parsing stats file';
console.error(`[bundle-stats] Failed to parse "${filePath}": ${message}`);
return null;
throw new Error(`Failed to load stats file "${filePath}": ${message}`);
}
}
@@ -698,13 +687,6 @@ async function main() {
);
const headStats = await loadStats(section.headPath);
if (!baseStats || !headStats) {
console.error(
`[bundle-stats] Skipping section "${section.name}": missing ${!baseStats ? 'base' : 'head'} stats`,
);
continue;
}
const statsDiff = getStatsDiff(baseStats, headStats);
const chunkDiff = getChunkModuleDiff(baseStats, headStats);

View File

@@ -0,0 +1,346 @@
import * as api from '@actual-app/api';
import { Command } from 'commander';
import { printOutput } from '../output';
import { parseOrderBy, registerQueryCommand } from './query';
vi.mock('@actual-app/api', () => {
const queryObj = {
select: vi.fn().mockReturnThis(),
filter: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
offset: vi.fn().mockReturnThis(),
groupBy: vi.fn().mockReturnThis(),
calculate: vi.fn().mockReturnThis(),
};
return {
q: vi.fn().mockReturnValue(queryObj),
aqlQuery: vi.fn().mockResolvedValue({ data: [] }),
};
});
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();
registerQueryCommand(program);
return program;
}
async function run(args: string[]) {
const program = createProgram();
await program.parseAsync(['node', 'test', ...args]);
}
function getQueryObj() {
return vi.mocked(api.q).mock.results[0]?.value;
}
describe('parseOrderBy', () => {
it('parses plain field names', () => {
expect(parseOrderBy('date')).toEqual(['date']);
});
it('parses field:desc', () => {
expect(parseOrderBy('date:desc')).toEqual([{ date: 'desc' }]);
});
it('parses field:asc', () => {
expect(parseOrderBy('amount:asc')).toEqual([{ amount: 'asc' }]);
});
it('parses multiple mixed fields', () => {
expect(parseOrderBy('date:desc,amount:asc,id')).toEqual([
{ date: 'desc' },
{ amount: 'asc' },
'id',
]);
});
it('throws on invalid direction', () => {
expect(() => parseOrderBy('date:backwards')).toThrow(
'Invalid order direction "backwards"',
);
});
it('throws on empty field', () => {
expect(() => parseOrderBy('date,,amount')).toThrow('empty field');
});
});
describe('query 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('run', () => {
it('builds a basic query from flags', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--select',
'date,amount',
'--limit',
'5',
]);
expect(api.q).toHaveBeenCalledWith('transactions');
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith(['date', 'amount']);
expect(qObj.limit).toHaveBeenCalledWith(5);
});
it('rejects unknown table name', async () => {
await expect(
run(['query', 'run', '--table', 'nonexistent']),
).rejects.toThrow('Unknown table "nonexistent"');
});
it('parses order-by with desc direction', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--order-by',
'date:desc,amount:asc',
]);
const qObj = getQueryObj();
expect(qObj.orderBy).toHaveBeenCalledWith([
{ date: 'desc' },
{ amount: 'asc' },
]);
});
it('passes --filter as JSON', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--filter',
'{"amount":{"$lt":0}}',
]);
const qObj = getQueryObj();
expect(qObj.filter).toHaveBeenCalledWith({ amount: { $lt: 0 } });
});
});
describe('--last flag', () => {
it('sets default table, select, orderBy, and limit', async () => {
await run(['query', 'run', '--last', '10']);
expect(api.q).toHaveBeenCalledWith('transactions');
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith([
'date',
'account.name',
'payee.name',
'category.name',
'amount',
'notes',
]);
expect(qObj.orderBy).toHaveBeenCalledWith([{ date: 'desc' }]);
expect(qObj.limit).toHaveBeenCalledWith(10);
});
it('allows explicit --select override', async () => {
await run(['query', 'run', '--last', '5', '--select', 'date,amount']);
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith(['date', 'amount']);
});
it('allows explicit --order-by override', async () => {
await run(['query', 'run', '--last', '5', '--order-by', 'amount:asc']);
const qObj = getQueryObj();
expect(qObj.orderBy).toHaveBeenCalledWith([{ amount: 'asc' }]);
});
it('allows --table transactions explicitly', async () => {
await run(['query', 'run', '--last', '5', '--table', 'transactions']);
expect(api.q).toHaveBeenCalledWith('transactions');
});
it('errors if --table is not transactions', async () => {
await expect(
run(['query', 'run', '--last', '5', '--table', 'accounts']),
).rejects.toThrow('--last implies --table transactions');
});
it('errors if --limit is also set', async () => {
await expect(
run(['query', 'run', '--last', '5', '--limit', '10']),
).rejects.toThrow('--last and --limit are mutually exclusive');
});
});
describe('--count flag', () => {
it('uses calculate with $count', async () => {
vi.mocked(api.aqlQuery).mockResolvedValueOnce({ data: 42 });
await run(['query', 'run', '--table', 'transactions', '--count']);
const qObj = getQueryObj();
expect(qObj.calculate).toHaveBeenCalledWith({ $count: '*' });
expect(printOutput).toHaveBeenCalledWith({ count: 42 }, undefined);
});
it('errors if --select is also set', async () => {
await expect(
run([
'query',
'run',
'--table',
'transactions',
'--count',
'--select',
'date',
]),
).rejects.toThrow('--count and --select are mutually exclusive');
});
});
describe('--where alias', () => {
it('works the same as --filter', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--where',
'{"amount":{"$gt":0}}',
]);
const qObj = getQueryObj();
expect(qObj.filter).toHaveBeenCalledWith({ amount: { $gt: 0 } });
});
it('errors if both --where and --filter are provided', async () => {
await expect(
run([
'query',
'run',
'--table',
'transactions',
'--where',
'{}',
'--filter',
'{}',
]),
).rejects.toThrow('--where and --filter are mutually exclusive');
});
});
describe('--offset flag', () => {
it('passes offset through to query', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--offset',
'20',
'--limit',
'10',
]);
const qObj = getQueryObj();
expect(qObj.offset).toHaveBeenCalledWith(20);
expect(qObj.limit).toHaveBeenCalledWith(10);
});
});
describe('--group-by flag', () => {
it('passes group-by through to query', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--group-by',
'category.name',
'--select',
'category.name,amount',
]);
const qObj = getQueryObj();
expect(qObj.groupBy).toHaveBeenCalledWith(['category.name']);
});
});
describe('tables subcommand', () => {
it('lists available tables', async () => {
await run(['query', 'tables']);
expect(printOutput).toHaveBeenCalledWith(
expect.arrayContaining([
{ name: 'transactions' },
{ name: 'accounts' },
{ name: 'categories' },
{ name: 'payees' },
]),
undefined,
);
});
});
describe('fields subcommand', () => {
it('lists fields for a known table', async () => {
await run(['query', 'fields', 'accounts']);
const output = vi.mocked(printOutput).mock.calls[0][0] as Array<{
name: string;
type: string;
}>;
expect(output).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'id', type: 'id' }),
expect.objectContaining({ name: 'name', type: 'string' }),
]),
);
});
it('errors on unknown table', async () => {
await expect(run(['query', 'fields', 'unknown'])).rejects.toThrow(
'Unknown table "unknown"',
);
});
});
});

View File

@@ -10,6 +10,115 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* Parse order-by strings like "date:desc,amount:asc,id" into
* AQL orderBy format: [{ date: 'desc' }, { amount: 'asc' }, 'id']
*/
export function parseOrderBy(
input: string,
): Array<string | Record<string, string>> {
return input.split(',').map(part => {
const trimmed = part.trim();
if (!trimmed) {
throw new Error('--order-by contains an empty field');
}
const colonIndex = trimmed.indexOf(':');
if (colonIndex === -1) {
return trimmed;
}
const field = trimmed.slice(0, colonIndex).trim();
if (!field) {
throw new Error(
`Invalid order field in "${trimmed}". Field name cannot be empty.`,
);
}
const direction = trimmed.slice(colonIndex + 1);
if (direction !== 'asc' && direction !== 'desc') {
throw new Error(
`Invalid order direction "${direction}" for field "${field}". Expected "asc" or "desc".`,
);
}
return { [field]: direction };
});
}
// TODO: Import schema from API once it exposes table/field metadata
const TABLE_SCHEMA: Record<
string,
Record<string, { type: string; ref?: string }>
> = {
transactions: {
id: { type: 'id' },
account: { type: 'id', ref: 'accounts' },
date: { type: 'date' },
amount: { type: 'integer' },
payee: { type: 'id', ref: 'payees' },
category: { type: 'id', ref: 'categories' },
notes: { type: 'string' },
imported_id: { type: 'string' },
transfer_id: { type: 'id' },
cleared: { type: 'boolean' },
reconciled: { type: 'boolean' },
starting_balance_flag: { type: 'boolean' },
imported_payee: { type: 'string' },
is_parent: { type: 'boolean' },
is_child: { type: 'boolean' },
parent_id: { type: 'id' },
sort_order: { type: 'float' },
schedule: { type: 'id', ref: 'schedules' },
'account.name': { type: 'string', ref: 'accounts' },
'payee.name': { type: 'string', ref: 'payees' },
'category.name': { type: 'string', ref: 'categories' },
'category.group.name': { type: 'string', ref: 'category_groups' },
},
accounts: {
id: { type: 'id' },
name: { type: 'string' },
offbudget: { type: 'boolean' },
closed: { type: 'boolean' },
sort_order: { type: 'float' },
},
categories: {
id: { type: 'id' },
name: { type: 'string' },
is_income: { type: 'boolean' },
group_id: { type: 'id', ref: 'category_groups' },
sort_order: { type: 'float' },
hidden: { type: 'boolean' },
'group.name': { type: 'string', ref: 'category_groups' },
},
payees: {
id: { type: 'id' },
name: { type: 'string' },
transfer_acct: { type: 'id', ref: 'accounts' },
},
rules: {
id: { type: 'id' },
stage: { type: 'string' },
conditions_op: { type: 'string' },
conditions: { type: 'json' },
actions: { type: 'json' },
},
schedules: {
id: { type: 'id' },
name: { type: 'string' },
rule: { type: 'id', ref: 'rules' },
next_date: { type: 'date' },
completed: { type: 'boolean' },
},
};
const AVAILABLE_TABLES = Object.keys(TABLE_SCHEMA).join(', ');
const LAST_DEFAULT_SELECT = [
'date',
'account.name',
'payee.name',
'category.name',
'amount',
'notes',
];
function buildQueryFromFile(
parsed: Record<string, unknown>,
fallbackTable: string | undefined,
@@ -27,34 +136,125 @@ function buildQueryFromFile(
queryObj = queryObj.orderBy(parsed.orderBy);
}
if (typeof parsed.limit === 'number') queryObj = queryObj.limit(parsed.limit);
if (typeof parsed.offset === 'number') {
queryObj = queryObj.offset(parsed.offset);
}
if (Array.isArray(parsed.groupBy)) {
queryObj = queryObj.groupBy(parsed.groupBy);
}
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);
const last = cmdOpts.last ? parseIntFlag(cmdOpts.last, '--last') : undefined;
if (cmdOpts.select) {
if (last !== undefined) {
if (cmdOpts.table && cmdOpts.table !== 'transactions') {
throw new Error(
'--last implies --table transactions. Cannot use with --table ' +
cmdOpts.table,
);
}
if (cmdOpts.limit) {
throw new Error('--last and --limit are mutually exclusive');
}
}
const table =
cmdOpts.table ?? (last !== undefined ? 'transactions' : undefined);
if (!table) {
throw new Error('--table is required (or use --file or --last)');
}
if (!(table in TABLE_SCHEMA)) {
throw new Error(
`Unknown table "${table}". Available tables: ${AVAILABLE_TABLES}`,
);
}
if (cmdOpts.where && cmdOpts.filter) {
throw new Error('--where and --filter are mutually exclusive');
}
if (cmdOpts.count && cmdOpts.select) {
throw new Error('--count and --select are mutually exclusive');
}
let queryObj = api.q(table);
if (cmdOpts.count) {
queryObj = queryObj.calculate({ $count: '*' });
} else if (cmdOpts.select) {
queryObj = queryObj.select(cmdOpts.select.split(','));
} else if (last !== undefined) {
queryObj = queryObj.select(LAST_DEFAULT_SELECT);
}
if (cmdOpts.filter) {
queryObj = queryObj.filter(JSON.parse(cmdOpts.filter));
const filterStr = cmdOpts.filter ?? cmdOpts.where;
if (filterStr) {
queryObj = queryObj.filter(JSON.parse(filterStr));
}
if (cmdOpts.orderBy) {
queryObj = queryObj.orderBy(cmdOpts.orderBy.split(','));
const orderByStr =
cmdOpts.orderBy ??
(last !== undefined && !cmdOpts.count ? 'date:desc' : undefined);
if (orderByStr) {
queryObj = queryObj.orderBy(parseOrderBy(orderByStr));
}
if (cmdOpts.limit) {
queryObj = queryObj.limit(parseIntFlag(cmdOpts.limit, '--limit'));
const limitVal =
last ??
(cmdOpts.limit ? parseIntFlag(cmdOpts.limit, '--limit') : undefined);
if (limitVal !== undefined) {
queryObj = queryObj.limit(limitVal);
}
if (cmdOpts.offset) {
queryObj = queryObj.offset(parseIntFlag(cmdOpts.offset, '--offset'));
}
if (cmdOpts.groupBy) {
queryObj = queryObj.groupBy(cmdOpts.groupBy.split(','));
}
return queryObj;
}
const RUN_EXAMPLES = `
Examples:
# Show last 5 transactions (shortcut)
actual query run --last 5
# Transactions ordered by date descending
actual query run --table transactions --select "date,amount,payee.name" --order-by "date:desc" --limit 10
# Filter with JSON (negative amounts = expenses)
actual query run --table transactions --filter '{"amount":{"$lt":0}}' --limit 5
# Count transactions
actual query run --table transactions --count
# Group by category (use --file for aggregate expressions)
echo '{"table":"transactions","groupBy":["category.name"],"select":["category.name",{"amount":{"$sum":"$amount"}}]}' | actual query run --file -
# Pagination
actual query run --table transactions --order-by "date:desc" --limit 10 --offset 20
# Use --where (alias for --filter)
actual query run --table transactions --where '{"payee.name":"Grocery Store"}' --limit 5
# Read query from a JSON file
actual query run --file query.json
# Pipe query from stdin
echo '{"table":"transactions","limit":5}' | actual query run --file -
Available tables: ${AVAILABLE_TABLES}
Use "actual query tables" and "actual query fields <table>" for schema info.
Common filter operators: $eq, $ne, $lt, $lte, $gt, $gte, $like, $and, $or
See ActualQL docs for full reference: https://actualbudget.org/docs/api/actual-ql/`;
export function registerQueryCommand(program: Command) {
const query = program
.command('query')
@@ -65,16 +265,34 @@ export function registerQueryCommand(program: Command) {
.description('Execute an AQL query')
.option(
'--table <table>',
'Table to query (transactions, accounts, categories, payees)',
'Table to query (use "actual query tables" to list available tables)',
)
.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('--filter <json>', 'Filter as JSON (e.g. \'{"amount":{"$lt":0}}\')')
.option(
'--where <json>',
'Alias for --filter (cannot be used together with --filter)',
)
.option(
'--order-by <fields>',
'Fields with optional direction: field1:desc,field2 (default: asc)',
)
.option('--limit <n>', 'Limit number of results')
.option('--offset <n>', 'Skip first N results (for pagination)')
.option(
'--last <n>',
'Show last N transactions (implies --table transactions, --order-by date:desc)',
)
.option('--count', 'Count matching rows instead of returning them')
.option(
'--group-by <fields>',
'Comma-separated fields to group by (use with aggregate selects)',
)
.option(
'--file <path>',
'Read full query object from JSON file (use - for stdin)',
)
.addHelpText('after', RUN_EXAMPLES)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
@@ -87,7 +305,40 @@ export function registerQueryCommand(program: Command) {
: buildQueryFromFlags(cmdOpts);
const result = await api.aqlQuery(queryObj);
printOutput(result, opts.format);
if (cmdOpts.count) {
printOutput({ count: result.data }, opts.format);
} else {
printOutput(result, opts.format);
}
});
});
query
.command('tables')
.description('List available tables for querying')
.action(() => {
const opts = program.opts();
const tables = Object.keys(TABLE_SCHEMA).map(name => ({ name }));
printOutput(tables, opts.format);
});
query
.command('fields <table>')
.description('List fields for a given table')
.action((table: string) => {
const opts = program.opts();
const schema = TABLE_SCHEMA[table];
if (!schema) {
throw new Error(
`Unknown table "${table}". Available tables: ${Object.keys(TABLE_SCHEMA).join(', ')}`,
);
}
const fields = Object.entries(schema).map(([name, info]) => ({
name,
type: info.type,
...(info.ref ? { ref: info.ref } : {}),
}));
printOutput(fields, opts.format);
});
}

View File

@@ -101,3 +101,30 @@ or
);
});
```
## CLI Usage
The examples above are shown in JavaScript. If you're using the [CLI tool](../cli.md), you can express many of the same queries with command-line flags. Here's how the JS patterns translate:
```bash
# Select specific fields (JS: .select(['date', 'amount', 'payee.name']))
actual query run --table transactions --select "date,amount,payee.name"
# Filter by condition (JS: .filter({ amount: { $lt: 0 } }))
actual query run --table transactions --filter '{"amount":{"$lt":0}}'
# Order by field descending (JS: .orderBy([{ date: 'desc' }]))
actual query run --table transactions --order-by "date:desc"
# Search by month (JS: .filter({ date: { $transform: '$month', $eq: '2021-01' } }))
actual query run --table transactions --filter '{"date":{"$transform":"$month","$eq":"2021-01"}}'
# Group by payee with sum — use --file for aggregate queries
echo '{"table":"transactions","groupBy":["payee.name"],"select":["payee.name",{"amount":{"$sum":"$amount"}}]}' | actual query run --file -
# Count transactions (JS: .calculate({ $count: '*' }))
actual query run --table transactions --count
# Quick shortcut: last 10 transactions
actual query run --last 10
```

View File

@@ -274,16 +274,80 @@ actual schedules delete <id>
### Query (ActualQL)
Run queries using [ActualQL](./actual-ql/index.md):
Run queries using [ActualQL](./actual-ql/index.md).
#### Subcommands
| Subcommand | Description |
| ---------------------- | --------------------------------- |
| `query run` | Execute an AQL query |
| `query tables` | List available tables |
| `query fields <table>` | List fields and types for a table |
#### `query run` Options
| Option | Description |
| --------------------- | ------------------------------------------------------------------------------------------- |
| `--table <table>` | Table to query (use `actual query tables` to list) |
| `--select <fields>` | Comma-separated fields to select |
| `--filter <json>` | Filter as JSON (e.g. `'{"amount":{"$lt":0}}'`) |
| `--where <json>` | Alias for `--filter` (cannot be used together) |
| `--order-by <fields>` | Fields with optional direction: `field1:desc,field2` (default: asc) |
| `--limit <n>` | Limit number of results |
| `--offset <n>` | Skip first N results (for pagination) |
| `--last <n>` | Show last N transactions (shortcut: implies `--table transactions`, `--order-by date:desc`) |
| `--count` | Count matching rows instead of returning them |
| `--group-by <fields>` | Comma-separated fields to group by |
| `--file <path>` | Read query from JSON file (use `-` for stdin) |
#### Examples
```bash
# Run a query (inline)
actual query run --table transactions --select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
# Show last 5 transactions (convenience shortcut)
actual query run --last 5
# Run a query (from file)
# Override default columns with --last
actual query run --last 10 --select "date,amount,notes"
# Transactions ordered by date descending with limit
actual query run --table transactions --select "date,amount,payee.name" --order-by "date:desc" --limit 10
# Filter with JSON — negative amounts (expenses)
actual query run --table transactions --filter '{"amount":{"$lt":0}}' --limit 5
# Use --where (alias for --filter, more intuitive for SQL users)
actual query run --table transactions --where '{"payee.name":"Grocery Store"}' --limit 5
# Count all transactions
actual query run --table transactions --count
# Count with a filter
actual query run --table transactions --filter '{"category.name":"Groceries"}' --count
# Group by category with aggregate (use --file for aggregate expressions)
echo '{"table":"transactions","groupBy":["category.name"],"select":["category.name",{"amount":{"$sum":"$amount"}}]}' | actual query run --file -
# Pagination: skip first 20, show next 10
actual query run --table transactions --order-by "date:desc" --limit 10 --offset 20
# Multi-field ordering
actual query run --table transactions --order-by "date:desc,amount:asc" --limit 10
# Run a query from a JSON file
actual query run --file query.json
# Pipe query from stdin
echo '{"table":"transactions","select":["date","amount"],"limit":5}' | actual query run --file -
# List available tables
actual query tables
# List fields for a table
actual query fields transactions
```
See [ActualQL](./actual-ql/index.md) for full filter/function reference including `$transform`, `$month`, `$year`, and aggregate functions.
### Server
```bash

7
packages/mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# iOS build artifacts
ios/App/Pods/
ios/App/DerivedData/
ios/App/build/
# Node modules are at root level
node_modules/

View File

@@ -0,0 +1,46 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'org.actualbudget.mobile',
appName: 'Actual Budget',
// Point to the desktop-client build output
webDir: '../desktop-client/build',
server: {
// Serve from localhost to enable COOP/COEP headers,
// which are required for SharedArrayBuffer (used by absurd-sql).
// On iOS, this uses WKURLSchemeHandler with the http scheme,
// allowing cross-origin isolation to work in WKWebView.
iosScheme: 'http',
androidScheme: 'http',
},
ios: {
// Minimum iOS version that supports SharedArrayBuffer in WKWebView
// with cross-origin isolation headers (iOS 15.2+)
minVersion: '16.0',
// Allow inline media playback (needed for some UI interactions)
allowsLinkPreview: false,
contentInset: 'always',
preferredContentMode: 'mobile',
},
plugins: {
SplashScreen: {
launchAutoHide: true,
launchShowDuration: 2000,
backgroundColor: '#5c3dbb',
showSpinner: false,
iosSpinnerStyle: 'small',
splashFullScreen: true,
splashImmersive: true,
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
},
StatusBar: {
style: 'LIGHT',
backgroundColor: '#5c3dbb',
},
},
};
export default config;

13
packages/mobile/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml

View File

@@ -0,0 +1,416 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
A1B2C3D41000000000000001 /* ActualBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */; };
A1B2C3D41000000000000003 /* CrossOriginIsolationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActualBridgeViewController.swift; sourceTree = "<group>"; };
A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossOriginIsolationPlugin.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */,
A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
A1B2C3D41000000000000001 /* ActualBridgeViewController.swift in Sources */,
A1B2C3D41000000000000003 /* CrossOriginIsolationPlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = org.actualbudget.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.actualbudget.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
import UIKit
import Capacitor
import WebKit
/// Custom Capacitor bridge view controller that enables cross-origin isolation
/// for SharedArrayBuffer support in WKWebView.
///
/// SharedArrayBuffer is required by absurd-sql, which Actual Budget uses for
/// local-first SQLite storage in the browser/WebView environment.
class ActualBridgeViewController: CAPBridgeViewController {
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
let config = super.webViewConfiguration(for: instanceConfiguration)
// Enable SharedArrayBuffer support by setting crossOriginIsolation
// This is available on iOS 15.4+ and is required for absurd-sql
if #available(iOS 15.4, *) {
// Set preferences for cross-origin isolation
let prefs = config.preferences
prefs.isElementFullscreenEnabled = true
}
// Allow file access for local assets
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
return config
}
override func capacitorDidLoad() {
// Register the cross-origin isolation plugin
bridge?.registerPluginInstance(CrossOriginIsolationPlugin())
}
}

View File

@@ -0,0 +1,49 @@
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -0,0 +1,14 @@
{
"images": [
{
"filename": "AppIcon-512@2x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info": {
"version": 1,
"author": "xcode"
}
}

View File

@@ -0,0 +1,23 @@
{
"images": [
{
"idiom": "universal",
"filename": "splash-2732x2732-2.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "splash-2732x2732-1.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "splash-2732x2732.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</imageView>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
</dependencies>
<scenes>
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ActualBridgeViewController" customModule="App" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,39 @@
import Foundation
import Capacitor
import WebKit
/// A Capacitor plugin that injects Cross-Origin-Opener-Policy and
/// Cross-Origin-Embedder-Policy headers into the WKWebView configuration.
///
/// These headers are required for SharedArrayBuffer support, which is used by
/// absurd-sql (the SQLite-in-browser engine) for offline local-first data storage.
///
/// On iOS 16+, WKWebView supports SharedArrayBuffer when COOP/COEP headers are present.
@objc(CrossOriginIsolationPlugin)
public class CrossOriginIsolationPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CrossOriginIsolationPlugin"
public let jsName = "CrossOriginIsolation"
public let pluginMethods: [CAPPluginMethod] = []
override public func load() {
// Inject a script that sets SharedArrayBuffer override flag
// as a fallback for environments where COOP/COEP can't be set
let script = WKUserScript(
source: """
// Ensure SharedArrayBuffer override is set for Capacitor context
if (!window.SharedArrayBuffer) {
localStorage.setItem('SharedArrayBufferOverride', 'true');
console.log('[Capacitor] SharedArrayBuffer not available, override enabled');
} else {
console.log('[Capacitor] SharedArrayBuffer is available');
}
// Mark as running in Capacitor for platform detection
window.__ACTUAL_IS_CAPACITOR__ = true;
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
bridge?.webView?.configuration.userContentController.addUserScript(script)
}
}

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Actual Budget</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,23 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '16.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end

View File

@@ -0,0 +1,24 @@
{
"name": "@actual-app/mobile",
"version": "26.3.0",
"private": true,
"description": "Actual Budget iOS app powered by Capacitor",
"license": "MIT",
"scripts": {
"build": "yarn workspace @actual-app/web build && npx cap sync ios",
"sync": "npx cap sync ios",
"open": "npx cap open ios",
"run:ios": "npx cap run ios"
},
"dependencies": {
"@capacitor/app": "^7.0.0",
"@capacitor/core": "^7.0.0",
"@capacitor/ios": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
"@capacitor/splash-screen": "^7.0.0",
"@capacitor/status-bar": "^7.0.0"
},
"devDependencies": {
"@capacitor/cli": "^7.0.0"
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": ["capacitor.config.ts"]
}

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [MatissJanis]
---
cli: improved aql support

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Add post-merge hook to automatically install dependencies when yarn.lock changes after merges.

394
yarn.lock
View File

@@ -182,6 +182,20 @@ __metadata:
languageName: unknown
linkType: soft
"@actual-app/mobile@workspace:packages/mobile":
version: 0.0.0-use.local
resolution: "@actual-app/mobile@workspace:packages/mobile"
dependencies:
"@capacitor/app": "npm:^7.0.0"
"@capacitor/cli": "npm:^7.0.0"
"@capacitor/core": "npm:^7.0.0"
"@capacitor/ios": "npm:^7.0.0"
"@capacitor/keyboard": "npm:^7.0.0"
"@capacitor/splash-screen": "npm:^7.0.0"
"@capacitor/status-bar": "npm:^7.0.0"
languageName: unknown
linkType: soft
"@actual-app/sync-server@workspace:*, @actual-app/sync-server@workspace:packages/sync-server":
version: 0.0.0-use.local
resolution: "@actual-app/sync-server@workspace:packages/sync-server"
@@ -2137,6 +2151,88 @@ __metadata:
languageName: node
linkType: hard
"@capacitor/app@npm:^7.0.0":
version: 7.1.2
resolution: "@capacitor/app@npm:7.1.2"
peerDependencies:
"@capacitor/core": ">=7.0.0"
checksum: 10/f48ebfca4e86cbf578a893cbfc9136b70af85a42638048467b32a68a98548b0713d524f7fe24ed0172acaa8d94324d185ba4e2d6e83608da84ce79f1cb38aafe
languageName: node
linkType: hard
"@capacitor/cli@npm:^7.0.0":
version: 7.6.0
resolution: "@capacitor/cli@npm:7.6.0"
dependencies:
"@ionic/cli-framework-output": "npm:^2.2.8"
"@ionic/utils-subprocess": "npm:^3.0.1"
"@ionic/utils-terminal": "npm:^2.3.5"
commander: "npm:^12.1.0"
debug: "npm:^4.4.0"
env-paths: "npm:^2.2.0"
fs-extra: "npm:^11.2.0"
kleur: "npm:^4.1.5"
native-run: "npm:^2.0.3"
open: "npm:^8.4.0"
plist: "npm:^3.1.0"
prompts: "npm:^2.4.2"
rimraf: "npm:^6.0.1"
semver: "npm:^7.6.3"
tar: "npm:^7.5.3"
tslib: "npm:^2.8.1"
xml2js: "npm:^0.6.2"
bin:
cap: bin/capacitor
capacitor: bin/capacitor
checksum: 10/87d5a765df53548814b5aac2da15f4bd53fb0b5bd400e6837f906d7a486e59f88a0e67891a12f31c1acf89a210703396d5a90744aa68794aae04ca6ef47beba3
languageName: node
linkType: hard
"@capacitor/core@npm:^7.0.0":
version: 7.6.0
resolution: "@capacitor/core@npm:7.6.0"
dependencies:
tslib: "npm:^2.1.0"
checksum: 10/7eb5e45e8e5a39852b758c0fe3fab62df4af7ad903dd286395bdb1408a46eac0eb1a6c36d34b94f66f7c7263f026b543325fd973adf93ed0886d1b27b62c9f99
languageName: node
linkType: hard
"@capacitor/ios@npm:^7.0.0":
version: 7.6.0
resolution: "@capacitor/ios@npm:7.6.0"
peerDependencies:
"@capacitor/core": ^7.6.0
checksum: 10/08ef3c1cf0085b1268d6320265ec04b1b48a0d53310e265b8cf2833a60de6ee9da01e4a92dbfc93e0259611cd3044f399164720955107e8c2a1caf3fa11baf41
languageName: node
linkType: hard
"@capacitor/keyboard@npm:^7.0.0":
version: 7.0.5
resolution: "@capacitor/keyboard@npm:7.0.5"
peerDependencies:
"@capacitor/core": ">=7.0.0"
checksum: 10/c4588923e436c7ac0ddcba58437c7430d405e97da17a6c6e0a34e9d4a50f1be9f757ecacc748ae439b56de0c3b805d5d32819d010674385ac38002bfb3d58f55
languageName: node
linkType: hard
"@capacitor/splash-screen@npm:^7.0.0":
version: 7.0.5
resolution: "@capacitor/splash-screen@npm:7.0.5"
peerDependencies:
"@capacitor/core": ">=7.0.0"
checksum: 10/6b4668150c9d82a5a4819139f5440a0040bd656178519a68b5ae8ef89be17a3eebe10b062903f76a0ee492b342c1d8975d400c62e0d4ea6de45bbfcf1c99c771
languageName: node
linkType: hard
"@capacitor/status-bar@npm:^7.0.0":
version: 7.0.5
resolution: "@capacitor/status-bar@npm:7.0.5"
peerDependencies:
"@capacitor/core": ">=7.0.0"
checksum: 10/8a50eb8a4c14126469dde5604bb272c33472ee7a607e40a031f26adaff8de0ac72bb0a45575fe727084a9a5e87340dacb46f153e095c6cb147fbe76506bb98e4
languageName: node
linkType: hard
"@chevrotain/cst-dts-gen@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
@@ -4630,6 +4726,106 @@ __metadata:
languageName: node
linkType: hard
"@ionic/cli-framework-output@npm:^2.2.8":
version: 2.2.8
resolution: "@ionic/cli-framework-output@npm:2.2.8"
dependencies:
"@ionic/utils-terminal": "npm:2.3.5"
debug: "npm:^4.0.0"
tslib: "npm:^2.0.1"
checksum: 10/773cf0cdbd0fbb5709822b84c06a9e9cec6df5a87d9542bcc21a027a9434117e1d3f1a7d04fe11f252d9d232a5678d056ea80d2d4506e38a5f18fc563a990f34
languageName: node
linkType: hard
"@ionic/utils-array@npm:2.1.6":
version: 2.1.6
resolution: "@ionic/utils-array@npm:2.1.6"
dependencies:
debug: "npm:^4.0.0"
tslib: "npm:^2.0.1"
checksum: 10/13d7b56906bb394a9362622c001578f0788f06ee2d8c724a51fb415cf4bdc1ccf1f92c2358935524f0089a660a4323d5f0bfa9403a0b3050ed921c039125b5e8
languageName: node
linkType: hard
"@ionic/utils-fs@npm:3.1.7, @ionic/utils-fs@npm:^3.1.7":
version: 3.1.7
resolution: "@ionic/utils-fs@npm:3.1.7"
dependencies:
"@types/fs-extra": "npm:^8.0.0"
debug: "npm:^4.0.0"
fs-extra: "npm:^9.0.0"
tslib: "npm:^2.0.1"
checksum: 10/65279a445b4499b9db40dae0c4eface0fdcd6ee95c7931ecb89fb097ee07e61e316ad13da7f495ee567d95dd0dddcbad9b1e46656676e22e51041dde62b06b9d
languageName: node
linkType: hard
"@ionic/utils-object@npm:2.1.6":
version: 2.1.6
resolution: "@ionic/utils-object@npm:2.1.6"
dependencies:
debug: "npm:^4.0.0"
tslib: "npm:^2.0.1"
checksum: 10/c545f09ede118a801c7eb4c794d4b479c574a98023b752421c0b18e340cb0f509aa7d7e92ef9b1048361e194a0b6f80df8922cd56bae8201d2fb796b71d87e60
languageName: node
linkType: hard
"@ionic/utils-process@npm:2.1.12":
version: 2.1.12
resolution: "@ionic/utils-process@npm:2.1.12"
dependencies:
"@ionic/utils-object": "npm:2.1.6"
"@ionic/utils-terminal": "npm:2.3.5"
debug: "npm:^4.0.0"
signal-exit: "npm:^3.0.3"
tree-kill: "npm:^1.2.2"
tslib: "npm:^2.0.1"
checksum: 10/3a94eeb4cc7c05a2d3058e997a2ca05c50bf1a9d76e39b91659d0cbb402adbe2b24b8bc08d8b95bbab1703328dcc50c2e333b345700709ed2ec94e8d844d20c1
languageName: node
linkType: hard
"@ionic/utils-stream@npm:3.1.7":
version: 3.1.7
resolution: "@ionic/utils-stream@npm:3.1.7"
dependencies:
debug: "npm:^4.0.0"
tslib: "npm:^2.0.1"
checksum: 10/23667cfeaa4f710e556cd4f055ae6a99a59bf9f83ed7224fea5cf82d2e0539b8d42c4bf133db82f8a2156c004145fdb2e252837388b552c5775126700bce4a95
languageName: node
linkType: hard
"@ionic/utils-subprocess@npm:^3.0.1":
version: 3.0.1
resolution: "@ionic/utils-subprocess@npm:3.0.1"
dependencies:
"@ionic/utils-array": "npm:2.1.6"
"@ionic/utils-fs": "npm:3.1.7"
"@ionic/utils-process": "npm:2.1.12"
"@ionic/utils-stream": "npm:3.1.7"
"@ionic/utils-terminal": "npm:2.3.5"
cross-spawn: "npm:^7.0.3"
debug: "npm:^4.0.0"
tslib: "npm:^2.0.1"
checksum: 10/7411d691b3cdaa1ab74171a521813118a4b7180a373d6d80ee6b2b7946490486b3cb9f8ca338a9ffb4167c098d0c79ce14d8579ac82db794e5e1017b73bf85eb
languageName: node
linkType: hard
"@ionic/utils-terminal@npm:2.3.5, @ionic/utils-terminal@npm:^2.3.4, @ionic/utils-terminal@npm:^2.3.5":
version: 2.3.5
resolution: "@ionic/utils-terminal@npm:2.3.5"
dependencies:
"@types/slice-ansi": "npm:^4.0.0"
debug: "npm:^4.0.0"
signal-exit: "npm:^3.0.3"
slice-ansi: "npm:^4.0.0"
string-width: "npm:^4.1.0"
strip-ansi: "npm:^6.0.0"
tslib: "npm:^2.0.1"
untildify: "npm:^4.0.0"
wrap-ansi: "npm:^7.0.0"
checksum: 10/40bae30f8cff2d7efa74118d96b2ab16ac9048ba8ffe28a8c203cd398f2688955ddb28d5ebe0c6948890632dc1ca2fe5e84c547ce930cde3ac7be4d5f53e5245
languageName: node
linkType: hard
"@isaacs/balanced-match@npm:^4.0.1":
version: 4.0.1
resolution: "@isaacs/balanced-match@npm:4.0.1"
@@ -9911,6 +10107,15 @@ __metadata:
languageName: node
linkType: hard
"@types/fs-extra@npm:^8.0.0":
version: 8.1.5
resolution: "@types/fs-extra@npm:8.1.5"
dependencies:
"@types/node": "npm:*"
checksum: 10/565d9e55cd05064b3ab272b8748ed512b8fa5cddc23fd32b0d5f147f9ea3a45981577c4478b5060cae7b3d914c508bd2ea97eb84d9c1fa6f967982c892e4ab26
languageName: node
linkType: hard
"@types/geojson@npm:*":
version: 7946.0.16
resolution: "@types/geojson@npm:7946.0.16"
@@ -10368,6 +10573,13 @@ __metadata:
languageName: node
linkType: hard
"@types/slice-ansi@npm:^4.0.0":
version: 4.0.0
resolution: "@types/slice-ansi@npm:4.0.0"
checksum: 10/343086bc4e686349382b386a6fb49d632fb4dd08ecc08f1b9872815199efe8db7648731e56c1d10dc0f596fe77a8fce6136e0067136f8e0cefda6b29797aa4f1
languageName: node
linkType: hard
"@types/sockjs@npm:^0.3.36":
version: 0.3.36
resolution: "@types/sockjs@npm:0.3.36"
@@ -12270,6 +12482,13 @@ __metadata:
languageName: node
linkType: hard
"big-integer@npm:1.6.x":
version: 1.6.52
resolution: "big-integer@npm:1.6.52"
checksum: 10/4bc6ae152a96edc9f95020f5fc66b13d26a9ad9a021225a9f0213f7e3dc44269f423aa8c42e19d6ac4a63bb2b22140b95d10be8f9ca7a6d9aa1b22b330d1f514
languageName: node
linkType: hard
"big.js@npm:^5.2.2":
version: 5.2.2
resolution: "big.js@npm:5.2.2"
@@ -12422,6 +12641,15 @@ __metadata:
languageName: node
linkType: hard
"bplist-parser@npm:^0.3.2":
version: 0.3.2
resolution: "bplist-parser@npm:0.3.2"
dependencies:
big-integer: "npm:1.6.x"
checksum: 10/6edf4354c32f5661c258422e478be0f5c6a779bb87c2ae15ee92dd1c046368decbff8a28c86c558a3b7007e1381b91d5eed1c4c8e83e86405197777d944abaa8
languageName: node
linkType: hard
"brace-expansion@npm:^1.1.7":
version: 1.1.12
resolution: "brace-expansion@npm:1.1.12"
@@ -15582,6 +15810,15 @@ __metadata:
languageName: node
linkType: hard
"elementtree@npm:^0.1.7":
version: 0.1.7
resolution: "elementtree@npm:0.1.7"
dependencies:
sax: "npm:1.1.4"
checksum: 10/7e8f0683c0f5a95b298f56426f0f3796600ddac611cfaf92b76f1f4354b04c3f9aadadd1f86a91e5df372a58f2c3da97ae5988597f5052dd15bbeefbff8381c9
languageName: node
linkType: hard
"elliptic@npm:^6.5.3, elliptic@npm:^6.6.1":
version: 6.6.1
resolution: "elliptic@npm:6.6.1"
@@ -17629,6 +17866,17 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^13.0.3":
version: 13.0.6
resolution: "glob@npm:13.0.6"
dependencies:
minimatch: "npm:^10.2.2"
minipass: "npm:^7.1.3"
path-scurry: "npm:^2.0.2"
checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214
languageName: node
linkType: hard
"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.6":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@@ -18774,6 +19022,13 @@ __metadata:
languageName: node
linkType: hard
"ini@npm:^4.1.1":
version: 4.1.3
resolution: "ini@npm:4.1.3"
checksum: 10/f536b414d1442e5b233429e2b56efcdb354109b2d65ddd489e5939d8f0f5ad23c88aa2b19c92987249d0dd63ba8192e9aeb1a02b0459549c5a9ff31acd729a5d
languageName: node
linkType: hard
"inline-style-parser@npm:0.2.4":
version: 0.2.4
resolution: "inline-style-parser@npm:0.2.4"
@@ -19954,6 +20209,13 @@ __metadata:
languageName: node
linkType: hard
"kleur@npm:^4.1.5":
version: 4.1.5
resolution: "kleur@npm:4.1.5"
checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df
languageName: node
linkType: hard
"kolorist@npm:^1.8.0":
version: 1.8.0
resolution: "kolorist@npm:1.8.0"
@@ -21892,6 +22154,13 @@ __metadata:
languageName: node
linkType: hard
"minipass@npm:^7.1.3":
version: 7.1.3
resolution: "minipass@npm:7.1.3"
checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6
languageName: node
linkType: hard
"minizlib@npm:^2.1.1":
version: 2.1.2
resolution: "minizlib@npm:2.1.2"
@@ -22039,6 +22308,27 @@ __metadata:
languageName: node
linkType: hard
"native-run@npm:^2.0.3":
version: 2.0.3
resolution: "native-run@npm:2.0.3"
dependencies:
"@ionic/utils-fs": "npm:^3.1.7"
"@ionic/utils-terminal": "npm:^2.3.4"
bplist-parser: "npm:^0.3.2"
debug: "npm:^4.3.4"
elementtree: "npm:^0.1.7"
ini: "npm:^4.1.1"
plist: "npm:^3.1.0"
split2: "npm:^4.2.0"
through2: "npm:^4.0.2"
tslib: "npm:^2.6.2"
yauzl: "npm:^2.10.0"
bin:
native-run: bin/native-run
checksum: 10/f78268262d94f758d4d0ecb7a3ece3c4b38f57aefcc9d71d99e0f1d81c5ea9bf2dd6f459fab64652071f5743f3a80e92259590c54754be6f25a600d9b4ccd856
languageName: node
linkType: hard
"natural-compare@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare@npm:1.4.0"
@@ -22945,7 +23235,7 @@ __metadata:
languageName: node
linkType: hard
"package-json-from-dist@npm:^1.0.0":
"package-json-from-dist@npm:^1.0.0, package-json-from-dist@npm:^1.0.1":
version: 1.0.1
resolution: "package-json-from-dist@npm:1.0.1"
checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602
@@ -23198,6 +23488,16 @@ __metadata:
languageName: node
linkType: hard
"path-scurry@npm:^2.0.2":
version: 2.0.2
resolution: "path-scurry@npm:2.0.2"
dependencies:
lru-cache: "npm:^11.0.0"
minipass: "npm:^7.1.2"
checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3
languageName: node
linkType: hard
"path-to-regexp@npm:0.1.12":
version: 0.1.12
resolution: "path-to-regexp@npm:0.1.12"
@@ -25359,6 +25659,17 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
inherits: "npm:^2.0.3"
string_decoder: "npm:^1.1.1"
util-deprecate: "npm:^1.0.1"
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
languageName: node
linkType: hard
"readable-stream@npm:^2.0.1, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
@@ -25374,17 +25685,6 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
inherits: "npm:^2.0.3"
string_decoder: "npm:^1.1.1"
util-deprecate: "npm:^1.0.1"
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
languageName: node
linkType: hard
"readable-stream@npm:~1.0.31":
version: 1.0.34
resolution: "readable-stream@npm:1.0.34"
@@ -26039,6 +26339,18 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^6.0.1":
version: 6.1.3
resolution: "rimraf@npm:6.1.3"
dependencies:
glob: "npm:^13.0.3"
package-json-from-dist: "npm:^1.0.1"
bin:
rimraf: dist/esm/bin.mjs
checksum: 10/dd98ec2ad7cd2cccae1c7110754d472eac8edb2bab8a8b057dce04edfe1433dab246a889b3fd85a66c78ca81caa1429caa0e736c7647f6832b04fd5d4dfb8ab8
languageName: node
linkType: hard
"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.3":
version: 2.0.3
resolution: "ripemd160@npm:2.0.3"
@@ -26396,6 +26708,13 @@ __metadata:
languageName: node
linkType: hard
"sax@npm:1.1.4":
version: 1.1.4
resolution: "sax@npm:1.1.4"
checksum: 10/50dd85c562f6de00f1a6c4049f27471258b35c77961498784976445455f99d105bac0de2edf342730b6e0e410346581622c21494355178b6883611619757ae1b
languageName: node
linkType: hard
"sax@npm:>=0.6.0, sax@npm:^1.2.4":
version: 1.4.1
resolution: "sax@npm:1.4.1"
@@ -26986,6 +27305,17 @@ __metadata:
languageName: node
linkType: hard
"slice-ansi@npm:^4.0.0":
version: 4.0.0
resolution: "slice-ansi@npm:4.0.0"
dependencies:
ansi-styles: "npm:^4.0.0"
astral-regex: "npm:^2.0.0"
is-fullwidth-code-point: "npm:^3.0.0"
checksum: 10/4a82d7f085b0e1b070e004941ada3c40d3818563ac44766cca4ceadd2080427d337554f9f99a13aaeb3b4a94d9964d9466c807b3d7b7541d1ec37ee32d308756
languageName: node
linkType: hard
"slice-ansi@npm:^7.1.0":
version: 7.1.2
resolution: "slice-ansi@npm:7.1.2"
@@ -27206,6 +27536,13 @@ __metadata:
languageName: node
linkType: hard
"split2@npm:^4.2.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"
checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.1, sprintf-js@npm:^1.1.2":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
@@ -27882,6 +28219,19 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^7.5.3":
version: 7.5.12
resolution: "tar@npm:7.5.12"
dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2"
minizlib: "npm:^3.1.0"
yallist: "npm:^5.0.0"
checksum: 10/a72114d28ab9b4878eeebaae8987692a577c390683c13f150d8330e139237038cc46fbb0be6983b02acf5a31b01d74776436ba03790f320a59efb44b8ac39e39
languageName: node
linkType: hard
"teex@npm:^1.0.1":
version: 1.0.1
resolution: "teex@npm:1.0.1"
@@ -28017,6 +28367,15 @@ __metadata:
languageName: node
linkType: hard
"through2@npm:^4.0.2":
version: 4.0.2
resolution: "through2@npm:4.0.2"
dependencies:
readable-stream: "npm:3"
checksum: 10/72c246233d9a989bbebeb6b698ef0b7b9064cb1c47930f79b25d87b6c867e075432811f69b7b2ac8da00ca308191c507bdab913944be8019ac43b036ce88f6ba
languageName: node
linkType: hard
"thunky@npm:^1.0.2":
version: 1.1.0
resolution: "thunky@npm:1.1.0"
@@ -28258,6 +28617,15 @@ __metadata:
languageName: node
linkType: hard
"tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
bin:
tree-kill: cli.js
checksum: 10/49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7
languageName: node
linkType: hard
"trim-lines@npm:^3.0.0":
version: 3.0.1
resolution: "trim-lines@npm:3.0.1"
@@ -28364,7 +28732,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7