noxctl 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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/auth.d.ts +29 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +267 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +1012 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/credentials-store.d.ts +4 -0
  12. package/dist/credentials-store.d.ts.map +1 -0
  13. package/dist/credentials-store.js +223 -0
  14. package/dist/credentials-store.js.map +1 -0
  15. package/dist/formatter.d.ts +62 -0
  16. package/dist/formatter.d.ts.map +1 -0
  17. package/dist/formatter.js +200 -0
  18. package/dist/formatter.js.map +1 -0
  19. package/dist/fortnox-client.d.ts +27 -0
  20. package/dist/fortnox-client.d.ts.map +1 -0
  21. package/dist/fortnox-client.js +178 -0
  22. package/dist/fortnox-client.js.map +1 -0
  23. package/dist/identifiers.d.ts +4 -0
  24. package/dist/identifiers.d.ts.map +1 -0
  25. package/dist/identifiers.js +16 -0
  26. package/dist/identifiers.js.map +1 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +43 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/operations/accounts.d.ts +18 -0
  32. package/dist/operations/accounts.d.ts.map +1 -0
  33. package/dist/operations/accounts.js +28 -0
  34. package/dist/operations/accounts.js.map +1 -0
  35. package/dist/operations/articles.d.ts +20 -0
  36. package/dist/operations/articles.d.ts.map +1 -0
  37. package/dist/operations/articles.js +36 -0
  38. package/dist/operations/articles.js.map +1 -0
  39. package/dist/operations/company.d.ts +2 -0
  40. package/dist/operations/company.d.ts.map +1 -0
  41. package/dist/operations/company.js +6 -0
  42. package/dist/operations/company.js.map +1 -0
  43. package/dist/operations/customers.d.ts +20 -0
  44. package/dist/operations/customers.d.ts.map +1 -0
  45. package/dist/operations/customers.js +37 -0
  46. package/dist/operations/customers.js.map +1 -0
  47. package/dist/operations/financial-reports.d.ts +37 -0
  48. package/dist/operations/financial-reports.d.ts.map +1 -0
  49. package/dist/operations/financial-reports.js +193 -0
  50. package/dist/operations/financial-reports.js.map +1 -0
  51. package/dist/operations/invoices.d.ts +32 -0
  52. package/dist/operations/invoices.d.ts.map +1 -0
  53. package/dist/operations/invoices.js +74 -0
  54. package/dist/operations/invoices.js.map +1 -0
  55. package/dist/operations/supplier-invoices.d.ts +23 -0
  56. package/dist/operations/supplier-invoices.d.ts.map +1 -0
  57. package/dist/operations/supplier-invoices.js +36 -0
  58. package/dist/operations/supplier-invoices.js.map +1 -0
  59. package/dist/operations/suppliers.d.ts +20 -0
  60. package/dist/operations/suppliers.d.ts.map +1 -0
  61. package/dist/operations/suppliers.js +36 -0
  62. package/dist/operations/suppliers.js.map +1 -0
  63. package/dist/operations/tax.d.ts +26 -0
  64. package/dist/operations/tax.d.ts.map +1 -0
  65. package/dist/operations/tax.js +47 -0
  66. package/dist/operations/tax.js.map +1 -0
  67. package/dist/operations/vouchers.d.ts +22 -0
  68. package/dist/operations/vouchers.d.ts.map +1 -0
  69. package/dist/operations/vouchers.js +41 -0
  70. package/dist/operations/vouchers.js.map +1 -0
  71. package/dist/tool-output.d.ts +16 -0
  72. package/dist/tool-output.d.ts.map +1 -0
  73. package/dist/tool-output.js +48 -0
  74. package/dist/tool-output.js.map +1 -0
  75. package/dist/tools/articles.d.ts +3 -0
  76. package/dist/tools/articles.d.ts.map +1 -0
  77. package/dist/tools/articles.js +75 -0
  78. package/dist/tools/articles.js.map +1 -0
  79. package/dist/tools/bookkeeping.d.ts +3 -0
  80. package/dist/tools/bookkeeping.d.ts.map +1 -0
  81. package/dist/tools/bookkeeping.js +82 -0
  82. package/dist/tools/bookkeeping.js.map +1 -0
  83. package/dist/tools/company.d.ts +3 -0
  84. package/dist/tools/company.d.ts.map +1 -0
  85. package/dist/tools/company.js +13 -0
  86. package/dist/tools/company.js.map +1 -0
  87. package/dist/tools/customers.d.ts +3 -0
  88. package/dist/tools/customers.d.ts.map +1 -0
  89. package/dist/tools/customers.js +75 -0
  90. package/dist/tools/customers.js.map +1 -0
  91. package/dist/tools/financial-reports.d.ts +3 -0
  92. package/dist/tools/financial-reports.d.ts.map +1 -0
  93. package/dist/tools/financial-reports.js +35 -0
  94. package/dist/tools/financial-reports.js.map +1 -0
  95. package/dist/tools/invoices.d.ts +3 -0
  96. package/dist/tools/invoices.d.ts.map +1 -0
  97. package/dist/tools/invoices.js +160 -0
  98. package/dist/tools/invoices.js.map +1 -0
  99. package/dist/tools/status.d.ts +3 -0
  100. package/dist/tools/status.d.ts.map +1 -0
  101. package/dist/tools/status.js +109 -0
  102. package/dist/tools/status.js.map +1 -0
  103. package/dist/tools/supplier-invoices.d.ts +3 -0
  104. package/dist/tools/supplier-invoices.d.ts.map +1 -0
  105. package/dist/tools/supplier-invoices.js +88 -0
  106. package/dist/tools/supplier-invoices.js.map +1 -0
  107. package/dist/tools/suppliers.d.ts +3 -0
  108. package/dist/tools/suppliers.d.ts.map +1 -0
  109. package/dist/tools/suppliers.js +77 -0
  110. package/dist/tools/suppliers.js.map +1 -0
  111. package/dist/tools/tax.d.ts +3 -0
  112. package/dist/tools/tax.d.ts.map +1 -0
  113. package/dist/tools/tax.js +18 -0
  114. package/dist/tools/tax.js.map +1 -0
  115. package/dist/views.d.ts +19 -0
  116. package/dist/views.d.ts.map +1 -0
  117. package/dist/views.js +165 -0
  118. package/dist/views.js.map +1 -0
  119. package/package.json +60 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1012 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { readFileSync } from 'node:fs';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, } from './formatter.js';
