@lunchflow/actual-flow 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +196 -0
  2. package/dist/actual-budget-client.d.ts +19 -0
  3. package/dist/actual-budget-client.d.ts.map +1 -0
  4. package/dist/actual-budget-client.js +161 -0
  5. package/dist/actual-budget-client.js.map +1 -0
  6. package/dist/config-manager.d.ts +16 -0
  7. package/dist/config-manager.d.ts.map +1 -0
  8. package/dist/config-manager.js +119 -0
  9. package/dist/config-manager.js.map +1 -0
  10. package/dist/importer.d.ts +20 -0
  11. package/dist/importer.d.ts.map +1 -0
  12. package/dist/importer.js +234 -0
  13. package/dist/importer.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +30 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lunch-flow-client.d.ts +10 -0
  19. package/dist/lunch-flow-client.d.ts.map +1 -0
  20. package/dist/lunch-flow-client.js +67 -0
  21. package/dist/lunch-flow-client.js.map +1 -0
  22. package/dist/transaction-mapper.d.ts +8 -0
  23. package/dist/transaction-mapper.d.ts.map +1 -0
  24. package/dist/transaction-mapper.js +32 -0
  25. package/dist/transaction-mapper.js.map +1 -0
  26. package/dist/types.d.ts +55 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +3 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/ui.d.ts +30 -0
  31. package/dist/ui.d.ts.map +1 -0
  32. package/dist/ui.js +278 -0
  33. package/dist/ui.js.map +1 -0
  34. package/install.sh +55 -0
  35. package/package.json +35 -0
  36. package/pnpm-workspace.yaml +2 -0
  37. package/src/actual-budget-client.ts +132 -0
  38. package/src/config-manager.ts +128 -0
  39. package/src/importer.ts +279 -0
  40. package/src/index.ts +28 -0
  41. package/src/lunch-flow-client.ts +64 -0
  42. package/src/transaction-mapper.ts +37 -0
  43. package/src/types.ts +60 -0
  44. package/src/ui.ts +314 -0
  45. package/tsconfig.json +19 -0
