@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.
- package/README.md +196 -0
- package/dist/actual-budget-client.d.ts +19 -0
- package/dist/actual-budget-client.d.ts.map +1 -0
- package/dist/actual-budget-client.js +161 -0
- package/dist/actual-budget-client.js.map +1 -0
- package/dist/config-manager.d.ts +16 -0
- package/dist/config-manager.d.ts.map +1 -0
- package/dist/config-manager.js +119 -0
- package/dist/config-manager.js.map +1 -0
- package/dist/importer.d.ts +20 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/importer.js +234 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lunch-flow-client.d.ts +10 -0
- package/dist/lunch-flow-client.d.ts.map +1 -0
- package/dist/lunch-flow-client.js +67 -0
- package/dist/lunch-flow-client.js.map +1 -0
- package/dist/transaction-mapper.d.ts +8 -0
- package/dist/transaction-mapper.d.ts.map +1 -0
- package/dist/transaction-mapper.js +32 -0
- package/dist/transaction-mapper.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +30 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +278 -0
- package/dist/ui.js.map +1 -0
- package/install.sh +55 -0
- package/package.json +35 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/actual-budget-client.ts +132 -0
- package/src/config-manager.ts +128 -0
- package/src/importer.ts +279 -0
- package/src/index.ts +28 -0
- package/src/lunch-flow-client.ts +64 -0
- package/src/transaction-mapper.ts +37 -0
- package/src/types.ts +60 -0
- package/src/ui.ts +314 -0
- 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
|
+
}
|