@ktmcp-cli/billingo 1.0.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/.env.example +8 -0
- package/AGENT.md +447 -0
- package/CLI_SUMMARY.md +377 -0
- package/INDEX.md +364 -0
- package/INSTALL.sh +62 -0
- package/LICENSE +21 -0
- package/OPENCLAW.md +503 -0
- package/PROJECT_REPORT.md +462 -0
- package/QUICKSTART.md +212 -0
- package/README.md +378 -0
- package/STRUCTURE.txt +266 -0
- package/TESTING.md +513 -0
- package/banner.png +0 -0
- package/bin/billingo.js +75 -0
- package/examples/bank-account.json +8 -0
- package/examples/invoice.json +32 -0
- package/examples/partner.json +20 -0
- package/examples/product.json +10 -0
- package/logo.png +0 -0
- package/package.json +35 -0
- package/src/commands/bank-accounts.js +131 -0
- package/src/commands/config.js +73 -0
- package/src/commands/currencies.js +40 -0
- package/src/commands/document-blocks.js +40 -0
- package/src/commands/documents.js +248 -0
- package/src/commands/organization.js +35 -0
- package/src/commands/partners.js +130 -0
- package/src/commands/products.js +130 -0
- package/src/commands/utilities.js +34 -0
- package/src/lib/api.js +160 -0
- package/src/lib/auth.js +32 -0
- package/src/lib/config.js +87 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partner Commands
|
|
3
|
+
*
|
|
4
|
+
* Manage business partners (customers, suppliers)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { get, post, put, del, formatOutput } from '../lib/api.js';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
export function registerPartnerCommands(program) {
|
|
14
|
+
const partners = new Command('partners')
|
|
15
|
+
.description('Manage partners (customers, suppliers)');
|
|
16
|
+
|
|
17
|
+
partners
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List all partners')
|
|
20
|
+
.option('-p, --page <number>', 'Page number', '1')
|
|
21
|
+
.option('--per-page <number>', 'Results per page', '25')
|
|
22
|
+
.option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const spinner = ora('Fetching partners...').start();
|
|
25
|
+
try {
|
|
26
|
+
const data = await get('/partners', {
|
|
27
|
+
page: options.page,
|
|
28
|
+
per_page: options.perPage,
|
|
29
|
+
});
|
|
30
|
+
spinner.succeed('Partners retrieved');
|
|
31
|
+
console.log(formatOutput(data, options.format));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
spinner.fail('Failed to fetch partners');
|
|
34
|
+
console.error(chalk.red('Error:'), error.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
partners
|
|
40
|
+
.command('get <id>')
|
|
41
|
+
.description('Get partner by ID')
|
|
42
|
+
.option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
|
|
43
|
+
.action(async (id, options) => {
|
|
44
|
+
const spinner = ora(`Fetching partner ${id}...`).start();
|
|
45
|
+
try {
|
|
46
|
+
const data = await get(`/partners/${id}`);
|
|
47
|
+
spinner.succeed('Partner retrieved');
|
|
48
|
+
console.log(formatOutput(data, options.format));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
spinner.fail('Failed to fetch partner');
|
|
51
|
+
console.error(chalk.red('Error:'), error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
partners
|
|
57
|
+
.command('create')
|
|
58
|
+
.description('Create a new partner')
|
|
59
|
+
.option('-f, --file <path>', 'JSON file with partner data')
|
|
60
|
+
.option('-d, --data <json>', 'JSON string with partner data')
|
|
61
|
+
.action(async (options) => {
|
|
62
|
+
const spinner = ora('Creating partner...').start();
|
|
63
|
+
try {
|
|
64
|
+
let data;
|
|
65
|
+
if (options.file) {
|
|
66
|
+
data = JSON.parse(readFileSync(options.file, 'utf-8'));
|
|
67
|
+
} else if (options.data) {
|
|
68
|
+
data = JSON.parse(options.data);
|
|
69
|
+
} else {
|
|
70
|
+
spinner.fail('No data provided');
|
|
71
|
+
console.error(chalk.red('Error: Provide data with --file or --data'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await post('/partners', data);
|
|
76
|
+
spinner.succeed('Partner created');
|
|
77
|
+
console.log(formatOutput(result, 'pretty'));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
spinner.fail('Failed to create partner');
|
|
80
|
+
console.error(chalk.red('Error:'), error.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
partners
|
|
86
|
+
.command('update <id>')
|
|
87
|
+
.description('Update a partner')
|
|
88
|
+
.option('-f, --file <path>', 'JSON file with partner data')
|
|
89
|
+
.option('-d, --data <json>', 'JSON string with partner data')
|
|
90
|
+
.action(async (id, options) => {
|
|
91
|
+
const spinner = ora(`Updating partner ${id}...`).start();
|
|
92
|
+
try {
|
|
93
|
+
let data;
|
|
94
|
+
if (options.file) {
|
|
95
|
+
data = JSON.parse(readFileSync(options.file, 'utf-8'));
|
|
96
|
+
} else if (options.data) {
|
|
97
|
+
data = JSON.parse(options.data);
|
|
98
|
+
} else {
|
|
99
|
+
spinner.fail('No data provided');
|
|
100
|
+
console.error(chalk.red('Error: Provide data with --file or --data'));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await put(`/partners/${id}`, data);
|
|
105
|
+
spinner.succeed('Partner updated');
|
|
106
|
+
console.log(formatOutput(result, 'pretty'));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
spinner.fail('Failed to update partner');
|
|
109
|
+
console.error(chalk.red('Error:'), error.message);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
partners
|
|
115
|
+
.command('delete <id>')
|
|
116
|
+
.description('Delete a partner')
|
|
117
|
+
.action(async (id) => {
|
|
118
|
+
const spinner = ora(`Deleting partner ${id}...`).start();
|
|
119
|
+
try {
|
|
120
|
+
await del(`/partners/${id}`);
|
|
121
|
+
spinner.succeed('Partner deleted');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
spinner.fail('Failed to delete partner');
|
|
124
|
+
console.error(chalk.red('Error:'), error.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program.addCommand(partners);
|
|
130
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Commands
|
|
3
|
+
*
|
|
4
|
+
* Manage products for invoicing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { get, post, put, del, formatOutput } from '../lib/api.js';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
export function registerProductCommands(program) {
|
|
14
|
+
const products = new Command('products')
|
|
15
|
+
.description('Manage products');
|
|
16
|
+
|
|
17
|
+
products
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List all products')
|
|
20
|
+
.option('-p, --page <number>', 'Page number', '1')
|
|
21
|
+
.option('--per-page <number>', 'Results per page', '25')
|
|
22
|
+
.option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const spinner = ora('Fetching products...').start();
|
|
25
|
+
try {
|
|
26
|
+
const data = await get('/products', {
|
|
27
|
+
page: options.page,
|
|
28
|
+
per_page: options.perPage,
|
|
29
|
+
});
|
|
30
|
+
spinner.succeed('Products retrieved');
|
|
31
|
+
console.log(formatOutput(data, options.format));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
spinner.fail('Failed to fetch products');
|
|
34
|
+
console.error(chalk.red('Error:'), error.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
products
|
|
40
|
+
.command('get <id>')
|
|
41
|
+
.description('Get product by ID')
|
|
42
|
+
.option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
|
|
43
|
+
.action(async (id, options) => {
|
|
44
|
+
const spinner = ora(`Fetching product ${id}...`).start();
|
|
45
|
+
try {
|
|
46
|
+
const data = await get(`/products/${id}`);
|
|
47
|
+
spinner.succeed('Product retrieved');
|
|
48
|
+
console.log(formatOutput(data, options.format));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
spinner.fail('Failed to fetch product');
|
|
51
|
+
console.error(chalk.red('Error:'), error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
products
|
|
57
|
+
.command('create')
|
|
58
|
+
.description('Create a new product')
|
|
59
|
+
.option('-f, --file <path>', 'JSON file with product data')
|
|
60
|
+
.option('-d, --data <json>', 'JSON string with product data')
|
|
61
|
+
.action(async (options) => {
|
|
62
|
+
const spinner = ora('Creating product...').start();
|
|
63
|
+
try {
|
|
64
|
+
let data;
|
|
65
|
+
if (options.file) {
|
|
66
|
+
data = JSON.parse(readFileSync(options.file, 'utf-8'));
|
|
67
|
+
} else if (options.data) {
|
|
68
|
+
data = JSON.parse(options.data);
|
|
69
|
+
} else {
|
|
70
|
+
spinner.fail('No data provided');
|
|
71
|
+
console.error(chalk.red('Error: Provide data with --file or --data'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await post('/products', data);
|
|
76
|
+
spinner.succeed('Product created');
|
|
77
|
+
console.log(formatOutput(result, 'pretty'));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
spinner.fail('Failed to create product');
|
|
80
|
+
console.error(chalk.red('Error:'), error.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
products
|
|
86
|
+
.command('update <id>')
|
|
87
|
+
.description('Update a product')
|
|
88
|
+
.option('-f, --file <path>', 'JSON file with product data')
|
|
89
|
+
.option('-d, --data <json>', 'JSON string with product data')
|
|
90
|
+
.action(async (id, options) => {
|
|
91
|
+
const spinner = ora(`Updating product ${id}...`).start();
|
|
92
|
+
try {
|
|
93
|
+
let data;
|
|
94
|
+
if (options.file) {
|
|
95
|
+
data = JSON.parse(readFileSync(options.file, 'utf-8'));
|
|
96
|
+
} else if (options.data) {
|
|
97
|
+
data = JSON.parse(options.data);
|
|
98
|
+
} else {
|
|
99
|
+
spinner.fail('No data provided');
|
|
100
|
+
console.error(chalk.red('Error: Provide data with --file or --data'));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await put(`/products/${id}`, data);
|
|
105
|
+
spinner.succeed('Product updated');
|
|
106
|
+
console.log(formatOutput(result, 'pretty'));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
spinner.fail('Failed to update product');
|
|
109
|
+
console.error(chalk.red('Error:'), error.message);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
products
|
|
115
|
+
.command('delete <id>')
|
|
116
|
+
.description('Delete a product')
|
|
117
|
+
.action(async (id) => {
|
|
118
|
+
const spinner = ora(`Deleting product ${id}...`).start();
|
|
119
|
+
try {
|
|
120
|
+
await del(`/products/${id}`);
|
|
121
|
+
spinner.succeed('Product deleted');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
spinner.fail('Failed to delete product');
|
|
124
|
+
console.error(chalk.red('Error:'), error.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program.addCommand(products);
|
|
130
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility Commands
|
|
3
|
+
*
|
|
4
|
+
* Utility functions (ID conversion, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { get, formatOutput } from '../lib/api.js';
|
|
11
|
+
|
|
12
|
+
export function registerUtilityCommands(program) {
|
|
13
|
+
const utilities = new Command('utils')
|
|
14
|
+
.description('Utility functions');
|
|
15
|
+
|
|
16
|
+
utilities
|
|
17
|
+
.command('convert-id <id>')
|
|
18
|
+
.description('Convert legacy API ID to v3 ID')
|
|
19
|
+
.option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
|
|
20
|
+
.action(async (id, options) => {
|
|
21
|
+
const spinner = ora(`Converting ID ${id}...`).start();
|
|
22
|
+
try {
|
|
23
|
+
const data = await get(`/utils/convert-legacy-id/${id}`);
|
|
24
|
+
spinner.succeed('ID converted');
|
|
25
|
+
console.log(formatOutput(data, options.format));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
spinner.fail('Failed to convert ID');
|
|
28
|
+
console.error(chalk.red('Error:'), error.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program.addCommand(utilities);
|
|
34
|
+
}
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Module
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for Billingo API with error handling and rate limiting
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { getAuthHeaders } from './auth.js';
|
|
10
|
+
import { getBaseUrl } from './config.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create configured axios instance
|
|
14
|
+
* @returns {Object} Axios instance
|
|
15
|
+
*/
|
|
16
|
+
function createApiClient() {
|
|
17
|
+
const baseURL = getBaseUrl();
|
|
18
|
+
|
|
19
|
+
const client = axios.create({
|
|
20
|
+
baseURL,
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
headers: getAuthHeaders(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Response interceptor for error handling
|
|
26
|
+
client.interceptors.response.use(
|
|
27
|
+
(response) => {
|
|
28
|
+
// Log rate limit info if available
|
|
29
|
+
if (response.headers['x-ratelimit-remaining']) {
|
|
30
|
+
const remaining = response.headers['x-ratelimit-remaining'];
|
|
31
|
+
const limit = response.headers['x-ratelimit-limit'];
|
|
32
|
+
|
|
33
|
+
if (parseInt(remaining) < 10) {
|
|
34
|
+
console.warn(
|
|
35
|
+
chalk.yellow(`Warning: Only ${remaining}/${limit} API calls remaining in this window`)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response;
|
|
41
|
+
},
|
|
42
|
+
(error) => {
|
|
43
|
+
if (error.response) {
|
|
44
|
+
// Server responded with error status
|
|
45
|
+
const { status, data } = error.response;
|
|
46
|
+
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 401:
|
|
49
|
+
throw new Error('Authentication failed. Check your API key.');
|
|
50
|
+
case 403:
|
|
51
|
+
throw new Error('Access denied. Insufficient permissions.');
|
|
52
|
+
case 404:
|
|
53
|
+
throw new Error('Resource not found.');
|
|
54
|
+
case 422:
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Validation error: ${JSON.stringify(data.message || data, null, 2)}`
|
|
57
|
+
);
|
|
58
|
+
case 429:
|
|
59
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Rate limit exceeded. Retry after ${retryAfter} seconds.`
|
|
62
|
+
);
|
|
63
|
+
case 500:
|
|
64
|
+
throw new Error('Server error. Please try again later.');
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(
|
|
67
|
+
`API error (${status}): ${JSON.stringify(data, null, 2)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} else if (error.request) {
|
|
71
|
+
// Request made but no response
|
|
72
|
+
throw new Error('No response from server. Check your connection.');
|
|
73
|
+
} else {
|
|
74
|
+
// Error setting up request
|
|
75
|
+
throw new Error(`Request error: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return client;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Make GET request
|
|
85
|
+
* @param {string} endpoint - API endpoint
|
|
86
|
+
* @param {Object} params - Query parameters
|
|
87
|
+
* @returns {Promise<Object>} Response data
|
|
88
|
+
*/
|
|
89
|
+
export async function get(endpoint, params = {}) {
|
|
90
|
+
const client = createApiClient();
|
|
91
|
+
const response = await client.get(endpoint, { params });
|
|
92
|
+
return response.data;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Make POST request
|
|
97
|
+
* @param {string} endpoint - API endpoint
|
|
98
|
+
* @param {Object} data - Request body
|
|
99
|
+
* @returns {Promise<Object>} Response data
|
|
100
|
+
*/
|
|
101
|
+
export async function post(endpoint, data = {}) {
|
|
102
|
+
const client = createApiClient();
|
|
103
|
+
const response = await client.post(endpoint, data);
|
|
104
|
+
return response.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Make PUT request
|
|
109
|
+
* @param {string} endpoint - API endpoint
|
|
110
|
+
* @param {Object} data - Request body
|
|
111
|
+
* @returns {Promise<Object>} Response data
|
|
112
|
+
*/
|
|
113
|
+
export async function put(endpoint, data = {}) {
|
|
114
|
+
const client = createApiClient();
|
|
115
|
+
const response = await client.put(endpoint, data);
|
|
116
|
+
return response.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Make DELETE request
|
|
121
|
+
* @param {string} endpoint - API endpoint
|
|
122
|
+
* @returns {Promise<Object>} Response data
|
|
123
|
+
*/
|
|
124
|
+
export async function del(endpoint) {
|
|
125
|
+
const client = createApiClient();
|
|
126
|
+
const response = await client.delete(endpoint);
|
|
127
|
+
return response.data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Download file from API
|
|
132
|
+
* @param {string} endpoint - API endpoint
|
|
133
|
+
* @returns {Promise<Buffer>} File buffer
|
|
134
|
+
*/
|
|
135
|
+
export async function downloadFile(endpoint) {
|
|
136
|
+
const client = createApiClient();
|
|
137
|
+
const response = await client.get(endpoint, { responseType: 'arraybuffer' });
|
|
138
|
+
return response.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format output for CLI display
|
|
143
|
+
* @param {*} data - Data to format
|
|
144
|
+
* @param {string} format - Output format (json, pretty)
|
|
145
|
+
* @returns {string} Formatted output
|
|
146
|
+
*/
|
|
147
|
+
export function formatOutput(data, format = 'pretty') {
|
|
148
|
+
if (format === 'json') {
|
|
149
|
+
return JSON.stringify(data, null, 2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Pretty print for terminal
|
|
153
|
+
if (Array.isArray(data)) {
|
|
154
|
+
return data.map((item, idx) => {
|
|
155
|
+
return `${chalk.cyan(`[${idx}]`)} ${JSON.stringify(item, null, 2)}`;
|
|
156
|
+
}).join('\n\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return JSON.stringify(data, null, 2);
|
|
160
|
+
}
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Module
|
|
3
|
+
*
|
|
4
|
+
* Handles API key authentication for Billingo API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getApiKey } from './config.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get authentication headers for API requests
|
|
11
|
+
* @returns {Object} Headers object with API key
|
|
12
|
+
* @throws {Error} If API key is not configured
|
|
13
|
+
*/
|
|
14
|
+
export function getAuthHeaders() {
|
|
15
|
+
const apiKey = getApiKey();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
'X-API-KEY': apiKey,
|
|
19
|
+
'Accept': 'application/json',
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate API key format
|
|
26
|
+
* @param {string} apiKey - API key to validate
|
|
27
|
+
* @returns {boolean} True if format is valid
|
|
28
|
+
*/
|
|
29
|
+
export function validateApiKeyFormat(apiKey) {
|
|
30
|
+
// Billingo API keys are typically 32+ character alphanumeric strings
|
|
31
|
+
return typeof apiKey === 'string' && apiKey.length >= 20;
|
|
32
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Management
|
|
3
|
+
*
|
|
4
|
+
* Handles API key storage and configuration using conf package
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Conf from 'conf';
|
|
8
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
9
|
+
|
|
10
|
+
// Load environment variables
|
|
11
|
+
dotenvConfig();
|
|
12
|
+
|
|
13
|
+
const config = new Conf({
|
|
14
|
+
projectName: 'billingo-cli',
|
|
15
|
+
defaults: {
|
|
16
|
+
apiKey: process.env.BILLINGO_API_KEY || '',
|
|
17
|
+
baseUrl: process.env.BILLINGO_BASE_URL || 'https://api.billingo.hu/v3',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get configuration value
|
|
23
|
+
* @param {string} key - Configuration key
|
|
24
|
+
* @returns {*} Configuration value
|
|
25
|
+
*/
|
|
26
|
+
export function getConfig(key) {
|
|
27
|
+
return config.get(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set configuration value
|
|
32
|
+
* @param {string} key - Configuration key
|
|
33
|
+
* @param {*} value - Configuration value
|
|
34
|
+
*/
|
|
35
|
+
export function setConfig(key, value) {
|
|
36
|
+
config.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all configuration
|
|
41
|
+
* @returns {Object} All configuration values
|
|
42
|
+
*/
|
|
43
|
+
export function getAllConfig() {
|
|
44
|
+
return config.store;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete configuration value
|
|
49
|
+
* @param {string} key - Configuration key
|
|
50
|
+
*/
|
|
51
|
+
export function deleteConfig(key) {
|
|
52
|
+
config.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clear all configuration
|
|
57
|
+
*/
|
|
58
|
+
export function clearConfig() {
|
|
59
|
+
config.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get API key from config or environment
|
|
64
|
+
* @returns {string} API key
|
|
65
|
+
* @throws {Error} If API key is not configured
|
|
66
|
+
*/
|
|
67
|
+
export function getApiKey() {
|
|
68
|
+
const apiKey = getConfig('apiKey') || process.env.BILLINGO_API_KEY;
|
|
69
|
+
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'API key not configured. Set it with: billingo config set API_KEY <your-api-key>\n' +
|
|
73
|
+
'Or set BILLINGO_API_KEY environment variable.\n' +
|
|
74
|
+
'Get your API key at: https://app.billingo.hu/api-key'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return apiKey;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get base URL from config or environment
|
|
83
|
+
* @returns {string} Base URL
|
|
84
|
+
*/
|
|
85
|
+
export function getBaseUrl() {
|
|
86
|
+
return getConfig('baseUrl') || process.env.BILLINGO_BASE_URL || 'https://api.billingo.hu/v3';
|
|
87
|
+
}
|