@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.
@@ -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
+ }
@@ -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
+ }