6
+ import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, } from './views.js';
7
+ const program = new Command();
8
+ program
9
+ .name('noxctl')
10
+ .description('CLI and MCP server for Fortnox accounting')
11
+ .version('0.1.0')
12
+ .addOption(new Option('-o, --output <format>', 'Output format')
13
+ .choices(['json', 'table'])
14
+ .default(undefined));
15
+ function json() {
16
+ return isJsonMode(program.opts());
17
+ }
18
+ async function confirmMutation(action, opts, payload) {
19
+ if (opts.dryRun) {
20
+ console.log(`Dry run: ${action}. No request was sent to Fortnox.`);
21
+ if (payload !== undefined) {
22
+ console.log('\nRequest payload:');
23
+ console.log(JSON.stringify(payload, null, 2));
24
+ }
25
+ return false;
26
+ }
27
+ if (opts.yes)
28
+ return true;
29
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
30
+ throw new Error(`Confirmation required to ${action}. Re-run with --yes, or --dry-run first.`);
31
+ }
32
+ const rl = createInterface({
33
+ input: process.stdin,
34
+ output: process.stdout,
35
+ });
36
+ try {
37
+ const answer = await rl.question(`${action}. Continue? [y/N] `);
38
+ return ['y', 'yes'].includes(answer.trim().toLowerCase());
39
+ }
40
+ finally {
41
+ rl.close();
42
+ }
43
+ }
44
+ // --- init (interactive setup wizard) ---
45
+ program
46
+ .command('init')
47
+ .description('Interactive setup wizard — recommended onboarding path')
48
+ .action(async () => {
49
+ const { loadCredentials, runOAuthSetup } = await import('./auth.js');
50
+ // Step 1: Check if already configured
51
+ const existing = await loadCredentials();
52
+ if (existing) {
53
+ console.log('Existing credentials found.');
54
+ if (process.stdin.isTTY && process.stdout.isTTY) {
55
+ const rlExisting = createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout,
58
+ });
59
+ try {
60
+ const answer = (await rlExisting.question('Re-run setup? This will replace your current credentials. [y/N] '))
61
+ .trim()
62
+ .toLowerCase();
63
+ if (answer !== 'y' && answer !== 'yes') {
64
+ console.log('Run `noxctl company info` to verify your current connection.');
65
+ return;
66
+ }
67
+ }
68
+ finally {
69
+ rlExisting.close();
70
+ }
71
+ }
72
+ else {
73
+ console.log('Run `noxctl company info` to verify, or re-run interactively to reconfigure.');
74
+ return;
75
+ }
76
+ }
77
+ // Step 2: Welcome message
78
+ console.log('Welcome to noxctl init!');
79
+ console.log('');
80
+ console.log("You'll need a Fortnox app from developer.fortnox.se with:");
81
+ console.log(' - Redirect URI: http://localhost:9876/callback');
82
+ console.log(' - Scopes (Behörigheter): Artikel, Bokföring, Faktura, Företagsinformation, Inställningar, Kund, Leverantör, Leverantörsfaktura');
83
+ console.log(' - Service account enabled (recommended)');
84
+ console.log('');
85
+ console.log('See the README for detailed portal instructions.');
86
+ console.log('');
87
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
88
+ let clientId;
89
+ let clientSecret;
90
+ let serviceAccount;
91
+ if (!isTTY) {
92
+ // CI / non-interactive mode: fall back to env vars
93
+ clientId = process.env.FORTNOX_CLIENT_ID ?? '';
94
+ clientSecret = process.env.FORTNOX_CLIENT_SECRET ?? '';
95
+ serviceAccount = process.env.FORTNOX_SERVICE_ACCOUNT === '1';
96
+ if (!clientId || !clientSecret) {
97
+ console.error('Error: stdin is not a TTY. Set FORTNOX_CLIENT_ID and FORTNOX_CLIENT_SECRET env vars to run non-interactively.');
98
+ process.exit(1);
99
+ }
100
+ }
101
+ else {
102
+ let rl = createInterface({
103
+ input: process.stdin,
104
+ output: process.stdout,
105
+ });
106
+ try {
107
+ // Step 3: Prompt for Client ID
108
+ const envClientId = process.env.FORTNOX_CLIENT_ID;
109
+ const clientIdPrompt = envClientId ? `Client ID [${envClientId}]: ` : 'Client ID: ';
110
+ const clientIdAnswer = (await rl.question(clientIdPrompt)).trim();
111
+ clientId = clientIdAnswer || envClientId || '';
112
+ if (!clientId) {
113
+ console.error('Error: Client ID is required.');
114
+ process.exit(1);
115
+ }
116
+ // Step 4: Prompt for Client Secret (masked input)
117
+ const envClientSecret = process.env.FORTNOX_CLIENT_SECRET;
118
+ if (envClientSecret) {
119
+ process.stdout.write('Client Secret [env var set — press Enter to use it]: ');
120
+ }
121
+ else {
122
+ process.stdout.write('Client Secret: ');
123
+ }
124
+ // Temporarily close rl so we can use raw mode for masked input
125
+ rl.close();
126
+ const clientSecretAnswer = await new Promise((resolve) => {
127
+ let buf = '';
128
+ const stdin = process.stdin;
129
+ stdin.setRawMode(true);
130
+ stdin.resume();
131
+ stdin.setEncoding('utf-8');
132
+ const onData = (chunk) => {
133
+ for (const ch of chunk) {
134
+ if (ch === '\r' || ch === '\n') {
135
+ stdin.setRawMode(false);
136
+ stdin.removeListener('data', onData);
137
+ process.stdout.write('\n');
138
+ resolve(buf.trim());
139
+ return;
140
+ }
141
+ else if (ch === '\u007f' || ch === '\b') {
142
+ if (buf.length > 0) {
143
+ buf = buf.slice(0, -1);
144
+ process.stdout.write('\b \b');
145
+ }
146
+ }
147
+ else if (ch === '\u0003') {
148
+ // Ctrl-C
149
+ process.exit(1);
150
+ }
151
+ else {
152
+ buf += ch;
153
+ }
154
+ }
155
+ };
156
+ stdin.on('data', onData);
157
+ });
158
+ // Re-create rl for subsequent questions
159
+ rl = createInterface({
160
+ input: process.stdin,
161
+ output: process.stdout,
162
+ });
163
+ clientSecret = clientSecretAnswer || envClientSecret || '';
164
+ if (!clientSecret) {
165
+ console.error('Error: Client Secret is required.');
166
+ process.exit(1);
167
+ }
168
+ // Step 5: Service account question — default yes
169
+ console.log('');
170
+ console.log('Service account mode lets noxctl refresh tokens automatically without');
171
+ console.log('opening a browser each time. Enable it in the Fortnox developer portal');
172
+ console.log('under your app\'s OAuth settings ("Möjliggör auktorisering som servicekonto").');
173
+ console.log('');
174
+ const saAnswer = (await rl.question('Is service account mode enabled for your app? [Y/n] '))
175
+ .trim()
176
+ .toLowerCase();
177
+ serviceAccount = saAnswer === '' || saAnswer === 'y' || saAnswer === 'yes';
178
+ }
179
+ finally {
180
+ rl.close();
181
+ }
182
+ }
183
+ // Step 6: Run OAuth flow
184
+ await runOAuthSetup({ clientId, clientSecret, serviceAccount });
185
+ // Step 7: Verify by fetching company info
186
+ try {
187
+ const { getCompanyInfo } = await import('./operations/company.js');
188
+ const data = await getCompanyInfo();
189
+ const company = data;
190
+ console.log('');
191
+ console.log('Connected successfully!');
192
+ if (company['CompanyName']) {
193
+ console.log(` Company: ${company['CompanyName']}`);
194
+ }
195
+ if (company['OrganizationNumber']) {
196
+ console.log(` Org number: ${company['OrganizationNumber']}`);
197
+ }
198
+ }
199
+ catch {
200
+ console.log('');
201
+ console.log('OAuth completed. Could not verify company info — you can run `noxctl company info` manually.');
202
+ }
203
+ // Step 8: Offer to register MCP server with Claude Code
204
+ if (process.stdin.isTTY && process.stdout.isTTY) {
205
+ const rl2 = createInterface({
206
+ input: process.stdin,
207
+ output: process.stdout,
208
+ });
209
+ try {
210
+ // Detect whether we're running via npx or from a local build.
211
+ const argv0 = process.argv[1] ?? '';
212
+ const useNpx = argv0.includes('npx') || argv0.includes('.bin/noxctl');
213
+ console.log('');
214
+ const mcpAnswer = (await rl2.question('Register the MCP server with Claude Code? [Y/n] '))
215
+ .trim()
216
+ .toLowerCase();
217
+ const doRegister = mcpAnswer === '' || mcpAnswer === 'y' || mcpAnswer === 'yes';
218
+ if (doRegister) {
219
+ const { execFile } = await import('node:child_process');
220
+ // All arguments below are static constants — no user input is interpolated.
221
+ const mcpArgs = useNpx
222
+ ? ['mcp', 'add', 'fortnox', '--', 'npx', 'noxctl', 'serve']
223
+ : ['mcp', 'add', 'fortnox', '--', 'node', argv0, 'serve'];
224
+ await new Promise((resolve) => {
225
+ execFile('claude', mcpArgs, (err) => {
226
+ if (err) {
227
+ const fallbackCmd = ['claude', ...mcpArgs].join(' ');
228
+ console.log('Could not register automatically. Run this manually:');
229
+ console.log(` ${fallbackCmd}`);
230
+ }
231
+ else {
232
+ console.log('MCP server registered. Restart Claude Code to pick it up.');
233
+ }
234
+ resolve();
235
+ });
236
+ });
237
+ }
238
+ // Offer npm link for local clone users so `noxctl` is in PATH
239
+ if (!useNpx) {
240
+ console.log('');
241
+ const linkAnswer = (await rl2.question('Add `noxctl` to your PATH via npm link? [Y/n] '))
242
+ .trim()
243
+ .toLowerCase();
244
+ const doLink = linkAnswer === '' || linkAnswer === 'y' || linkAnswer === 'yes';
245
+ if (doLink) {
246
+ const { execFile: execFileLink } = await import('node:child_process');
247
+ await new Promise((resolve) => {
248
+ execFileLink('npm', ['link'], { cwd: process.cwd() }, (err) => {
249
+ if (err) {
250
+ console.log('Could not link automatically. Run this manually:');
251
+ console.log(' npm link');
252
+ }
253
+ else {
254
+ console.log('Done! `noxctl` is now available globally.');
255
+ }
256
+ resolve();
257
+ });
258
+ });
259
+ }
260
+ }
261
+ }
262
+ finally {
263
+ rl2.close();
264
+ }
265
+ }
266
+ });
267
+ // --- logout ---
268
+ program
269
+ .command('logout')
270
+ .description('Remove stored Fortnox credentials')
271
+ .option('-y, --yes', 'Skip confirmation prompt')
272
+ .action(async (opts) => {
273
+ const { loadCredentials } = await import('./auth.js');
274
+ const { deleteCredentialBlob } = await import('./credentials-store.js');
275
+ const existing = await loadCredentials();
276
+ if (!existing) {
277
+ console.log('No credentials found. Nothing to remove.');
278
+ return;
279
+ }
280
+ if (!(await confirmMutation('Remove stored Fortnox credentials', opts))) {
281
+ return;
282
+ }
283
+ const deleted = await deleteCredentialBlob();
284
+ if (deleted) {
285
+ console.log('Credentials removed.');
286
+ }
287
+ else {
288
+ console.log('Could not remove credentials from the system keychain.');
289
+ console.log('They may have already been removed, or you may need to remove them manually.');
290
+ }
291
+ });
292
+ // --- doctor ---
293
+ program
294
+ .command('doctor')
295
+ .description('Check setup: credentials, token, API connectivity, and scopes')
296
+ .action(async () => {
297
+ const { loadCredentials } = await import('./auth.js');
298
+ let ok = true;
299
+ function pass(label, detail) {
300
+ console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`);
301
+ }
302
+ function fail(label, detail) {
303
+ ok = false;
304
+ console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`);
305
+ }
306
+ console.log('Checking noxctl configuration...\n');
307
+ // 1. Node version
308
+ const nodeVersion = process.versions.node;
309
+ const major = parseInt(nodeVersion.split('.')[0], 10);
310
+ if (major >= 20) {
311
+ pass('Node.js', `v${nodeVersion}`);
312
+ }
313
+ else {
314
+ fail('Node.js', `v${nodeVersion} (need 20+)`);
315
+ }
316
+ // 2. Credential store backend
317
+ const storeBackend = process.platform === 'darwin'
318
+ ? 'macOS Keychain'
319
+ : process.platform === 'win32'
320
+ ? 'Windows DPAPI'
321
+ : 'Linux Secret Service';
322
+ pass('Credential store', storeBackend);
323
+ // 3. Credentials exist
324
+ const creds = await loadCredentials();
325
+ if (!creds) {
326
+ fail('Credentials', 'not found — run `noxctl init` to set up');
327
+ console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
328
+ return;
329
+ }
330
+ pass('Credentials', 'found');
331
+ // 4. Client ID present
332
+ if (creds.client_id) {
333
+ pass('Client ID', `${creds.client_id.slice(0, 8)}...`);
334
+ }
335
+ else {
336
+ fail('Client ID', 'missing');
337
+ }
338
+ // 5. Tenant ID (service account)
339
+ if (creds.tenant_id) {
340
+ pass('Service account', `tenant ${creds.tenant_id}`);
341
+ }
342
+ else {
343
+ pass('Service account', 'not configured (using refresh token flow)');
344
+ }
345
+ // 6. Token expiry
346
+ const now = Date.now();
347
+ if (creds.expires_at > now) {
348
+ const minutesLeft = Math.round((creds.expires_at - now) / 60000);
349
+ pass('Access token', `valid for ~${minutesLeft} min`);
350
+ }
351
+ else {
352
+ pass('Access token', 'expired (will auto-refresh on next request)');
353
+ }
354
+ // 7. API connectivity — try fetching company info
355
+ try {
356
+ const { getCompanyInfo } = await import('./operations/company.js');
357
+ const data = await getCompanyInfo();
358
+ const company = data;
359
+ const name = company['CompanyName'] || 'unknown';
360
+ pass('API connection', `${name}`);
361
+ }
362
+ catch (err) {
363
+ const msg = err instanceof Error ? err.message : String(err);
364
+ fail('API connection', msg);
365
+ console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
366
+ return;
367
+ }
368
+ // 8. Scope validation — probe each scope with a lightweight GET
369
+ const { SCOPES } = await import('./auth.js');
370
+ const { fortnoxRequest, FortnoxApiError } = await import('./fortnox-client.js');
371
+ const scopeEndpoints = {
372
+ article: 'articles?limit=1',
373
+ customer: 'customers?limit=1',
374
+ invoice: 'invoices?limit=1',
375
+ supplier: 'suppliers?limit=1',
376
+ supplierinvoice: 'supplierinvoices?limit=1',
377
+ bookkeeping: 'vouchers?limit=1',
378
+ companyinformation: 'companyinformation',
379
+ settings: 'settings/company',
380
+ };
381
+ const required = SCOPES.split(' ');
382
+ const missing = [];
383
+ for (const scope of required) {
384
+ const endpoint = scopeEndpoints[scope];
385
+ if (!endpoint)
386
+ continue;
387
+ try {
388
+ await fortnoxRequest(endpoint);
389
+ }
390
+ catch (err) {
391
+ if (err instanceof FortnoxApiError && err.statusCode === 403) {
392
+ missing.push(scope);
393
+ }
394
+ // Non-403 errors (e.g. 500) are not scope problems — ignore here
395
+ }
396
+ }
397
+ if (missing.length === 0) {
398
+ pass('Scopes', `all ${required.length} scopes authorized`);
399
+ }
400
+ else {
401
+ fail('Scopes', `missing: ${missing.join(', ')}. Enable them in your Fortnox app at developer.fortnox.se`);
402
+ }
403
+ console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
404
+ });
405
+ // --- serve (default command) ---
406
+ program
407
+ .command('serve', { isDefault: true })
408
+ .description('Start the MCP server (stdio transport)')
409
+ .action(async () => {
410
+ const { startMcpServer } = await import('./index.js');
411
+ await startMcpServer();
412
+ });
413
+ // --- invoices ---
414
+ const invoices = program.command('invoices').description('Invoice operations');
415
+ invoices
416
+ .command('list')
417
+ .description('List/filter invoices')
418
+ .option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked')
419
+ .option('--customer <number>', 'Filter by customer number')
420
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
421
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
422
+ .option('--page <number>', 'Page number', parseInt)
423
+ .option('--limit <number>', 'Results per page', parseInt)
424
+ .option('-a, --all', 'Fetch all pages')
425
+ .action(async (opts) => {
426
+ const { listInvoices } = await import('./operations/invoices.js');
427
+ const data = await listInvoices({
428
+ filter: opts.filter,
429
+ customerNumber: opts.customer,
430
+ fromDate: opts.from,
431
+ toDate: opts.to,
432
+ page: opts.page,
433
+ limit: opts.limit,
434
+ all: opts.all,
435
+ });
436
+ const envelope = data;
437
+ outputList(envelope.Invoices ?? [], invoiceListColumns, json(), data, envelope.MetaInformation);
438
+ });
439
+ invoices
440
+ .command('get <documentNumber>')
441
+ .description('Get a single invoice')
442
+ .action(async (documentNumber) => {
443
+ const { getInvoice } = await import('./operations/invoices.js');
444
+ const data = await getInvoice(documentNumber);
445
+ outputDetail(data, invoiceDetailColumns, json());
446
+ });
447
+ invoices
448
+ .command('create')
449
+ .description('Create an invoice')
450
+ .requiredOption('--customer <number>', 'Customer number')
451
+ .requiredOption('--input <file>', 'Invoice data as JSON file (or - for stdin)')
452
+ .option('-y, --yes', 'Skip confirmation prompt')
453
+ .option('--dry-run', 'Preview the request without sending it')
454
+ .addHelpText('after', `
455
+ Examples:
456
+ echo '{"InvoiceRows":[{"ArticleNumber":"1","DeliveredQuantity":10,"Price":1500}]}' | noxctl invoices create --customer 25 --input - --dry-run
457
+
458
+ # Minimal JSON (Description instead of ArticleNumber):
459
+ echo '{"InvoiceRows":[{"Description":"Consulting","DeliveredQuantity":8,"Price":1200,"AccountNumber":3001,"VAT":25}]}' | noxctl invoices create --customer 25 --input - --yes`)
460
+ .action(async (opts) => {
461
+ const { createInvoice } = await import('./operations/invoices.js');
462
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
463
+ const input = JSON.parse(raw);
464
+ const params = { CustomerNumber: opts.customer, ...input };
465
+ if (!(await confirmMutation(`Create invoice for customer ${opts.customer}`, opts, {
466
+ Invoice: params,
467
+ }))) {
468
+ return;
469
+ }
470
+ const data = await createInvoice(params);
471
+ outputDetail(data, invoiceDetailColumns, json());
472
+ });
473
+ invoices
474
+ .command('update <documentNumber>')
475
+ .description('Update an invoice (not yet bookkeept)')
476
+ .requiredOption('--input <file>', 'Invoice data as JSON file (or - for stdin)')
477
+ .option('-y, --yes', 'Skip confirmation prompt')
478
+ .option('--dry-run', 'Preview the request without sending it')
479
+ .addHelpText('after', `
480
+ Examples:
481
+ echo '{"DueDate":"2026-04-30","OurReference":"Casey Example"}' | noxctl invoices update 28 --input - --dry-run
482
+ echo '{"InvoiceRows":[{"ArticleNumber":"1","DeliveredQuantity":5,"Price":2000}]}' | noxctl invoices update 28 --input - --yes`)
483
+ .action(async (documentNumber, opts) => {
484
+ const { updateInvoice } = await import('./operations/invoices.js');
485
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
486
+ const fields = JSON.parse(raw);
487
+ if (!(await confirmMutation(`Update invoice ${documentNumber}`, opts, { Invoice: fields }))) {
488
+ return;
489
+ }
490
+ const data = await updateInvoice(documentNumber, fields);
491
+ outputDetail(data, invoiceDetailColumns, json());
492
+ });
493
+ invoices
494
+ .command('send <documentNumber>')
495
+ .description('Send an invoice')
496
+ .addOption(new Option('--method <method>', 'Send method: email, print, einvoice')
497
+ .choices(['email', 'print', 'einvoice'])
498
+ .default('email'))
499
+ .option('--subject <subject>', 'Email subject (default: keeps existing)')
500
+ .option('--body <body>', 'Email body text')
501
+ .option('--bcc <email>', 'BCC email address')
502
+ .option('-y, --yes', 'Skip confirmation prompt')
503
+ .option('--dry-run', 'Preview the action without sending it')
504
+ .action(async (documentNumber, opts) => {
505
+ const { sendInvoice } = await import('./operations/invoices.js');
506
+ if (!(await confirmMutation(`Send invoice ${documentNumber} via ${opts.method}`, opts))) {
507
+ return;
508
+ }
509
+ const emailOptions = opts.subject || opts.body || opts.bcc
510
+ ? { subject: opts.subject, body: opts.body, bcc: opts.bcc }
511
+ : undefined;
512
+ const data = await sendInvoice(documentNumber, opts.method, emailOptions);
513
+ outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns);
514
+ });
515
+ invoices
516
+ .command('bookkeep <documentNumber>')
517
+ .description('Bookkeep an invoice')
518
+ .option('-y, --yes', 'Skip confirmation prompt')
519
+ .option('--dry-run', 'Preview the action without sending it')
520
+ .action(async (documentNumber, opts) => {
521
+ const { bookkeepInvoice } = await import('./operations/invoices.js');
522
+ if (!(await confirmMutation(`Bookkeep invoice ${documentNumber}`, opts))) {
523
+ return;
524
+ }
525
+ const data = await bookkeepInvoice(documentNumber);
526
+ outputConfirmation(`Invoice ${documentNumber} bookkeept.`, json(), data, invoiceConfirmColumns);
527
+ });
528
+ invoices
529
+ .command('credit <documentNumber>')
530
+ .description('Credit an invoice')
531
+ .option('-y, --yes', 'Skip confirmation prompt')
532
+ .option('--dry-run', 'Preview the action without sending it')
533
+ .action(async (documentNumber, opts) => {
534
+ const { creditInvoice } = await import('./operations/invoices.js');
535
+ if (!(await confirmMutation(`Credit invoice ${documentNumber}`, opts))) {
536
+ return;
537
+ }
538
+ const data = await creditInvoice(documentNumber);
539
+ outputConfirmation(`Invoice ${documentNumber} credited.`, json(), data, invoiceConfirmColumns);
540
+ });
541
+ // --- tax ---
542
+ const tax = program.command('tax').description('Tax operations');
543
+ tax
544
+ .command('report')
545
+ .description('Generate VAT tax report for a period')
546
+ .requiredOption('--from <date>', 'From date (YYYY-MM-DD)')
547
+ .requiredOption('--to <date>', 'To date (YYYY-MM-DD)')
548
+ .option('--year <number>', 'Financial year', parseInt)
549
+ .action(async (opts) => {
550
+ const { generateTaxReport } = await import('./operations/tax.js');
551
+ const data = await generateTaxReport({
552
+ fromDate: opts.from,
553
+ toDate: opts.to,
554
+ financialYear: opts.year,
555
+ });
556
+ if (json()) {
557
+ console.log(JSON.stringify(data, null, 2));
558
+ }
559
+ else {
560
+ console.log(formatTaxReport(data));
561
+ }
562
+ });
563
+ // --- reports ---
564
+ const reports = program
565
+ .command('reports')
566
+ .description('Financial reports (resultat/balansräkning)');
567
+ reports
568
+ .command('income')
569
+ .alias('resultat')
570
+ .description('Income statement (resultaträkning)')
571
+ .option('--year <number>', 'Financial year', parseInt)
572
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
573
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
574
+ .action(async (opts) => {
575
+ const { getIncomeStatement } = await import('./operations/financial-reports.js');
576
+ const data = await getIncomeStatement({
577
+ financialYear: opts.year,
578
+ fromDate: opts.from,
579
+ toDate: opts.to,
580
+ });
581
+ if (json()) {
582
+ console.log(JSON.stringify(data, null, 2));
583
+ }
584
+ else {
585
+ console.log(formatFinancialReport(data));
586
+ }
587
+ });
588
+ reports
589
+ .command('balance')
590
+ .alias('balans')
591
+ .description('Balance sheet (balansräkning)')
592
+ .option('--year <number>', 'Financial year', parseInt)
593
+ .option('--to <date>', 'As-of date (YYYY-MM-DD)')
594
+ .action(async (opts) => {
595
+ const { getBalanceSheet } = await import('./operations/financial-reports.js');
596
+ const data = await getBalanceSheet({ financialYear: opts.year, toDate: opts.to });
597
+ if (json()) {
598
+ console.log(JSON.stringify(data, null, 2));
599
+ }
600
+ else {
601
+ console.log(formatFinancialReport(data));
602
+ }
603
+ });
604
+ // --- accounts ---
605
+ const accounts = program.command('accounts').description('Chart of accounts operations');
606
+ accounts
607
+ .command('list')
608
+ .description('List accounts')
609
+ .option('--search <term>', 'Search by account name or number')
610
+ .option('--year <number>', 'Financial year', parseInt)
611
+ .option('--page <number>', 'Page number', parseInt)
612
+ .option('--limit <number>', 'Results per page', parseInt)
613
+ .option('-a, --all', 'Fetch all pages')
614
+ .action(async (opts) => {
615
+ const { listAccounts } = await import('./operations/accounts.js');
616
+ const data = await listAccounts({
617
+ search: opts.search,
618
+ financialYear: opts.year,
619
+ page: opts.page,
620
+ limit: opts.limit,
621
+ all: opts.all,
622
+ });
623
+ outputList(data.Accounts ?? [], accountListColumns, json(), data, data.MetaInformation);
624
+ });
625
+ // --- customers ---
626
+ const customers = program.command('customers').description('Customer operations');
627
+ customers
628
+ .command('list')
629
+ .description('List/search customers')
630
+ .option('--search <term>', 'Search by name')
631
+ .option('--page <number>', 'Page number', parseInt)
632
+ .option('--limit <number>', 'Results per page', parseInt)
633
+ .option('-a, --all', 'Fetch all pages')
634
+ .action(async (opts) => {
635
+ const { listCustomers } = await import('./operations/customers.js');
636
+ const data = await listCustomers({
637
+ search: opts.search,
638
+ page: opts.page,
639
+ limit: opts.limit,
640
+ all: opts.all,
641
+ });
642
+ const envelope = data;
643
+ outputList(envelope.Customers ?? [], customerListColumns, json(), data, envelope.MetaInformation);
644
+ });
645
+ customers
646
+ .command('get <customerNumber>')
647
+ .description('Get a single customer')
648
+ .action(async (customerNumber) => {
649
+ const { getCustomer } = await import('./operations/customers.js');
650
+ const data = await getCustomer(customerNumber);
651
+ outputDetail(data, customerDetailColumns, json());
652
+ });
653
+ customers
654
+ .command('create')
655
+ .description('Create a customer')
656
+ .requiredOption('--name <name>', 'Customer name')
657
+ .option('--input <file>', 'Customer data as JSON file (or - for stdin)')
658
+ .option('-y, --yes', 'Skip confirmation prompt')
659
+ .option('--dry-run', 'Preview the request without sending it')
660
+ .addHelpText('after', `
661
+ Examples:
662
+ noxctl customers create --name "Acme AB" --yes
663
+ echo '{"OrganisationNumber":"556677-8899","Email":"info@acme.se","City":"Stockholm"}' | noxctl customers create --name "Acme AB" --input - --yes`)
664
+ .action(async (opts) => {
665
+ const { createCustomer } = await import('./operations/customers.js');
666
+ let input = {};
667
+ if (opts.input) {
668
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
669
+ input = JSON.parse(raw);
670
+ }
671
+ const params = { ...input, Name: opts.name };
672
+ if (!(await confirmMutation(`Create customer "${opts.name}"`, opts, { Customer: params }))) {
673
+ return;
674
+ }
675
+ const data = await createCustomer(params);
676
+ outputDetail(data, customerDetailColumns, json());
677
+ });
678
+ customers
679
+ .command('update <customerNumber>')
680
+ .description('Update a customer')
681
+ .requiredOption('--input <file>', 'Customer data as JSON file (or - for stdin)')
682
+ .option('-y, --yes', 'Skip confirmation prompt')
683
+ .option('--dry-run', 'Preview the request without sending it')
684
+ .addHelpText('after', `
685
+ Examples:
686
+ echo '{"Email":"new@acme.se","Phone":"08-123456"}' | noxctl customers update 25 --input - --yes`)
687
+ .action(async (customerNumber, opts) => {
688
+ const { updateCustomer } = await import('./operations/customers.js');
689
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
690
+ const fields = JSON.parse(raw);
691
+ if (!(await confirmMutation(`Update customer ${customerNumber}`, opts, { Customer: fields }))) {
692
+ return;
693
+ }
694
+ const data = await updateCustomer(customerNumber, fields);
695
+ outputDetail(data, customerDetailColumns, json());
696
+ });
697
+ // --- articles ---
698
+ const articles = program.command('articles').description('Article operations');
699
+ articles
700
+ .command('list')
701
+ .description('List/search articles')
702
+ .option('--search <term>', 'Search by description')
703
+ .option('--page <number>', 'Page number', parseInt)
704
+ .option('--limit <number>', 'Results per page', parseInt)
705
+ .option('-a, --all', 'Fetch all pages')
706
+ .action(async (opts) => {
707
+ const { listArticles } = await import('./operations/articles.js');
708
+ const data = await listArticles({
709
+ search: opts.search,
710
+ page: opts.page,
711
+ limit: opts.limit,
712
+ all: opts.all,
713
+ });
714
+ const envelope = data;
715
+ outputList(envelope.Articles ?? [], articleListColumns, json(), data, envelope.MetaInformation);
716
+ });
717
+ articles
718
+ .command('get <articleNumber>')
719
+ .description('Get a single article')
720
+ .action(async (articleNumber) => {
721
+ const { getArticle } = await import('./operations/articles.js');
722
+ const data = await getArticle(articleNumber);
723
+ outputDetail(data, articleDetailColumns, json());
724
+ });
725
+ articles
726
+ .command('create')
727
+ .description('Create an article')
728
+ .requiredOption('--description <text>', 'Article description')
729
+ .option('--article-number <number>', 'Article number (auto-generated if omitted)')
730
+ .option('--input <file>', 'Article data as JSON file (or - for stdin)')
731
+ .option('-y, --yes', 'Skip confirmation prompt')
732
+ .option('--dry-run', 'Preview the request without sending it')
733
+ .addHelpText('after', `
734
+ Examples:
735
+ noxctl articles create --description "Konsulttimme" --yes
736
+ echo '{"SalesPrice":1500,"Unit":"tim","SalesAccount":3001,"VAT":25}' | noxctl articles create --description "Konsulttimme" --input - --yes`)
737
+ .action(async (opts) => {
738
+ const { createArticle } = await import('./operations/articles.js');
739
+ let input = {};
740
+ if (opts.input) {
741
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
742
+ input = JSON.parse(raw);
743
+ }
744
+ const params = { ...input, Description: opts.description };
745
+ if (opts.articleNumber)
746
+ params.ArticleNumber = opts.articleNumber;
747
+ if (!(await confirmMutation(`Create article "${opts.description}"`, opts, { Article: params }))) {
748
+ return;
749
+ }
750
+ const data = await createArticle(params);
751
+ outputDetail(data, articleDetailColumns, json());
752
+ });
753
+ articles
754
+ .command('update <articleNumber>')
755
+ .description('Update an article')
756
+ .requiredOption('--input <file>', 'Article data as JSON file (or - for stdin)')
757
+ .option('-y, --yes', 'Skip confirmation prompt')
758
+ .option('--dry-run', 'Preview the request without sending it')
759
+ .addHelpText('after', `
760
+ Examples:
761
+ echo '{"SalesPrice":1800}' | noxctl articles update 1 --input - --yes`)
762
+ .action(async (articleNumber, opts) => {
763
+ const { updateArticle } = await import('./operations/articles.js');
764
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
765
+ const fields = JSON.parse(raw);
766
+ if (!(await confirmMutation(`Update article ${articleNumber}`, opts, { Article: fields }))) {
767
+ return;
768
+ }
769
+ const data = await updateArticle(articleNumber, fields);
770
+ outputDetail(data, articleDetailColumns, json());
771
+ });
772
+ // --- suppliers ---
773
+ const suppliers = program.command('suppliers').description('Supplier operations');
774
+ suppliers
775
+ .command('list')
776
+ .description('List/search suppliers')
777
+ .option('--search <term>', 'Search by name')
778
+ .option('--page <number>', 'Page number', parseInt)
779
+ .option('--limit <number>', 'Results per page', parseInt)
780
+ .option('-a, --all', 'Fetch all pages')
781
+ .action(async (opts) => {
782
+ const { listSuppliers } = await import('./operations/suppliers.js');
783
+ const data = await listSuppliers({
784
+ search: opts.search,
785
+ page: opts.page,
786
+ limit: opts.limit,
787
+ all: opts.all,
788
+ });
789
+ const envelope = data;
790
+ outputList(envelope.Suppliers ?? [], supplierListColumns, json(), data, envelope.MetaInformation);
791
+ });
792
+ suppliers
793
+ .command('get <supplierNumber>')
794
+ .description('Get a single supplier')
795
+ .action(async (supplierNumber) => {
796
+ const { getSupplier } = await import('./operations/suppliers.js');
797
+ const data = await getSupplier(supplierNumber);
798
+ outputDetail(data, supplierDetailColumns, json());
799
+ });
800
+ suppliers
801
+ .command('create')
802
+ .description('Create a supplier')
803
+ .requiredOption('--name <name>', 'Supplier name')
804
+ .option('--input <file>', 'Supplier data as JSON file (or - for stdin)')
805
+ .option('-y, --yes', 'Skip confirmation prompt')
806
+ .option('--dry-run', 'Preview the request without sending it')
807
+ .addHelpText('after', `
808
+ Examples:
809
+ noxctl suppliers create --name "Dustin AB" --yes
810
+ echo '{"OrganisationNumber":"556123-4567","BG":"123-4567","Email":"faktura@dustin.se"}' | noxctl suppliers create --name "Dustin AB" --input - --yes`)
811
+ .action(async (opts) => {
812
+ const { createSupplier } = await import('./operations/suppliers.js');
813
+ let input = {};
814
+ if (opts.input) {
815
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
816
+ input = JSON.parse(raw);
817
+ }
818
+ const params = { ...input, Name: opts.name };
819
+ if (!(await confirmMutation(`Create supplier "${opts.name}"`, opts, { Supplier: params }))) {
820
+ return;
821
+ }
822
+ const data = await createSupplier(params);
823
+ outputDetail(data, supplierDetailColumns, json());
824
+ });
825
+ suppliers
826
+ .command('update <supplierNumber>')
827
+ .description('Update a supplier')
828
+ .requiredOption('--input <file>', 'Supplier data as JSON file (or - for stdin)')
829
+ .option('-y, --yes', 'Skip confirmation prompt')
830
+ .option('--dry-run', 'Preview the request without sending it')
831
+ .addHelpText('after', `
832
+ Examples:
833
+ echo '{"Email":"new@dustin.se","BG":"765-4321"}' | noxctl suppliers update 1 --input - --yes`)
834
+ .action(async (supplierNumber, opts) => {
835
+ const { updateSupplier } = await import('./operations/suppliers.js');
836
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
837
+ const fields = JSON.parse(raw);
838
+ if (!(await confirmMutation(`Update supplier ${supplierNumber}`, opts, { Supplier: fields }))) {
839
+ return;
840
+ }
841
+ const data = await updateSupplier(supplierNumber, fields);
842
+ outputDetail(data, supplierDetailColumns, json());
843
+ });
844
+ // --- supplier-invoices ---
845
+ const supplierInvoices = program
846
+ .command('supplier-invoices')
847
+ .alias('si')
848
+ .description('Supplier invoice operations (leverantörsfakturor)');
849
+ supplierInvoices
850
+ .command('list')
851
+ .description('List/filter supplier invoices')
852
+ .option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked, pendingpayment')
853
+ .option('--supplier <number>', 'Filter by supplier number')
854
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
855
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
856
+ .option('--page <number>', 'Page number', parseInt)
857
+ .option('--limit <number>', 'Results per page', parseInt)
858
+ .option('-a, --all', 'Fetch all pages')
859
+ .action(async (opts) => {
860
+ const { listSupplierInvoices } = await import('./operations/supplier-invoices.js');
861
+ const data = await listSupplierInvoices({
862
+ filter: opts.filter,
863
+ supplierNumber: opts.supplier,
864
+ fromDate: opts.from,
865
+ toDate: opts.to,
866
+ page: opts.page,
867
+ limit: opts.limit,
868
+ all: opts.all,
869
+ });
870
+ const envelope = data;
871
+ outputList(envelope.SupplierInvoices ?? [], supplierInvoiceListColumns, json(), data, envelope.MetaInformation);
872
+ });
873
+ supplierInvoices
874
+ .command('get <givenNumber>')
875
+ .description('Get a single supplier invoice')
876
+ .action(async (givenNumber) => {
877
+ const { getSupplierInvoice } = await import('./operations/supplier-invoices.js');
878
+ const data = await getSupplierInvoice(givenNumber);
879
+ outputDetail(data, supplierInvoiceDetailColumns, json());
880
+ });
881
+ supplierInvoices
882
+ .command('create')
883
+ .description('Create a supplier invoice')
884
+ .requiredOption('--supplier <number>', 'Supplier number')
885
+ .requiredOption('--input <file>', 'Invoice data as JSON file (or - for stdin)')
886
+ .option('-y, --yes', 'Skip confirmation prompt')
887
+ .option('--dry-run', 'Preview the request without sending it')
888
+ .addHelpText('after', `
889
+ Examples:
890
+ echo '{"InvoiceDate":"2026-03-01","DueDate":"2026-03-30","Total":1250,"OCR":"12345","SupplierInvoiceRows":[{"Account":6570,"Debit":1000,"Credit":0},{"Account":2641,"Debit":250,"Credit":0},{"Account":2440,"Debit":0,"Credit":1250}]}' | noxctl supplier-invoices create --supplier 1 --input - --dry-run`)
891
+ .action(async (opts) => {
892
+ const { createSupplierInvoice } = await import('./operations/supplier-invoices.js');
893
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
894
+ const input = JSON.parse(raw);
895
+ const params = { SupplierNumber: opts.supplier, ...input };
896
+ if (!(await confirmMutation(`Create supplier invoice for supplier ${opts.supplier}`, opts, {
897
+ SupplierInvoice: params,
898
+ }))) {
899
+ return;
900
+ }
901
+ const data = await createSupplierInvoice(params);
902
+ outputDetail(data, supplierInvoiceDetailColumns, json());
903
+ });
904
+ supplierInvoices
905
+ .command('bookkeep <givenNumber>')
906
+ .description('Bookkeep a supplier invoice')
907
+ .option('-y, --yes', 'Skip confirmation prompt')
908
+ .option('--dry-run', 'Preview the action without sending it')
909
+ .action(async (givenNumber, opts) => {
910
+ const { bookkeepSupplierInvoice } = await import('./operations/supplier-invoices.js');
911
+ if (!(await confirmMutation(`Bookkeep supplier invoice ${givenNumber}`, opts))) {
912
+ return;
913
+ }
914
+ const data = await bookkeepSupplierInvoice(givenNumber);
915
+ outputConfirmation(`Supplier invoice ${givenNumber} bookkeept.`, json(), data, supplierInvoiceConfirmColumns);
916
+ });
917
+ // --- company ---
918
+ const company = program.command('company').description('Company operations');
919
+ company
920
+ .command('info')
921
+ .description('Get company information')
922
+ .action(async () => {
923
+ const { getCompanyInfo } = await import('./operations/company.js');
924
+ const data = await getCompanyInfo();
925
+ outputDetail(data, companyDetailColumns, json());
926
+ });
927
+ // --- vouchers ---
928
+ const vouchers = program.command('vouchers').description('Voucher operations');
929
+ vouchers
930
+ .command('list')
931
+ .description('List vouchers')
932
+ .option('--series <series>', 'Voucher series (e.g. "A")')
933
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
934
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
935
+ .option('--year <number>', 'Financial year', parseInt)
936
+ .option('--page <number>', 'Page number', parseInt)
937
+ .option('--limit <number>', 'Results per page', parseInt)
938
+ .option('-a, --all', 'Fetch all pages')
939
+ .action(async (opts) => {
940
+ const { listVouchers } = await import('./operations/vouchers.js');
941
+ const data = await listVouchers({
942
+ series: opts.series,
943
+ fromDate: opts.from,
944
+ toDate: opts.to,
945
+ financialYear: opts.year,
946
+ page: opts.page,
947
+ limit: opts.limit,
948
+ all: opts.all,
949
+ });
950
+ const envelope = data;
951
+ outputList(envelope.Vouchers ?? [], voucherListColumns, json(), data, envelope.MetaInformation);
952
+ });
953
+ vouchers
954
+ .command('get <series> <voucherNumber>')
955
+ .description('Get a single voucher with rows (account, debit, credit)')
956
+ .option('--year <number>', 'Financial year', parseInt)
957
+ .action(async (series, voucherNumber, opts) => {
958
+ const { getVoucher } = await import('./operations/vouchers.js');
959
+ const data = await getVoucher(series, voucherNumber, opts.year);
960
+ if (json()) {
961
+ console.log(JSON.stringify(data, null, 2));
962
+ }
963
+ else {
964
+ outputDetail(data, voucherDetailColumns, false);
965
+ const rows = data.VoucherRows;
966
+ if (rows?.length) {
967
+ console.log('\nRows:');
968
+ outputList(rows, voucherRowColumns, false, rows);
969
+ }
970
+ }
971
+ });
972
+ vouchers
973
+ .command('create')
974
+ .description('Create a voucher')
975
+ .requiredOption('--input <file>', 'Voucher data as JSON file (or - for stdin)')
976
+ .option('-y, --yes', 'Skip confirmation prompt')
977
+ .option('--dry-run', 'Preview the request without sending it')
978
+ .addHelpText('after', `
979
+ Examples:
980
+ echo '{"Description":"Bankkostnad","TransactionDate":"2026-03-01","VoucherRows":[{"Account":6570,"Debit":500},{"Account":1930,"Credit":500}]}' | noxctl vouchers create --input - --dry-run`)
981
+ .action(async (opts) => {
982
+ const { createVoucher } = await import('./operations/vouchers.js');
983
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
984
+ const params = JSON.parse(raw);
985
+ if (!(await confirmMutation(`Create voucher "${String(params.Description || '')}"`, opts, {
986
+ Voucher: params,
987
+ }))) {
988
+ return;
989
+ }
990
+ const data = await createVoucher(params);
991
+ outputDetail(data, voucherDetailColumns, json());
992
+ });
993
+ // Error handling
994
+ program.exitOverride();
995
+ try {
996
+ await program.parseAsync(process.argv);
997
+ }
998
+ catch (err) {
999
+ if (err instanceof Error && 'code' in err) {
1000
+ const code = err.code;
1001
+ // Commander throws for --help and --version with exitCode 0
1002
+ if (code === 'commander.helpDisplayed' || code === 'commander.version') {
1003
+ process.exit(0);
1004
+ }
1005
+ if (code === 'commander.unknownCommand' || code === 'commander.missingMandatoryOptionValue') {
1006
+ process.exit(1);
1007
+ }
1008
+ }
1009
+ console.error(err instanceof Error ? err.message : err);
1010
+ process.exit(1);
1011
+ }
1012
+ //# sourceMappingURL=cli.js.map