package/dist/ui.js ADDED
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TerminalUI = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const ora_1 = __importDefault(require("ora"));
10
+ const cli_table3_1 = __importDefault(require("cli-table3"));
11
+ class TerminalUI {
12
+ async showWelcome() {
13
+ console.clear();
14
+ console.log(chalk_1.default.blue.bold('\nšŸ½ļø Lunch Flow → Actual Budget Importer\n'));
15
+ console.log(chalk_1.default.gray('This tool helps you import transactions from Lunch Flow to Actual Budget\n'));
16
+ }
17
+ async getLunchFlowCredentials() {
18
+ console.log(chalk_1.default.yellow('šŸ“” Lunch Flow Configuration\n'));
19
+ const answers = await inquirer_1.default.prompt([
20
+ {
21
+ type: 'input',
22
+ name: 'apiKey',
23
+ message: 'Enter your Lunch Flow API key:',
24
+ validate: (input) => input.length > 0 || 'API key is required',
25
+ },
26
+ {
27
+ type: 'input',
28
+ name: 'baseUrl',
29
+ message: 'Enter Lunch Flow API base URL:',
30
+ default: 'https://api.lunchflow.com',
31
+ validate: (input) => {
32
+ try {
33
+ new URL(input);
34
+ return true;
35
+ }
36
+ catch {
37
+ return 'Please enter a valid URL';
38
+ }
39
+ },
40
+ },
41
+ ]);
42
+ return answers;
43
+ }
44
+ async getActualBudgetCredentials() {
45
+ console.log(chalk_1.default.yellow('\nšŸ’° Actual Budget Configuration\n'));
46
+ console.log(chalk_1.default.gray('To find your budget sync ID:'));
47
+ console.log(chalk_1.default.gray('1. Open Actual Budget in your browser'));
48
+ console.log(chalk_1.default.gray('2. Go to Settings → Show advanced settings'));
49
+ console.log(chalk_1.default.gray('3. Look for "Sync ID" - that\'s your budget sync ID'));
50
+ console.log(chalk_1.default.gray('4. Or check the URL: http://localhost:5007/budget/your-sync-id'));
51
+ console.log(chalk_1.default.gray('5. The sync ID is the part after "/budget/"\n'));
52
+ const answers = await inquirer_1.default.prompt([
53
+ {
54
+ type: 'input',
55
+ name: 'serverUrl',
56
+ message: 'Enter Actual Budget server URL:',
57
+ default: 'http://localhost:5007',
58
+ validate: (input) => {
59
+ try {
60
+ new URL(input);
61
+ return true;
62
+ }
63
+ catch {
64
+ return 'Please enter a valid URL';
65
+ }
66
+ },
67
+ },
68
+ {
69
+ type: 'input',
70
+ name: 'budgetSyncId',
71
+ message: 'Enter Actual Budget budget sync ID:',
72
+ validate: (input) => input.length > 0 || 'Budget sync ID is required',
73
+ },
74
+ {
75
+ type: 'password',
76
+ name: 'password',
77
+ message: 'Enter Actual Budget password (optional):',
78
+ mask: '*',
79
+ },
80
+ ]);
81
+ return answers;
82
+ }
83
+ async showConnectionStatus(status) {
84
+ console.log(chalk_1.default.blue('\nšŸ”— Connection Status\n'));
85
+ const lfStatus = status.lunchFlow ? chalk_1.default.green('āœ… Connected') : chalk_1.default.red('āŒ Disconnected');
86
+ const abStatus = status.actualBudget ? chalk_1.default.green('āœ… Connected') : chalk_1.default.red('āŒ Disconnected');
87
+ console.log(`Lunch Flow: ${lfStatus}`);
88
+ console.log(`Actual Budget: ${abStatus}\n`);
89
+ }
90
+ async showAccountsTable(accounts, title) {
91
+ console.log(chalk_1.default.blue(`\n${title}\n`));
92
+ if (accounts.length === 0) {
93
+ console.log(chalk_1.default.yellow('No accounts found.\n'));
94
+ return;
95
+ }
96
+ const table = new cli_table3_1.default({
97
+ head: ['ID', 'Name'],
98
+ colWidths: [8, 25],
99
+ style: {
100
+ head: ['cyan'],
101
+ border: ['gray'],
102
+ }
103
+ });
104
+ accounts.forEach(account => {
105
+ table.push([
106
+ account.id.toString().substring(0, 8) + '...',
107
+ account.name,
108
+ ]);
109
+ });
110
+ console.log(table.toString());
111
+ }
112
+ async configureAccountMappings(lfAccounts, abAccounts) {
113
+ console.log(chalk_1.default.yellow('\nšŸ“‹ Configure Account Mappings\n'));
114
+ console.log(chalk_1.default.gray('Map each Lunch Flow account to an Actual Budget account:\n'));
115
+ const mappings = [];
116
+ for (const lfAccount of lfAccounts) {
117
+ const choices = abAccounts.map(abAccount => ({
118
+ name: `${abAccount.name} (${abAccount.currency})`,
119
+ value: abAccount.id,
120
+ }));
121
+ const answer = await inquirer_1.default.prompt([
122
+ {
123
+ type: 'list',
124
+ name: 'abAccountId',
125
+ message: `Map "${lfAccount.name}" (${lfAccount.institution_name}) to:`,
126
+ choices: [
127
+ { name: 'Skip this account', value: 'skip' },
128
+ ...choices,
129
+ ],
130
+ },
131
+ ]);
132
+ if (answer.abAccountId !== 'skip') {
133
+ const abAccount = abAccounts.find(a => a.id === answer.abAccountId);
134
+ if (abAccount) {
135
+ mappings.push({
136
+ lunchFlowAccountId: lfAccount.id,
137
+ lunchFlowAccountName: lfAccount.name,
138
+ actualBudgetAccountId: abAccount.id,
139
+ actualBudgetAccountName: abAccount.name,
140
+ });
141
+ }
142
+ }
143
+ }
144
+ return mappings;
145
+ }
146
+ async showAccountMappings(mappings) {
147
+ console.log(chalk_1.default.blue('\nšŸ“‹ Current Account Mappings\n'));
148
+ if (mappings.length === 0) {
149
+ console.log(chalk_1.default.yellow('No account mappings configured.\n'));
150
+ return;
151
+ }
152
+ const table = new cli_table3_1.default({
153
+ head: ['Lunch Flow Account', '→', 'Actual Budget Account'],
154
+ colWidths: [25, 3, 25],
155
+ style: {
156
+ head: ['cyan'],
157
+ border: ['gray'],
158
+ }
159
+ });
160
+ mappings.forEach(mapping => {
161
+ table.push([
162
+ mapping.lunchFlowAccountName,
163
+ '→',
164
+ mapping.actualBudgetAccountName
165
+ ]);
166
+ });
167
+ console.log(table.toString());
168
+ }
169
+ async confirmImport(transactionCount, dateRange) {
170
+ console.log(chalk_1.default.yellow('\nāš ļø Import Confirmation\n'));
171
+ console.log(`Date Range: ${dateRange.startDate} to ${dateRange.endDate}`);
172
+ console.log(`Transactions to import: ${transactionCount}\n`);
173
+ const answer = await inquirer_1.default.prompt([
174
+ {
175
+ type: 'confirm',
176
+ name: 'confirm',
177
+ message: 'Proceed with import?',
178
+ default: true,
179
+ },
180
+ ]);
181
+ return answer.confirm;
182
+ }
183
+ async showMainMenu() {
184
+ console.log(chalk_1.default.blue('\nšŸŽÆ Main Menu\n'));
185
+ const answer = await inquirer_1.default.prompt([
186
+ {
187
+ type: 'list',
188
+ name: 'action',
189
+ message: 'What would you like to do?',
190
+ choices: [
191
+ { name: 'šŸ”— Test connections', value: 'test' },
192
+ { name: 'šŸ“‹ List available budgets', value: 'list-budgets' },
193
+ { name: 'šŸ“‹ Configure account mappings', value: 'configure' },
194
+ { name: 'šŸ“Š Show current mappings', value: 'show' },
195
+ { name: 'šŸ“„ Import transactions', value: 'import' },
196
+ { name: 'āš™ļø Reconfigure credentials', value: 'reconfigure' },
197
+ { name: 'āŒ Exit', value: 'exit' },
198
+ ],
199
+ },
200
+ ]);
201
+ return answer.action;
202
+ }
203
+ async showReconfigureMenu() {
204
+ console.log(chalk_1.default.yellow('\nāš™ļø Reconfigure Credentials\n'));
205
+ const answer = await inquirer_1.default.prompt([
206
+ {
207
+ type: 'list',
208
+ name: 'action',
209
+ message: 'What would you like to reconfigure?',
210
+ choices: [
211
+ { name: 'Lunch Flow credentials', value: 'lunchflow' },
212
+ { name: 'Actual Budget credentials', value: 'actualbudget' },
213
+ { name: 'Both', value: 'both' },
214
+ { name: 'Cancel', value: 'cancel' },
215
+ ],
216
+ },
217
+ ]);
218
+ return answer.action;
219
+ }
220
+ showSpinner(message) {
221
+ return (0, ora_1.default)(message).start();
222
+ }
223
+ showSuccess(message) {
224
+ console.log(chalk_1.default.green(`āœ… ${message}`));
225
+ }
226
+ showError(message) {
227
+ console.log(chalk_1.default.red(`āŒ ${message}`));
228
+ }
229
+ showInfo(message) {
230
+ console.log(chalk_1.default.blue(`ā„¹ļø ${message}`));
231
+ }
232
+ showWarning(message) {
233
+ console.log(chalk_1.default.yellow(`āš ļø ${message}`));
234
+ }
235
+ async showTransactionPreview(transactions, accounts, count = 10) {
236
+ console.log(chalk_1.default.blue(`\nšŸ“Š Transaction Preview (showing first ${Math.min(count, transactions.length)})\n`));
237
+ if (transactions.length === 0) {
238
+ console.log(chalk_1.default.yellow('No transactions to preview.\n'));
239
+ return;
240
+ }
241
+ const accountNames = accounts.reduce((acc, account) => {
242
+ acc[account.id] = account.name;
243
+ return acc;
244
+ }, {});
245
+ const table = new cli_table3_1.default({
246
+ head: ['Date', 'Description', 'Amount', 'Account'],
247
+ colWidths: [12, 30, 12, 20],
248
+ style: {
249
+ head: ['cyan'],
250
+ border: ['gray'],
251
+ }
252
+ });
253
+ transactions
254
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
255
+ .slice(0, count)
256
+ .forEach(transaction => {
257
+ const amount = transaction.amount / 100;
258
+ const amountDisplay = transaction.amount >= 0
259
+ ? chalk_1.default.green(`+${amount.toFixed(2)}`)
260
+ : chalk_1.default.red(`-${Math.abs(amount).toFixed(2)}`);
261
+ table.push([
262
+ transaction.date,
263
+ transaction.imported_payee,
264
+ amountDisplay,
265
+ accountNames[transaction.account] || 'Unknown'
266
+ ]);
267
+ });
268
+ console.log(table.toString());
269
+ if (transactions.length > count) {
270
+ console.log(chalk_1.default.gray(`... and ${transactions.length - count} more transactions\n`));
271
+ }
272
+ else {
273
+ console.log();
274
+ }
275
+ }
276
+ }
277
+ exports.TerminalUI = TerminalUI;
278
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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"}
package/install.sh ADDED
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+
3
+ echo "šŸ½ļø Installing Lunch Flow → Actual Budget Importer"
4
+ echo "=================================================="
5
+
6
+ # Check if Node.js is installed
7
+ if ! command -v node &> /dev/null; then
8
+ echo "āŒ Node.js is not installed. Please install Node.js 16+ first."
9
+ echo " Visit: https://nodejs.org/"
10
+ exit 1
11
+ fi
12
+
13
+ # Check Node.js version
14
+ NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
15
+ if [ "$NODE_VERSION" -lt 16 ]; then
16
+ echo "āŒ Node.js version 16+ is required. Current version: $(node -v)"
17
+ exit 1
18
+ fi
19
+
20
+ echo "āœ… Node.js $(node -v) detected"
21
+
22
+ # Check if pnpm is installed
23
+ if ! command -v pnpm &> /dev/null; then
24
+ echo "āŒ pnpm is not installed. Please install pnpm first."
25
+ echo " Run: npm install -g pnpm"
26
+ echo " Or visit: https://pnpm.io/installation"
27
+ exit 1
28
+ fi
29
+
30
+ echo "āœ… pnpm $(pnpm --version) detected"
31
+
32
+ # Install dependencies
33
+ echo "šŸ“¦ Installing dependencies..."
34
+ pnpm install
35
+
36
+ if [ $? -ne 0 ]; then
37
+ echo "āŒ Failed to install dependencies"
38
+ exit 1
39
+ fi
40
+
41
+ # Build the project
42
+ echo "šŸ”Ø Building project..."
43
+ pnpm run build
44
+
45
+ if [ $? -ne 0 ]; then
46
+ echo "āŒ Failed to build project"
47
+ exit 1
48
+ fi
49
+
50
+ echo "āœ… Installation complete!"
51
+ echo ""
52
+ echo "šŸš€ To get started, run:"
53
+ echo " pnpm start"
54
+ echo ""
55
+ echo "šŸ“– For more information, see README.md"
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lunchflow/actual-flow",
3
+ "version": "0.0.1",
4
+ "description": "Import transactions from Lunch Flow to Actual Budget with terminal UI.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "actual-flow": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node src/index.ts",
13
+ "prepare": "pnpm run build",
14
+ "install:dev": "pnpm install",
15
+ "clean": "rm -rf dist"
16
+ },
17
+ "keywords": ["lunch-flow", "actual-budget", "importer", "transactions"],
18
+ "author": "Amr Awad",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@actual-app/api": "^25.9.0",
22
+ "inquirer": "^8.2.6",
23
+ "chalk": "^4.1.2",
24
+ "ora": "^5.4.1",
25
+ "axios": "^1.6.0",
26
+ "dotenv": "^16.3.1",
27
+ "cli-table3": "^0.6.3"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "@types/inquirer": "^9.0.7",
32
+ "typescript": "^5.0.0",
33
+ "ts-node": "^10.9.0"
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - '.'
@@ -0,0 +1,132 @@
1
+ import * as actualAPI from '@actual-app/api';
2
+ import { ActualBudgetTransaction, ActualBudgetAccount } from './types';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ export class ActualBudgetClient {
7
+ private serverUrl: string;
8
+ private budgetSyncId: string;
9
+ private password?: string;
10
+ private connected: boolean = false;
11
+ private dataDir: string;
12
+
13
+ constructor(serverUrl: string, budgetSyncId: string, password?: string) {
14
+ this.serverUrl = serverUrl;
15
+ this.budgetSyncId = budgetSyncId;
16
+ this.password = password;
17
+ this.dataDir = './actual-data'; // Local cache directory
18
+ }
19
+
20
+ async connect(): Promise<boolean> {
21
+ try {
22
+ // Ensure data directory exists
23
+ if (!fs.existsSync(this.dataDir)) {
24
+ fs.mkdirSync(this.dataDir, { recursive: true });
25
+ }
26
+
27
+ await actualAPI.init({
28
+ dataDir: this.dataDir,
29
+ serverURL: this.serverUrl,
30
+ password: this.password,
31
+ });
32
+
33
+ // Download the budget to local cache
34
+ await actualAPI.downloadBudget(this.budgetSyncId);
35
+
36
+ this.connected = true;
37
+ return true;
38
+ } catch (error: any) {
39
+ if (error.message.includes('budget directory does not exist')) {
40
+ console.error(`Budget with sync ID "${this.budgetSyncId}" does not exist on the server.`);
41
+ console.error('Please check your budget sync ID and try again.');
42
+ } else if (error.message.includes('ECONNREFUSED')) {
43
+ console.error(`Cannot connect to Actual Budget server at ${this.serverUrl}`);
44
+ console.error('Please check that your Actual Budget server is running.');
45
+ } else if (error.message.includes('not found')) {
46
+ console.error(`Budget with sync ID "${this.budgetSyncId}" not found on the server.`);
47
+ console.error('Please verify your budget sync ID in Actual Budget settings.');
48
+ } else {
49
+ console.error('Failed to connect to Actual Budget:', error.message);
50
+ }
51
+ this.connected = false;
52
+ return false;
53
+ }
54
+ }
55
+
56
+ async testConnection(): Promise<boolean> {
57
+ try {
58
+ await this.connect();
59
+ return this.connected;
60
+ } catch (error: any) {
61
+ console.error('Actual Budget connection test failed:', error.message);
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async getAccounts(): Promise<ActualBudgetAccount[]> {
67
+ if (!this.connected) {
68
+ await this.connect();
69
+ }
70
+
71
+ try {
72
+ const accounts = await actualAPI.getAccounts();
73
+ return accounts.map((account: any) => ({
74
+ id: account.id,
75
+ name: account.name,
76
+ type: account.type as any,
77
+ balance: actualAPI.utils.integerToAmount(account.balance || 0),
78
+ currency: 'USD', // Assuming USD, adjust as needed
79
+ }));
80
+ } catch (error: any) {
81
+ console.error('Failed to fetch Actual Budget accounts:', error.message);
82
+ throw new Error(`Failed to fetch accounts: ${error.message}`);
83
+ }
84
+ }
85
+
86
+ async importTransactions(transactions: ActualBudgetTransaction[]): Promise<void> {
87
+ if (!this.connected) {
88
+ await this.connect();
89
+ }
90
+
91
+ try {
92
+ const result = await actualAPI.importTransactions(transactions[0].account, transactions);
93
+ console.log('Transactions imported successfully:', result);
94
+ } catch (error: any) {
95
+ console.error('Failed to import transactions to Actual Budget:', error.message);
96
+ throw new Error(`Failed to import transactions: ${error.message}`);
97
+ }
98
+ }
99
+
100
+ async listAvailableBudgets(): Promise<{ id: string; name: string }[]> {
101
+ try {
102
+ // Ensure data directory exists
103
+ if (!fs.existsSync(this.dataDir)) {
104
+ fs.mkdirSync(this.dataDir, { recursive: true });
105
+ }
106
+
107
+ await actualAPI.init({
108
+ dataDir: this.dataDir,
109
+ serverURL: this.serverUrl,
110
+ password: this.password,
111
+ });
112
+
113
+ const budgets = await actualAPI.getBudgets();
114
+ return budgets.map((budget: any) => ({
115
+ id: budget.id,
116
+ name: budget.name || 'Unnamed Budget'
117
+ }));
118
+ } catch (error: any) {
119
+ console.error('Failed to fetch available budgets:', error.message);
120
+ throw new Error(`Failed to fetch budgets: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ async shutdown(): Promise<void> {
125
+ try {
126
+ await actualAPI.shutdown();
127
+ this.connected = false;
128
+ } catch (error: any) {
129
+ console.error('Error during shutdown:', error.message);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,128 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Config, AccountMapping } from './types';
4
+
5
+ export class ConfigManager {
6
+ private configPath: string;
7
+
8
+ constructor(configPath: string = path.join(process.cwd(), 'config.json')) {
9
+ this.configPath = configPath;
10
+ }
11
+
12
+ loadConfig(): Config | null {
13
+ try {
14
+ if (!fs.existsSync(this.configPath)) {
15
+ return null;
16
+ }
17
+ const configData = fs.readFileSync(this.configPath, 'utf8');
18
+ const config = JSON.parse(configData);
19
+
20
+ // Validate config structure
21
+ if (!this.validateConfig(config)) {
22
+ console.warn('Invalid config file structure, will recreate');
23
+ return null;
24
+ }
25
+
26
+ return config;
27
+ } catch (error) {
28
+ console.error('Failed to load config:', error);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ saveConfig(config: Config): void {
34
+ try {
35
+ // Ensure directory exists
36
+ const dir = path.dirname(this.configPath);
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+
41
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
42
+ } catch (error) {
43
+ console.error('Failed to save config:', error);
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ createDefaultConfig(): Config {
49
+ return {
50
+ lunchFlow: {
51
+ apiKey: '',
52
+ baseUrl: 'https://api.lunchflow.com',
53
+ },
54
+ actualBudget: {
55
+ serverUrl: '',
56
+ budgetSyncId: '',
57
+ password: '',
58
+ },
59
+ accountMappings: [],
60
+ };
61
+ }
62
+
63
+ updateLunchFlowConfig(apiKey: string, baseUrl: string): void {
64
+ const config = this.loadConfig() || this.createDefaultConfig();
65
+ config.lunchFlow = { apiKey, baseUrl };
66
+ this.saveConfig(config);
67
+ }
68
+
69
+ updateActualBudgetConfig(serverUrl: string, budgetSyncId: string, password?: string): void {
70
+ const config = this.loadConfig() || this.createDefaultConfig();
71
+ config.actualBudget = { serverUrl, budgetSyncId, password };
72
+ this.saveConfig(config);
73
+ }
74
+
75
+ updateAccountMappings(mappings: AccountMapping[]): void {
76
+ const config = this.loadConfig();
77
+ if (config) {
78
+ config.accountMappings = mappings;
79
+ this.saveConfig(config);
80
+ }
81
+ }
82
+
83
+ private validateConfig(config: any): boolean {
84
+ return (
85
+ config &&
86
+ typeof config === 'object' &&
87
+ config.lunchFlow &&
88
+ typeof config.lunchFlow.apiKey === 'string' &&
89
+ typeof config.lunchFlow.baseUrl === 'string' &&
90
+ config.actualBudget &&
91
+ typeof config.actualBudget.serverUrl === 'string' &&
92
+ typeof config.actualBudget.budgetSyncId === 'string' &&
93
+ Array.isArray(config.accountMappings)
94
+ );
95
+ }
96
+
97
+ getConfigPath(): string {
98
+ return this.configPath;
99
+ }
100
+
101
+ // Check if config exists and has required fields
102
+ isConfigured(): boolean {
103
+ const config = this.loadConfig();
104
+ return config !== null &&
105
+ config.lunchFlow.apiKey.length > 0 &&
106
+ config.actualBudget.serverUrl.length > 0 &&
107
+ config.actualBudget.budgetSyncId.length > 0;
108
+ }
109
+
110
+ // Get a safe version of config for display (hides sensitive data)
111
+ getSafeConfig(): Partial<Config> | null {
112
+ const config = this.loadConfig();
113
+ if (!config) return null;
114
+
115
+ return {
116
+ lunchFlow: {
117
+ apiKey: config.lunchFlow.apiKey ? '***' + config.lunchFlow.apiKey.slice(-4) : '',
118
+ baseUrl: config.lunchFlow.baseUrl,
119
+ },
120
+ actualBudget: {
121
+ serverUrl: config.actualBudget.serverUrl,
122
+ budgetSyncId: config.actualBudget.budgetSyncId,
123
+ password: config.actualBudget.password ? '***' : '',
124
+ },
125
+ accountMappings: config.accountMappings,
126
+ };
127
+ }
128
+ }