simplefin-cli 0.1.0
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/PLAN.md +104 -0
- package/README.md +88 -0
- package/bin/simplefin.js +442 -0
- package/package.json +36 -0
- package/publish.sh +53 -0
- package/test.js +241 -0
package/PLAN.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SimpleFIN CLI Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
CLI tool for accessing financial data via SimpleFIN Bridge (multi-bank support)
|
|
5
|
+
|
|
6
|
+
## Author
|
|
7
|
+
dilllxd
|
|
8
|
+
|
|
9
|
+
## License
|
|
10
|
+
MIT + Commons Clause
|
|
11
|
+
|
|
12
|
+
## Module Path
|
|
13
|
+
github.com/dilllxd/simplefin-cli
|
|
14
|
+
|
|
15
|
+
## Dependencies
|
|
16
|
+
- native Go `net/http`
|
|
17
|
+
- charm.sh/gum for pretty output
|
|
18
|
+
- viper for config (TOML)
|
|
19
|
+
|
|
20
|
+
## Config Location
|
|
21
|
+
~/.config/simplefin-cli/config.toml
|
|
22
|
+
|
|
23
|
+
## Multi-Account Support
|
|
24
|
+
No - single account, but links multiple banks internally
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
### Connection
|
|
29
|
+
```bash
|
|
30
|
+
simplefin connect # Interactive setup token prompt
|
|
31
|
+
simplefin connect --token "..." # Non-interactive
|
|
32
|
+
simplefin status # Check connection status
|
|
33
|
+
simplefin disconnect # Remove all bank connections
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Accounts
|
|
37
|
+
```bash
|
|
38
|
+
simplefin accounts list # List all linked accounts
|
|
39
|
+
simplefin accounts get <account-id> # Get account details
|
|
40
|
+
simplefin accounts balances # Get all current balances
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Transactions
|
|
44
|
+
```bash
|
|
45
|
+
simplefin transactions # Recent transactions (default 30 days)
|
|
46
|
+
simplefin transactions --days 7
|
|
47
|
+
simplefin transactions --days 90
|
|
48
|
+
simplefin transactions --account <account-id>
|
|
49
|
+
simplefin transactions --format json
|
|
50
|
+
simplefin transactions-by-date --start 2026-01-01 --end 2026-01-31
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Sync
|
|
54
|
+
```bash
|
|
55
|
+
simplefin sync # Force refresh from all banks
|
|
56
|
+
simplefin sync --account <account-id> # Refresh specific account
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Config File
|
|
60
|
+
```toml
|
|
61
|
+
[connection]
|
|
62
|
+
setup_token = "${SIMPLEFIN_SETUP_TOKEN}"
|
|
63
|
+
access_url = "${SIMPLEFIN_ACCESS_URL}" # Optional, overrides token
|
|
64
|
+
|
|
65
|
+
[output]
|
|
66
|
+
format = "table"
|
|
67
|
+
colors = true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment Variables
|
|
71
|
+
- SIMPLEFIN_SETUP_TOKEN
|
|
72
|
+
- SIMPLEFIN_ACCESS_URL
|
|
73
|
+
|
|
74
|
+
## Storage
|
|
75
|
+
~/.config/simplefin-cli/connection.json:
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"access_url": "https://bridge.simplefin.org/simplefin/...",
|
|
79
|
+
"created_at": "2026-01-10T00:00:00Z",
|
|
80
|
+
"last_sync": "2026-01-10T12:00:00Z"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Auth Flow
|
|
85
|
+
1. User visits https://bridge.simplefin.org/simplefin/create
|
|
86
|
+
2. Gets a setup token
|
|
87
|
+
3. Runs `simplefin connect --token "..."`
|
|
88
|
+
4. CLI exchanges setup token for access URL
|
|
89
|
+
5. Access URL stored for future use
|
|
90
|
+
|
|
91
|
+
## Data Types
|
|
92
|
+
- Account (name, type, balance, currency, institution)
|
|
93
|
+
- Transaction (date, amount, description, category, account)
|
|
94
|
+
|
|
95
|
+
## Implementation Phases
|
|
96
|
+
1. Config + connection flow
|
|
97
|
+
2. Account listing and balance retrieval
|
|
98
|
+
3. Transaction retrieval (by days and date range)
|
|
99
|
+
4. Sync/refresh functionality
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
- Unit tests for API client
|
|
103
|
+
- Integration tests with real SimpleFIN connection
|
|
104
|
+
- Mock server for testing
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SimpleFIN CLI
|
|
2
|
+
|
|
3
|
+
CLI tool for accessing financial data via SimpleFIN Bridge.
|
|
4
|
+
|
|
5
|
+
## Author
|
|
6
|
+
dilllxd
|
|
7
|
+
|
|
8
|
+
## License
|
|
9
|
+
MIT
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g simplefin-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Connect to SimpleFIN Bridge
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
simplefin connect
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You'll need a setup token from https://bridge.simplefin.org/simplefin/create
|
|
26
|
+
|
|
27
|
+
### List Accounts
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
simplefin accounts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### View Transactions
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
simplefin transactions # Last 30 days
|
|
37
|
+
simplefin transactions --days 7 # Last 7 days
|
|
38
|
+
simplefin transactions --json # JSON output
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Disconnect
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
simplefin disconnect
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `simplefin connect` | Connect to SimpleFIN Bridge |
|
|
52
|
+
| `simplefin disconnect` | Disconnect and remove credentials |
|
|
53
|
+
| `simplefin status` | Check connection status |
|
|
54
|
+
| `simplefin accounts` | List connected accounts |
|
|
55
|
+
| `simplefin transactions` | List recent transactions |
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `--json` | Output as JSON (for all commands) |
|
|
62
|
+
| `-d, --days` | Number of days to look back (for transactions) |
|
|
63
|
+
| `-t, --token` | Setup token (for connect) |
|
|
64
|
+
|
|
65
|
+
## Environment Variables
|
|
66
|
+
|
|
67
|
+
- `SIMPLEFIN_SETUP_TOKEN` - Setup token for connect command
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Config stored in `~/.config/simplefin-cli/connection.json`
|
|
72
|
+
|
|
73
|
+
## Publishing to npm
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Log in to npm
|
|
77
|
+
npm login
|
|
78
|
+
|
|
79
|
+
# Publish
|
|
80
|
+
cd simplefin-cli
|
|
81
|
+
npm publish
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Testing
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node test.js
|
|
88
|
+
```
|
package/bin/simplefin.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
function getConfigDir() {
|
|
10
|
+
return path.join(os.homedir(), '.config', 'simplefin-cli');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getConnectionPath() {
|
|
14
|
+
return path.join(getConfigDir(), 'connection.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadConnection() {
|
|
18
|
+
try {
|
|
19
|
+
const data = fs.readFileSync(getConnectionPath(), 'utf-8');
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveConnection(accessUrl, username, password) {
|
|
27
|
+
fs.mkdirSync(getConfigDir(), { recursive: true });
|
|
28
|
+
const connection = {
|
|
29
|
+
access_url: accessUrl,
|
|
30
|
+
username,
|
|
31
|
+
password,
|
|
32
|
+
created_at: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(getConnectionPath(), JSON.stringify(connection, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function deleteConnection() {
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(getConnectionPath());
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseAuthFromUrl(accessUrl) {
|
|
46
|
+
const match = accessUrl.match(/:\/\/([^:]+):([^@]+)@/);
|
|
47
|
+
if (match) {
|
|
48
|
+
return {
|
|
49
|
+
username: match[1],
|
|
50
|
+
password: match[2],
|
|
51
|
+
baseUrl: accessUrl.replace(/:\/\/[^:]+:[^@]+@/, '://'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function claimAccessUrl(setupToken) {
|
|
58
|
+
const claimUrl = Buffer.from(setupToken, 'base64').toString('utf-8');
|
|
59
|
+
|
|
60
|
+
const response = await axios.post(claimUrl, '', {
|
|
61
|
+
headers: { 'Content-Length': '0' },
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (response.status === 403) {
|
|
66
|
+
throw new Error('Token is invalid or has already been used');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (response.status !== 200) {
|
|
70
|
+
throw new Error(`Unexpected response: ${response.status}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const accessUrl = response.data.trim();
|
|
74
|
+
if (!accessUrl) {
|
|
75
|
+
throw new Error('Empty response from server');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return accessUrl;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getInfo(baseUrl) {
|
|
82
|
+
const response = await axios.get(`${baseUrl}/info`);
|
|
83
|
+
return response.data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function getAccounts(options) {
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
|
|
89
|
+
if (options.startDate) params.append('start-date', options.startDate);
|
|
90
|
+
if (options.endDate) params.append('end-date', options.endDate);
|
|
91
|
+
if (options.pending) params.append('pending', '1');
|
|
92
|
+
if (options.account) params.append('account', options.account);
|
|
93
|
+
if (options.balancesOnly) params.append('balances-only', '1');
|
|
94
|
+
|
|
95
|
+
const url = `${options.baseUrl}/accounts${params.toString() ? '?' + params.toString() : ''}`;
|
|
96
|
+
|
|
97
|
+
const response = await axios.get(url, {
|
|
98
|
+
auth: { username: options.username, password: options.password },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (response.status === 403) {
|
|
102
|
+
throw new Error('Authentication failed - access may be revoked');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (response.status === 402) {
|
|
106
|
+
throw new Error('Payment required');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return response.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatDate(timestamp) {
|
|
113
|
+
if (!timestamp) return 'N/A';
|
|
114
|
+
const date = new Date(timestamp * 1000);
|
|
115
|
+
return date.toLocaleString('en-US', {
|
|
116
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
117
|
+
hour: 'numeric', minute: '2-digit', hour12: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatDateFull(timestamp) {
|
|
122
|
+
if (!timestamp) return 'N/A';
|
|
123
|
+
const date = new Date(timestamp * 1000);
|
|
124
|
+
return date.toLocaleString('en-US', {
|
|
125
|
+
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
|
|
126
|
+
hour: 'numeric', minute: '2-digit', hour12: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatDateShort(timestamp) {
|
|
131
|
+
if (!timestamp) return '';
|
|
132
|
+
const date = new Date(timestamp * 1000);
|
|
133
|
+
return date.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function truncate(str, len) {
|
|
137
|
+
if (!str) return '';
|
|
138
|
+
return str.length > len ? str.slice(0, len - 3) + '...' : str;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
program.name('simplefin').description('SimpleFIN CLI - Access your financial data');
|
|
142
|
+
|
|
143
|
+
// ===== INFO COMMAND =====
|
|
144
|
+
program.command('info')
|
|
145
|
+
.description('Get SimpleFIN server information')
|
|
146
|
+
.option('--json', 'Output as JSON')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
const conn = loadConnection();
|
|
149
|
+
if (!conn) {
|
|
150
|
+
console.log('Not connected');
|
|
151
|
+
console.log("Run 'simplefin connect' first");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const data = await getInfo(conn.access_url);
|
|
157
|
+
|
|
158
|
+
if (options.json) {
|
|
159
|
+
console.log(JSON.stringify(data, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('Server Info');
|
|
164
|
+
console.log('-----------');
|
|
165
|
+
console.log(`Supported versions: ${(data.versions || []).join(', ')}`);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error:', error.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ===== CONNECT COMMAND =====
|
|
173
|
+
program.command('connect')
|
|
174
|
+
.description('Connect to SimpleFIN Bridge')
|
|
175
|
+
.option('-t, --token <token>', 'Setup token (or use SIMPLEFIN_SETUP_TOKEN env var)')
|
|
176
|
+
.action(async (options) => {
|
|
177
|
+
let setupToken = options.token || process.env.SIMPLEFIN_SETUP_TOKEN;
|
|
178
|
+
|
|
179
|
+
if (!setupToken) {
|
|
180
|
+
console.log('Error: Setup token required');
|
|
181
|
+
console.log('Provide via:');
|
|
182
|
+
console.log(' -t, --token option: simplefin connect --token <token>');
|
|
183
|
+
console.log(' Environment variable: export SIMPLEFIN_SETUP_TOKEN=<token>');
|
|
184
|
+
console.log('\nGet a token at: https://bridge.simplefin.org/simplefin/create');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Connecting to SimpleFIN Bridge...');
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const accessUrl = await claimAccessUrl(setupToken);
|
|
192
|
+
const auth = parseAuthFromUrl(accessUrl);
|
|
193
|
+
if (!auth) {
|
|
194
|
+
throw new Error('Could not parse credentials from Access URL');
|
|
195
|
+
}
|
|
196
|
+
saveConnection(auth.baseUrl, auth.username, auth.password);
|
|
197
|
+
console.log('Connected successfully!');
|
|
198
|
+
console.log('\nNext steps:');
|
|
199
|
+
console.log(' simplefin info - Server info');
|
|
200
|
+
console.log(' simplefin accounts - List your accounts');
|
|
201
|
+
console.log(' simplefin transactions - View transactions');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Error:', error.message);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ===== DISCONNECT COMMAND =====
|
|
209
|
+
program.command('disconnect')
|
|
210
|
+
.description('Disconnect from SimpleFIN Bridge')
|
|
211
|
+
.action(() => {
|
|
212
|
+
deleteConnection();
|
|
213
|
+
console.log('Disconnected successfully');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ===== STATUS COMMAND =====
|
|
217
|
+
program.command('status')
|
|
218
|
+
.description('Check connection status')
|
|
219
|
+
.option('--json', 'Output as JSON')
|
|
220
|
+
.action((options) => {
|
|
221
|
+
const conn = loadConnection();
|
|
222
|
+
if (!conn) {
|
|
223
|
+
console.log('Not connected');
|
|
224
|
+
console.log("Run 'simplefin connect' first");
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (options.json) {
|
|
229
|
+
console.log(JSON.stringify({
|
|
230
|
+
connected: true,
|
|
231
|
+
server: conn.access_url,
|
|
232
|
+
created_at: conn.created_at,
|
|
233
|
+
}, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log('Connection Status');
|
|
238
|
+
console.log('-----------------');
|
|
239
|
+
console.log('Connected');
|
|
240
|
+
console.log(`Server: ${conn.access_url}`);
|
|
241
|
+
console.log(`Connected since: ${formatDate(new Date(conn.created_at).getTime() / 1000)}`);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ===== ACCOUNTS COMMAND =====
|
|
245
|
+
program.command('accounts')
|
|
246
|
+
.description('List all connected accounts')
|
|
247
|
+
.option('--json', 'Output as JSON')
|
|
248
|
+
.option('--balances-only', 'Only show balances, no transactions')
|
|
249
|
+
.action(async (options) => {
|
|
250
|
+
const conn = loadConnection();
|
|
251
|
+
if (!conn) {
|
|
252
|
+
console.log('Not connected');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const data = await getAccounts({
|
|
258
|
+
baseUrl: conn.access_url,
|
|
259
|
+
username: conn.username,
|
|
260
|
+
password: conn.password,
|
|
261
|
+
balancesOnly: options.balancesOnly,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (data.errors && data.errors.length > 0) {
|
|
265
|
+
console.log('Errors from server:');
|
|
266
|
+
for (const err of data.errors) {
|
|
267
|
+
console.log(` - ${err}`);
|
|
268
|
+
}
|
|
269
|
+
console.log('');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const accounts = data.accounts || [];
|
|
273
|
+
|
|
274
|
+
if (options.json) {
|
|
275
|
+
console.log(JSON.stringify({
|
|
276
|
+
connected: true,
|
|
277
|
+
errors: data.errors || [],
|
|
278
|
+
accounts: accounts.map(acc => ({
|
|
279
|
+
id: acc.id,
|
|
280
|
+
name: acc.name,
|
|
281
|
+
organization: acc.org?.name || acc.org?.domain || 'Unknown',
|
|
282
|
+
balance: acc.balance,
|
|
283
|
+
'available-balance': acc['available-balance'],
|
|
284
|
+
currency: acc.currency,
|
|
285
|
+
'balance-date': acc['balance-date'],
|
|
286
|
+
type: acc.type,
|
|
287
|
+
extra: acc.extra,
|
|
288
|
+
})),
|
|
289
|
+
total_accounts: accounts.length,
|
|
290
|
+
}, null, 2));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log('Accounts');
|
|
295
|
+
console.log('--------');
|
|
296
|
+
for (const acc of accounts) {
|
|
297
|
+
console.log(`${acc.name} (${acc.org?.name || acc.org?.domain || 'Unknown'})`);
|
|
298
|
+
console.log(` Balance: ${acc.balance} ${acc.currency}`);
|
|
299
|
+
if (acc['available-balance'] && acc['available-balance'] !== acc.balance) {
|
|
300
|
+
console.log(` Available: ${acc['available-balance']} ${acc.currency}`);
|
|
301
|
+
}
|
|
302
|
+
console.log(` Last updated: ${formatDate(acc['balance-date'])}`);
|
|
303
|
+
if (acc.type) {
|
|
304
|
+
console.log(` Type: ${acc.type}`);
|
|
305
|
+
}
|
|
306
|
+
if (acc.transactions && acc.transactions.length > 0 && !options.balancesOnly) {
|
|
307
|
+
console.log(` Transactions: ${acc.transactions.length}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
console.log('--------');
|
|
311
|
+
console.log(`Total: ${accounts.length} account(s)`);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Error:', error.message);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ===== TRANSACTIONS COMMAND =====
|
|
319
|
+
program.command('transactions')
|
|
320
|
+
.description('List transactions')
|
|
321
|
+
.option('-d, --days <days>', 'Number of days to look back', '30')
|
|
322
|
+
.option('--start-date <date>', 'Start date (Unix timestamp or YYYY-MM-DD)')
|
|
323
|
+
.option('--end-date <date>', 'End date (Unix timestamp or YYYY-MM-DD)')
|
|
324
|
+
.option('--account <id>', 'Filter by account ID')
|
|
325
|
+
.option('--pending', 'Include pending transactions')
|
|
326
|
+
.option('--json', 'Output as JSON')
|
|
327
|
+
.action(async (options) => {
|
|
328
|
+
const conn = loadConnection();
|
|
329
|
+
if (!conn) {
|
|
330
|
+
console.log('Not connected');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Parse dates
|
|
335
|
+
let startDate, endDate;
|
|
336
|
+
|
|
337
|
+
if (options.startDate) {
|
|
338
|
+
if (options.startDate.includes('-')) {
|
|
339
|
+
startDate = Math.floor(new Date(options.startDate).getTime() / 1000);
|
|
340
|
+
} else {
|
|
341
|
+
startDate = parseInt(options.startDate);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
const days = parseInt(options.days) || 30;
|
|
345
|
+
startDate = Math.floor(Date.now() / 1000) - (days * 24 * 60 * 60);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (options.endDate) {
|
|
349
|
+
if (options.endDate.includes('-')) {
|
|
350
|
+
endDate = Math.floor(new Date(options.endDate).getTime() / 1000);
|
|
351
|
+
} else {
|
|
352
|
+
endDate = parseInt(options.endDate);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const data = await getAccounts({
|
|
358
|
+
baseUrl: conn.access_url,
|
|
359
|
+
username: conn.username,
|
|
360
|
+
password: conn.password,
|
|
361
|
+
startDate: startDate.toString(),
|
|
362
|
+
endDate: endDate?.toString(),
|
|
363
|
+
account: options.account,
|
|
364
|
+
pending: options.pending,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (data.errors && data.errors.length > 0) {
|
|
368
|
+
console.log('Errors from server:');
|
|
369
|
+
for (const err of data.errors) {
|
|
370
|
+
console.log(` - ${err}`);
|
|
371
|
+
}
|
|
372
|
+
console.log('');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const accounts = data.accounts || [];
|
|
376
|
+
const allTransactions = [];
|
|
377
|
+
|
|
378
|
+
for (const acc of accounts) {
|
|
379
|
+
for (const tx of acc.transactions || []) {
|
|
380
|
+
allTransactions.push({
|
|
381
|
+
...tx,
|
|
382
|
+
account_name: acc.name,
|
|
383
|
+
account_id: acc.id,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
allTransactions.sort((a, b) => b.posted - a.posted);
|
|
389
|
+
|
|
390
|
+
if (options.json) {
|
|
391
|
+
console.log(JSON.stringify({
|
|
392
|
+
connected: true,
|
|
393
|
+
errors: data.errors || [],
|
|
394
|
+
query: {
|
|
395
|
+
start_date: startDate,
|
|
396
|
+
end_date: endDate,
|
|
397
|
+
account: options.account,
|
|
398
|
+
pending: options.pending,
|
|
399
|
+
},
|
|
400
|
+
transactions: allTransactions,
|
|
401
|
+
count: allTransactions.length,
|
|
402
|
+
}, null, 2));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log('Transactions');
|
|
407
|
+
console.log('------------');
|
|
408
|
+
|
|
409
|
+
if (startDate && !options.startDate) {
|
|
410
|
+
const days = parseInt(options.days) || 30;
|
|
411
|
+
console.log(`From last ${days} days`);
|
|
412
|
+
} else if (startDate) {
|
|
413
|
+
console.log(`From ${formatDate(startDate)}`);
|
|
414
|
+
if (endDate) console.log(`To ${formatDate(endDate)}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`Found ${allTransactions.length} transaction(s)\n`);
|
|
418
|
+
|
|
419
|
+
for (const tx of allTransactions) {
|
|
420
|
+
const amount = parseFloat(tx.amount).toFixed(2);
|
|
421
|
+
const sign = tx.amount.startsWith('-') ? '' : '+';
|
|
422
|
+
const pending = tx.pending ? '[PENDING] ' : '';
|
|
423
|
+
const date = tx.posted ? formatDateFull(tx.posted) : 'N/A';
|
|
424
|
+
|
|
425
|
+
console.log(`${date}`);
|
|
426
|
+
console.log(` ${sign}${amount.padStart(10)} ${pending}${truncate(tx.description, 50)}`);
|
|
427
|
+
console.log(` Account: ${tx.account_name}`);
|
|
428
|
+
if (tx.transacted_at && tx.transacted_at !== tx.posted) {
|
|
429
|
+
console.log(` Transacted: ${formatDateFull(tx.transacted_at)}`);
|
|
430
|
+
}
|
|
431
|
+
if (tx.category || (tx.extra && tx.extra.category)) {
|
|
432
|
+
console.log(` Category: ${tx.category || tx.extra.category}`);
|
|
433
|
+
}
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error('Error:', error.message);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simplefin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for accessing financial data via SimpleFIN Bridge",
|
|
5
|
+
"author": "dilllxd",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://git.dylan.lol/dylan/cli-tools"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://git.dylan.lol/dylan/cli-tools/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://git.dylan.lol/dylan/cli-tools/tree/main/simplefin-cli",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"simplefin",
|
|
17
|
+
"finance",
|
|
18
|
+
"bank",
|
|
19
|
+
"cli",
|
|
20
|
+
"terminal"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"simplefin": "./bin/simplefin.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node test.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"axios": "^1.6.0",
|
|
30
|
+
"commander": "^11.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/publish.sh
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SimpleFIN CLI Publishing Script
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "SimpleFIN CLI Publishing Script"
|
|
7
|
+
echo "================================"
|
|
8
|
+
echo ""
|
|
9
|
+
|
|
10
|
+
# Check if logged in
|
|
11
|
+
if ! npm whoami > /dev/null 2>&1; then
|
|
12
|
+
echo "You need to log in to npm first:"
|
|
13
|
+
echo " npm login"
|
|
14
|
+
echo ""
|
|
15
|
+
echo "Or create an account:"
|
|
16
|
+
echo " https://www.npmjs.com/signup"
|
|
17
|
+
echo ""
|
|
18
|
+
read -p "Press Enter after you've logged in..."
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Verify login
|
|
22
|
+
echo "Logged in as: $(npm whoami)"
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# Go to simplefin-cli directory
|
|
26
|
+
cd "$(dirname "$0")"
|
|
27
|
+
|
|
28
|
+
# Check package name
|
|
29
|
+
echo "Package name: $(node -e 'console.log(require("./package.json").name)')"
|
|
30
|
+
echo "Version: $(node -e 'console.log(require("./package.json").version)')"
|
|
31
|
+
echo ""
|
|
32
|
+
|
|
33
|
+
# Confirm publish
|
|
34
|
+
read -p "Publish to npm? (y/n) " -n 1 -r
|
|
35
|
+
echo ""
|
|
36
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
37
|
+
echo "Aborted."
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Publish
|
|
42
|
+
npm publish
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "Published successfully!"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "To install:"
|
|
48
|
+
echo " npm install -g simplefin-cli"
|
|
49
|
+
echo ""
|
|
50
|
+
echo "To use:"
|
|
51
|
+
echo " simplefin connect"
|
|
52
|
+
echo " simplefin accounts"
|
|
53
|
+
echo " simplefin transactions"
|
package/test.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const CLI = path.join(__dirname, 'bin', 'simplefin.js');
|
|
9
|
+
const configDir = path.join(os.homedir(), '.config', 'simplefin-cli');
|
|
10
|
+
const configFile = path.join(configDir, 'connection.json');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
function run(args) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const proc = spawn('node', [CLI, ...args], { cwd: path.dirname(CLI) });
|
|
18
|
+
let stdout = '';
|
|
19
|
+
let stderr = '';
|
|
20
|
+
|
|
21
|
+
proc.stdout.on('data', d => stdout += d.toString());
|
|
22
|
+
proc.stderr.on('data', d => stderr += d.toString());
|
|
23
|
+
|
|
24
|
+
proc.on('close', code => resolve({ code, stdout, stderr }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function test(name, args, check) {
|
|
29
|
+
const result = await run(args);
|
|
30
|
+
try {
|
|
31
|
+
check(result);
|
|
32
|
+
console.log('✓ ' + name);
|
|
33
|
+
passed++;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log('✗ ' + name);
|
|
36
|
+
console.log(' Error: ' + e.message);
|
|
37
|
+
console.log(' stdout: ' + result.stdout.substring(0, 200));
|
|
38
|
+
console.log(' stderr: ' + result.stderr.substring(0, 200));
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function testIntegration(name, args, check) {
|
|
44
|
+
const result = await run(args);
|
|
45
|
+
try {
|
|
46
|
+
check(result);
|
|
47
|
+
console.log('✓ ' + name);
|
|
48
|
+
passed++;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.log('✗ ' + name);
|
|
51
|
+
console.log(' Error: ' + e.message);
|
|
52
|
+
console.log(' Output: ' + result.stdout.substring(0, 300));
|
|
53
|
+
failed++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function setup() {
|
|
58
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
59
|
+
console.log('║ SimpleFIN CLI - Comprehensive Test Suite ║');
|
|
60
|
+
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
|
61
|
+
|
|
62
|
+
// Clean state
|
|
63
|
+
try { fs.unlinkSync(configFile); } catch (e) {}
|
|
64
|
+
|
|
65
|
+
// ===== HELP TESTS =====
|
|
66
|
+
console.log('━━━ Help & Usage ━━━');
|
|
67
|
+
|
|
68
|
+
await test('Main help shows usage', ['--help'], function(r) {
|
|
69
|
+
if (r.stdout.indexOf('SimpleFIN CLI') === -1) throw new Error('Missing header');
|
|
70
|
+
if (r.stdout.indexOf('connect') === -1) throw new Error('Missing connect command');
|
|
71
|
+
if (r.stdout.indexOf('disconnect') === -1) throw new Error('Missing disconnect command');
|
|
72
|
+
if (r.stdout.indexOf('status') === -1) throw new Error('Missing status command');
|
|
73
|
+
if (r.stdout.indexOf('accounts') === -1) throw new Error('Missing accounts command');
|
|
74
|
+
if (r.stdout.indexOf('transactions') === -1) throw new Error('Missing transactions command');
|
|
75
|
+
if (r.stdout.indexOf('info') === -1) throw new Error('Missing info command');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await test('Connect help shows token option', ['connect', '--help'], function(r) {
|
|
79
|
+
if (r.stdout.indexOf('--token') === -1) throw new Error('Missing --token option');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await test('Transactions help shows all options', ['transactions', '--help'], function(r) {
|
|
83
|
+
if (r.stdout.indexOf('--days') === -1) throw new Error('Missing --days option');
|
|
84
|
+
if (r.stdout.indexOf('--start-date') === -1) throw new Error('Missing --start-date option');
|
|
85
|
+
if (r.stdout.indexOf('--end-date') === -1) throw new Error('Missing --end-date option');
|
|
86
|
+
if (r.stdout.indexOf('--account') === -1) throw new Error('Missing --account option');
|
|
87
|
+
if (r.stdout.indexOf('--pending') === -1) throw new Error('Missing --pending option');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('Accounts help shows balances-only', ['accounts', '--help'], function(r) {
|
|
91
|
+
if (r.stdout.indexOf('--balances-only') === -1) throw new Error('Missing --balances-only option');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await test('Info help shows usage', ['info', '--help'], function(r) {
|
|
95
|
+
if (r.stdout.indexOf('info') === -1) throw new Error('Missing info help');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ===== ERROR HANDLING TESTS =====
|
|
99
|
+
console.log('\n━━━ Error Handling ━━━');
|
|
100
|
+
|
|
101
|
+
await test('Status exits 1 when not connected', ['status'], function(r) {
|
|
102
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
103
|
+
if (r.stdout.toLowerCase().indexOf('not connected') === -1) throw new Error('Missing error msg');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await test('Accounts exits 1 when not connected', ['accounts'], function(r) {
|
|
107
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await test('Transactions exits 1 when not connected', ['transactions'], function(r) {
|
|
111
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await test('Info exits 1 when not connected', ['info'], function(r) {
|
|
115
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await test('Connect without token shows error', ['connect'], function(r) {
|
|
119
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
120
|
+
if (r.stdout.toLowerCase().indexOf('token') === -1) throw new Error('Missing token error');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await test('Disconnect succeeds even when not connected', ['disconnect'], function(r) {
|
|
124
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ===== ARGUMENT PARSING TESTS =====
|
|
128
|
+
console.log('\n━━━ Argument Parsing ━━━');
|
|
129
|
+
|
|
130
|
+
var argTests = [
|
|
131
|
+
['transactions', '--days', '7'],
|
|
132
|
+
['transactions', '--start-date', '2026-01-01'],
|
|
133
|
+
['transactions', '--end-date', '2026-01-10'],
|
|
134
|
+
['transactions', '--account', 'ACT-123'],
|
|
135
|
+
['transactions', '--pending'],
|
|
136
|
+
['transactions', '--days', '30', '--json'],
|
|
137
|
+
['accounts', '--balances-only'],
|
|
138
|
+
['accounts', '--json'],
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (var i = 0; i < argTests.length; i++) {
|
|
142
|
+
var args = argTests[i];
|
|
143
|
+
await test('Parses: ' + args.join(' '), args, function(r) {
|
|
144
|
+
if (r.code !== 1) throw new Error('Should exit 1 (not connected), got ' + r.code);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===== EDGE CASES =====
|
|
149
|
+
console.log('\n━━━ Edge Cases ━━━');
|
|
150
|
+
|
|
151
|
+
await test('Unknown command shows error', ['unknown-command'], function(r) {
|
|
152
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await test('Missing required argument shows usage', ['transactions', '--start-date'], function(r) {
|
|
156
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ===== INTEGRATION TESTS =====
|
|
160
|
+
console.log('\n━━━ Integration Tests ━━━');
|
|
161
|
+
|
|
162
|
+
var realToken = process.env.SIMPLEFIN_TOKEN || 'aHR0cHM6Ly9iZXRhLWJyaWRnZS5zaW1wbGVmaW4ub3JnL3NpbXBsZWZpbi9jbGFpbS85RUE5MjM5NzlDN0ZCMDk3REFDQTU0NDJDOTcwNUZGNDRDODkyNDk1NkQzRDQxOTkyMUVDNDNCNEFBNTE3QTEwMzM1MUMxNDhGRDRFQzdFMTYxRUZDNTQwMUE5QzVENjI0MEZFNEZDRkEyN0VFMjM1RUIwRDRCRjI4NDYyODNBOQ==';
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await testIntegration('Connect with real token', ['connect', '--token', realToken], function(r) {
|
|
166
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
167
|
+
if (r.stdout.toLowerCase().indexOf('connected') === -1) throw new Error('Missing success message');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await testIntegration('Status shows connected', ['status'], function(r) {
|
|
171
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
172
|
+
if (r.stdout.toLowerCase().indexOf('connected') === -1) throw new Error('Missing connected status');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await testIntegration('Status --json outputs valid JSON', ['status', '--json'], function(r) {
|
|
176
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
177
|
+
try {
|
|
178
|
+
JSON.parse(r.stdout);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw new Error('Invalid JSON: ' + e.message);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await testIntegration('Accounts lists accounts', ['accounts'], function(r) {
|
|
185
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
186
|
+
if (r.stdout.toLowerCase().indexOf('accounts') === -1) throw new Error('Missing accounts header');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await testIntegration('Accounts --json outputs valid JSON', ['accounts', '--json'], function(r) {
|
|
190
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
191
|
+
try {
|
|
192
|
+
var data = JSON.parse(r.stdout);
|
|
193
|
+
if (!data.accounts) throw new Error('Missing accounts array');
|
|
194
|
+
} catch (e) {
|
|
195
|
+
throw new Error('Invalid JSON: ' + e.message);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await testIntegration('Transactions shows transactions', ['transactions', '--days', '7'], function(r) {
|
|
200
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
201
|
+
if (r.stdout.toLowerCase().indexOf('transactions') === -1) throw new Error('Missing transactions header');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await testIntegration('Transactions --json outputs valid JSON with count', ['transactions', '--days', '7', '--json'], function(r) {
|
|
205
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
206
|
+
try {
|
|
207
|
+
var data = JSON.parse(r.stdout);
|
|
208
|
+
if (typeof data.count !== 'number') throw new Error('Missing count field');
|
|
209
|
+
if (!Array.isArray(data.transactions)) throw new Error('Missing transactions array');
|
|
210
|
+
} catch (e) {
|
|
211
|
+
throw new Error('Invalid JSON: ' + e.message);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await testIntegration('Info shows server info', ['info'], function(r) {
|
|
216
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
217
|
+
if (r.stdout.toLowerCase().indexOf('server') === -1) throw new Error('Missing server info');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await testIntegration('Disconnect succeeds', ['disconnect'], function(r) {
|
|
221
|
+
if (r.code !== 0) throw new Error('Expected exit 0, got ' + r.code);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await testIntegration('Status shows disconnected after disconnect', ['status'], function(r) {
|
|
225
|
+
if (r.code !== 1) throw new Error('Expected exit 1, got ' + r.code);
|
|
226
|
+
if (r.stdout.toLowerCase().indexOf('not connected') === -1) throw new Error('Missing not connected');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.log('⚠ Integration tests skipped: ' + e.message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ===== SUMMARY =====
|
|
234
|
+
console.log('\n╔════════════════════════════════════════════════════════════╗');
|
|
235
|
+
console.log('║ Results: ' + passed + ' passed, ' + failed + ' failed ║');
|
|
236
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
237
|
+
|
|
238
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
setup();
|