@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/src/ui.ts ADDED
@@ -0,0 +1,314 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import Table from 'cli-table3';
5
+ import { LunchFlowAccount, ActualBudgetAccount, AccountMapping, ConnectionStatus, ActualBudgetTransaction } from './types';
6
+
7
+ export class TerminalUI {
8
+ async showWelcome(): Promise<void> {
9
+ console.clear();
10
+ console.log(chalk.blue.bold('\nšŸ½ļø Lunch Flow → Actual Budget Importer\n'));
11
+ console.log(chalk.gray('This tool helps you import transactions from Lunch Flow to Actual Budget\n'));
12
+ }
13
+
14
+ async getLunchFlowCredentials(): Promise<{ apiKey: string; baseUrl: string }> {
15
+ console.log(chalk.yellow('šŸ“” Lunch Flow Configuration\n'));
16
+
17
+ const answers = await inquirer.prompt([
18
+ {
19
+ type: 'input',
20
+ name: 'apiKey',
21
+ message: 'Enter your Lunch Flow API key:',
22
+ validate: (input: string) => input.length > 0 || 'API key is required',
23
+ },
24
+ {
25
+ type: 'input',
26
+ name: 'baseUrl',
27
+ message: 'Enter Lunch Flow API base URL:',
28
+ default: 'https://api.lunchflow.com',
29
+ validate: (input: string) => {
30
+ try {
31
+ new URL(input);
32
+ return true;
33
+ } catch {
34
+ return 'Please enter a valid URL';
35
+ }
36
+ },
37
+ },
38
+ ]);
39
+ return answers;
40
+ }
41
+
42
+ async getActualBudgetCredentials(): Promise<{ serverUrl: string; budgetSyncId: string; password?: string }> {
43
+ console.log(chalk.yellow('\nšŸ’° Actual Budget Configuration\n'));
44
+ console.log(chalk.gray('To find your budget sync ID:'));
45
+ console.log(chalk.gray('1. Open Actual Budget in your browser'));
46
+ console.log(chalk.gray('2. Go to Settings → Show advanced settings'));
47
+ console.log(chalk.gray('3. Look for "Sync ID" - that\'s your budget sync ID'));
48
+ console.log(chalk.gray('4. Or check the URL: http://localhost:5007/budget/your-sync-id'));
49
+ console.log(chalk.gray('5. The sync ID is the part after "/budget/"\n'));
50
+
51
+ const answers = await inquirer.prompt([
52
+ {
53
+ type: 'input',
54
+ name: 'serverUrl',
55
+ message: 'Enter Actual Budget server URL:',
56
+ default: 'http://localhost:5007',
57
+ validate: (input: string) => {
58
+ try {
59
+ new URL(input);
60
+ return true;
61
+ } catch {
62
+ return 'Please enter a valid URL';
63
+ }
64
+ },
65
+ },
66
+ {
67
+ type: 'input',
68
+ name: 'budgetSyncId',
69
+ message: 'Enter Actual Budget budget sync ID:',
70
+ validate: (input: string) => input.length > 0 || 'Budget sync ID is required',
71
+ },
72
+ {
73
+ type: 'password',
74
+ name: 'password',
75
+ message: 'Enter Actual Budget password (optional):',
76
+ mask: '*',
77
+ },
78
+ ]);
79
+ return answers;
80
+ }
81
+
82
+ async showConnectionStatus(status: ConnectionStatus): Promise<void> {
83
+ console.log(chalk.blue('\nšŸ”— Connection Status\n'));
84
+
85
+ const lfStatus = status.lunchFlow ? chalk.green('āœ… Connected') : chalk.red('āŒ Disconnected');
86
+ const abStatus = status.actualBudget ? chalk.green('āœ… Connected') : chalk.red('āŒ Disconnected');
87
+
88
+ console.log(`Lunch Flow: ${lfStatus}`);
89
+ console.log(`Actual Budget: ${abStatus}\n`);
90
+ }
91
+
92
+ async showAccountsTable(accounts: (LunchFlowAccount | ActualBudgetAccount)[], title: string): Promise<void> {
93
+ console.log(chalk.blue(`\n${title}\n`));
94
+
95
+ if (accounts.length === 0) {
96
+ console.log(chalk.yellow('No accounts found.\n'));
97
+ return;
98
+ }
99
+
100
+ const table = new Table({
101
+ head: ['ID', 'Name'],
102
+ colWidths: [8, 25],
103
+ style: {
104
+ head: ['cyan'],
105
+ border: ['gray'],
106
+ }
107
+ });
108
+
109
+ accounts.forEach(account => {
110
+ table.push([
111
+ account.id.toString().substring(0, 8) + '...',
112
+ account.name,
113
+ ]);
114
+ });
115
+
116
+ console.log(table.toString());
117
+ }
118
+
119
+ async configureAccountMappings(
120
+ lfAccounts: LunchFlowAccount[],
121
+ abAccounts: ActualBudgetAccount[]
122
+ ): Promise<AccountMapping[]> {
123
+ console.log(chalk.yellow('\nšŸ“‹ Configure Account Mappings\n'));
124
+ console.log(chalk.gray('Map each Lunch Flow account to an Actual Budget account:\n'));
125
+
126
+ const mappings: AccountMapping[] = [];
127
+
128
+ for (const lfAccount of lfAccounts) {
129
+ const choices = abAccounts.map(abAccount => ({
130
+ name: `${abAccount.name} (${abAccount.currency})`,
131
+ value: abAccount.id,
132
+ }));
133
+
134
+ const answer = await inquirer.prompt([
135
+ {
136
+ type: 'list',
137
+ name: 'abAccountId',
138
+ message: `Map "${lfAccount.name}" (${lfAccount.institution_name}) to:`,
139
+ choices: [
140
+ { name: 'Skip this account', value: 'skip' },
141
+ ...choices,
142
+ ],
143
+ },
144
+ ]);
145
+
146
+ if (answer.abAccountId !== 'skip') {
147
+ const abAccount = abAccounts.find(a => a.id === answer.abAccountId);
148
+ if (abAccount) {
149
+ mappings.push({
150
+ lunchFlowAccountId: lfAccount.id,
151
+ lunchFlowAccountName: lfAccount.name,
152
+ actualBudgetAccountId: abAccount.id,
153
+ actualBudgetAccountName: abAccount.name,
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ return mappings;
160
+ }
161
+
162
+ async showAccountMappings(mappings: AccountMapping[]): Promise<void> {
163
+ console.log(chalk.blue('\nšŸ“‹ Current Account Mappings\n'));
164
+
165
+ if (mappings.length === 0) {
166
+ console.log(chalk.yellow('No account mappings configured.\n'));
167
+ return;
168
+ }
169
+
170
+ const table = new Table({
171
+ head: ['Lunch Flow Account', '→', 'Actual Budget Account'],
172
+ colWidths: [25, 3, 25],
173
+ style: {
174
+ head: ['cyan'],
175
+ border: ['gray'],
176
+ }
177
+ });
178
+
179
+ mappings.forEach(mapping => {
180
+ table.push([
181
+ mapping.lunchFlowAccountName,
182
+ '→',
183
+ mapping.actualBudgetAccountName
184
+ ]);
185
+ });
186
+
187
+ console.log(table.toString());
188
+ }
189
+
190
+ async confirmImport(transactionCount: number, dateRange: { startDate: string; endDate: string }): Promise<boolean> {
191
+ console.log(chalk.yellow('\nāš ļø Import Confirmation\n'));
192
+ console.log(`Date Range: ${dateRange.startDate} to ${dateRange.endDate}`);
193
+ console.log(`Transactions to import: ${transactionCount}\n`);
194
+
195
+ const answer = await inquirer.prompt([
196
+ {
197
+ type: 'confirm',
198
+ name: 'confirm',
199
+ message: 'Proceed with import?',
200
+ default: true,
201
+ },
202
+ ]);
203
+ return answer.confirm;
204
+ }
205
+
206
+ async showMainMenu(): Promise<string> {
207
+ console.log(chalk.blue('\nšŸŽÆ Main Menu\n'));
208
+
209
+ const answer = await inquirer.prompt([
210
+ {
211
+ type: 'list',
212
+ name: 'action',
213
+ message: 'What would you like to do?',
214
+ choices: [
215
+ { name: 'šŸ”— Test connections', value: 'test' },
216
+ { name: 'šŸ“‹ List available budgets', value: 'list-budgets' },
217
+ { name: 'šŸ“‹ Configure account mappings', value: 'configure' },
218
+ { name: 'šŸ“Š Show current mappings', value: 'show' },
219
+ { name: 'šŸ“„ Import transactions', value: 'import' },
220
+ { name: 'āš™ļø Reconfigure credentials', value: 'reconfigure' },
221
+ { name: 'āŒ Exit', value: 'exit' },
222
+ ],
223
+ },
224
+ ]);
225
+ return answer.action;
226
+ }
227
+
228
+ async showReconfigureMenu(): Promise<string> {
229
+ console.log(chalk.yellow('\nāš™ļø Reconfigure Credentials\n'));
230
+
231
+ const answer = await inquirer.prompt([
232
+ {
233
+ type: 'list',
234
+ name: 'action',
235
+ message: 'What would you like to reconfigure?',
236
+ choices: [
237
+ { name: 'Lunch Flow credentials', value: 'lunchflow' },
238
+ { name: 'Actual Budget credentials', value: 'actualbudget' },
239
+ { name: 'Both', value: 'both' },
240
+ { name: 'Cancel', value: 'cancel' },
241
+ ],
242
+ },
243
+ ]);
244
+ return answer.action;
245
+ }
246
+
247
+ showSpinner(message: string): any {
248
+ return ora(message).start();
249
+ }
250
+
251
+ showSuccess(message: string): void {
252
+ console.log(chalk.green(`āœ… ${message}`));
253
+ }
254
+
255
+ showError(message: string): void {
256
+ console.log(chalk.red(`āŒ ${message}`));
257
+ }
258
+
259
+ showInfo(message: string): void {
260
+ console.log(chalk.blue(`ā„¹ļø ${message}`));
261
+ }
262
+
263
+ showWarning(message: string): void {
264
+ console.log(chalk.yellow(`āš ļø ${message}`));
265
+ }
266
+
267
+ async showTransactionPreview(transactions: ActualBudgetTransaction[], accounts: ActualBudgetAccount[], count: number = 10): Promise<void> {
268
+ console.log(chalk.blue(`\nšŸ“Š Transaction Preview (showing first ${Math.min(count, transactions.length)})\n`));
269
+
270
+ if (transactions.length === 0) {
271
+ console.log(chalk.yellow('No transactions to preview.\n'));
272
+ return;
273
+ }
274
+
275
+ const accountNames = accounts.reduce((acc, account) => {
276
+ acc[account.id] = account.name;
277
+ return acc;
278
+ }, {} as Record<string, string>);
279
+
280
+ const table = new Table({
281
+ head: ['Date', 'Description', 'Amount', 'Account'],
282
+ colWidths: [12, 30, 12, 20],
283
+ style: {
284
+ head: ['cyan'],
285
+ border: ['gray'],
286
+ }
287
+ });
288
+
289
+ transactions
290
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
291
+ .slice(0, count)
292
+ .forEach(transaction => {
293
+ const amount = transaction.amount / 100;
294
+ const amountDisplay = transaction.amount >= 0
295
+ ? chalk.green(`+${amount.toFixed(2)}`)
296
+ : chalk.red(`-${Math.abs(amount).toFixed(2)}`);
297
+
298
+ table.push([
299
+ transaction.date,
300
+ transaction.imported_payee,
301
+ amountDisplay,
302
+ accountNames[transaction.account] || 'Unknown'
303
+ ]);
304
+ });
305
+
306
+ console.log(table.toString());
307
+
308
+ if (transactions.length > count) {
309
+ console.log(chalk.gray(`... and ${transactions.length - count} more transactions\n`));
310
+ } else {
311
+ console.log();
312
+ }
313
+ }
314
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020", "DOM"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }