noxctl 0.2.0 → 0.4.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/README.md +149 -6
- package/dist/auth.d.ts +7 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +24 -7
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1118 -95
- package/dist/cli.js.map +1 -1
- package/dist/completions.d.ts +13 -0
- package/dist/completions.d.ts.map +1 -0
- package/dist/completions.js +150 -0
- package/dist/completions.js.map +1 -0
- package/dist/credentials-store.d.ts.map +1 -1
- package/dist/credentials-store.js +45 -16
- package/dist/credentials-store.js.map +1 -1
- package/dist/date-periods.d.ts +14 -0
- package/dist/date-periods.d.ts.map +1 -0
- package/dist/date-periods.js +103 -0
- package/dist/date-periods.js.map +1 -0
- package/dist/formatter.d.ts +11 -2
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +27 -4
- package/dist/formatter.js.map +1 -1
- package/dist/fortnox-client.d.ts +1 -0
- package/dist/fortnox-client.d.ts.map +1 -1
- package/dist/fortnox-client.js +26 -3
- package/dist/fortnox-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/keychain-target.d.ts +32 -0
- package/dist/keychain-target.d.ts.map +1 -0
- package/dist/keychain-target.js +349 -0
- package/dist/keychain-target.js.map +1 -0
- package/dist/operations/absencetransactions.d.ts +21 -0
- package/dist/operations/absencetransactions.d.ts.map +1 -0
- package/dist/operations/absencetransactions.js +36 -0
- package/dist/operations/absencetransactions.js.map +1 -0
- package/dist/operations/analytics.d.ts +84 -0
- package/dist/operations/analytics.d.ts.map +1 -0
- package/dist/operations/analytics.js +154 -0
- package/dist/operations/analytics.js.map +1 -0
- package/dist/operations/attendancetransactions.d.ts +21 -0
- package/dist/operations/attendancetransactions.d.ts.map +1 -0
- package/dist/operations/attendancetransactions.js +40 -0
- package/dist/operations/attendancetransactions.js.map +1 -0
- package/dist/operations/contracts.d.ts +23 -0
- package/dist/operations/contracts.d.ts.map +1 -0
- package/dist/operations/contracts.js +52 -0
- package/dist/operations/contracts.js.map +1 -0
- package/dist/operations/customers.d.ts.map +1 -1
- package/dist/operations/customers.js +12 -2
- package/dist/operations/customers.js.map +1 -1
- package/dist/operations/employees.d.ts +19 -0
- package/dist/operations/employees.d.ts.map +1 -0
- package/dist/operations/employees.js +34 -0
- package/dist/operations/employees.js.map +1 -0
- package/dist/operations/financial-years.d.ts +16 -0
- package/dist/operations/financial-years.d.ts.map +1 -0
- package/dist/operations/financial-years.js +32 -0
- package/dist/operations/financial-years.js.map +1 -0
- package/dist/operations/salarytransactions.d.ts +21 -0
- package/dist/operations/salarytransactions.d.ts.map +1 -0
- package/dist/operations/salarytransactions.js +35 -0
- package/dist/operations/salarytransactions.js.map +1 -0
- package/dist/operations/scheduletimes.d.ts +4 -0
- package/dist/operations/scheduletimes.d.ts.map +1 -0
- package/dist/operations/scheduletimes.js +24 -0
- package/dist/operations/scheduletimes.js.map +1 -0
- package/dist/operations/vouchers.d.ts +15 -0
- package/dist/operations/vouchers.d.ts.map +1 -1
- package/dist/operations/vouchers.js +97 -0
- package/dist/operations/vouchers.js.map +1 -1
- package/dist/tools/absencetransactions.d.ts +3 -0
- package/dist/tools/absencetransactions.d.ts.map +1 -0
- package/dist/tools/absencetransactions.js +66 -0
- package/dist/tools/absencetransactions.js.map +1 -0
- package/dist/tools/analytics.d.ts +3 -0
- package/dist/tools/analytics.d.ts.map +1 -0
- package/dist/tools/analytics.js +52 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/attendancetransactions.d.ts +3 -0
- package/dist/tools/attendancetransactions.d.ts.map +1 -0
- package/dist/tools/attendancetransactions.js +63 -0
- package/dist/tools/attendancetransactions.js.map +1 -0
- package/dist/tools/bookkeeping.d.ts.map +1 -1
- package/dist/tools/bookkeeping.js +38 -2
- package/dist/tools/bookkeeping.js.map +1 -1
- package/dist/tools/contracts.d.ts +3 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +136 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/employees.d.ts +3 -0
- package/dist/tools/employees.d.ts.map +1 -0
- package/dist/tools/employees.js +107 -0
- package/dist/tools/employees.js.map +1 -0
- package/dist/tools/financial-years.d.ts +3 -0
- package/dist/tools/financial-years.d.ts.map +1 -0
- package/dist/tools/financial-years.js +33 -0
- package/dist/tools/financial-years.js.map +1 -0
- package/dist/tools/salarytransactions.d.ts +3 -0
- package/dist/tools/salarytransactions.d.ts.map +1 -0
- package/dist/tools/salarytransactions.js +70 -0
- package/dist/tools/salarytransactions.js.map +1 -0
- package/dist/tools/scheduletimes.d.ts +3 -0
- package/dist/tools/scheduletimes.d.ts.map +1 -0
- package/dist/tools/scheduletimes.js +62 -0
- package/dist/tools/scheduletimes.js.map +1 -0
- package/dist/tools/status.d.ts.map +1 -1
- package/dist/tools/status.js +10 -3
- package/dist/tools/status.js.map +1 -1
- package/dist/views.d.ts +17 -0
- package/dist/views.d.ts.map +1 -1
- package/dist/views.js +154 -0
- package/dist/views.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2,29 +2,65 @@
|
|
|
2
2
|
import { Command, Option } from 'commander';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
|
-
import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, } from './formatter.js';
|
|
5
|
+
import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, errorEnvelope, } from './formatter.js';
|
|
6
6
|
import { readActivePointer, readActivePointerOutcome, writeActivePointer, deleteActivePointer, readProfileIndex, resolveProfile, } from './profiles.js';
|
|
7
7
|
import { setResolvedProfile } from './auth.js';
|
|
8
|
+
import { applyPeriod } from './date-periods.js';
|
|
8
9
|
import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
|
|
9
|
-
import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, } from './views.js';
|
|
10
|
+
import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
|
|
10
11
|
const program = new Command();
|
|
11
12
|
program
|
|
12
13
|
.name('noxctl')
|
|
13
14
|
.description('CLI and MCP server for Fortnox accounting')
|
|
14
|
-
.version('0.
|
|
15
|
-
.addOption(new Option('-o, --output <format>', 'Output format')
|
|
15
|
+
.version('0.4.0')
|
|
16
|
+
.addOption(new Option('-o, --output <format>', 'Output format (default: table on TTY, json when piped)')
|
|
16
17
|
.choices(['json', 'table'])
|
|
17
18
|
.default(undefined))
|
|
18
19
|
.option('--profile <name>', 'Profile to operate on (overrides NOXCTL_PROFILE and active pointer)');
|
|
19
20
|
function json() {
|
|
20
21
|
return isJsonMode(program.opts());
|
|
21
22
|
}
|
|
23
|
+
// Suppress Commander's own plain-text usage errors (unknown command, missing
|
|
24
|
+
// required option, ...) when in JSON mode, so the top-level catch can emit a
|
|
25
|
+
// structured error envelope instead of leaving non-JSON on stderr. In table
|
|
26
|
+
// mode, keep Commander's friendly usage message.
|
|
27
|
+
//
|
|
28
|
+
// Both configureOutput and exitOverride must be set HERE, before the command
|
|
29
|
+
// tree is built: Commander only copies these settings into subcommands created
|
|
30
|
+
// *after* they're configured on the parent. Set at the bottom, a subcommand's
|
|
31
|
+
// parse error would call process.exit() directly and bypass the catch below
|
|
32
|
+
// (leaving JSON-mode failures with no envelope).
|
|
33
|
+
program.configureOutput({
|
|
34
|
+
outputError: (str, write) => {
|
|
35
|
+
if (!json())
|
|
36
|
+
write(str);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
program.exitOverride();
|
|
40
|
+
// Emit a fatal error honoring -o json mode (structured envelope to stderr) vs
|
|
41
|
+
// plain text, then exit. Validation failures inside command actions must go
|
|
42
|
+
// through this so they don't bypass the JSON error contract the way a bare
|
|
43
|
+
// console.error would. The top-level catch handles thrown errors the same way;
|
|
44
|
+
// this is for the cases where we want a specific non-1 exit code.
|
|
45
|
+
function fail(message, exitCode = 1) {
|
|
46
|
+
if (json()) {
|
|
47
|
+
console.error(JSON.stringify(errorEnvelope(new Error(message)), null, 2));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.error(message);
|
|
51
|
+
}
|
|
52
|
+
process.exit(exitCode);
|
|
53
|
+
}
|
|
54
|
+
function fromToParams(opts) {
|
|
55
|
+
const { from, to } = applyPeriod(opts);
|
|
56
|
+
return { fromDate: from, toDate: to };
|
|
57
|
+
}
|
|
22
58
|
let resolvedProfileInfo = { name: DEFAULT_PROFILE, source: 'default' };
|
|
23
59
|
export function getResolvedProfileInfo() {
|
|
24
60
|
return resolvedProfileInfo;
|
|
25
61
|
}
|
|
26
62
|
// Commands that don't need (and shouldn't require) a resolved profile.
|
|
27
|
-
const PROFILE_RESOLUTION_SKIP = new Set(['help']);
|
|
63
|
+
const PROFILE_RESOLUTION_SKIP = new Set(['help', 'completion']);
|
|
28
64
|
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
29
65
|
const name = actionCommand.name();
|
|
30
66
|
if (PROFILE_RESOLUTION_SKIP.has(name))
|
|
@@ -46,8 +82,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
46
82
|
// loudly but continue so `doctor` / `profile use` can repair the state.
|
|
47
83
|
const wouldRelyOnPointer = !flag && !env;
|
|
48
84
|
if (wouldRelyOnPointer && name === 'serve') {
|
|
49
|
-
|
|
50
|
-
process.exit(2);
|
|
85
|
+
fail(`Active profile pointer ${desc}. Refusing to start MCP server with ambiguous profile. Run \`noxctl doctor\` or set NOXCTL_PROFILE explicitly.`, 2);
|
|
51
86
|
}
|
|
52
87
|
process.stderr.write(`[warning: active-profile pointer ${desc}; ignoring]\n`);
|
|
53
88
|
}
|
|
@@ -56,8 +91,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
56
91
|
}
|
|
57
92
|
catch (err) {
|
|
58
93
|
if (err instanceof InvalidProfileNameError) {
|
|
59
|
-
|
|
60
|
-
process.exit(2);
|
|
94
|
+
fail(err.message, 2);
|
|
61
95
|
}
|
|
62
96
|
throw err;
|
|
63
97
|
}
|
|
@@ -108,6 +142,12 @@ async function confirmMutation(action, opts, payload) {
|
|
|
108
142
|
}
|
|
109
143
|
const company = await fetchCompanyHint();
|
|
110
144
|
const suffix = company ? ` (${company})` : '';
|
|
145
|
+
// Show what will actually be sent so the user can verify before confirming.
|
|
146
|
+
if (payload !== undefined) {
|
|
147
|
+
console.log('Request payload:');
|
|
148
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
111
151
|
const rl = createInterface({
|
|
112
152
|
input: process.stdin,
|
|
113
153
|
output: process.stdout,
|
|
@@ -120,14 +160,39 @@ async function confirmMutation(action, opts, payload) {
|
|
|
120
160
|
rl.close();
|
|
121
161
|
}
|
|
122
162
|
}
|
|
163
|
+
function requireDarwin() {
|
|
164
|
+
if (process.platform !== 'darwin') {
|
|
165
|
+
fail('The dedicated YubiKey-locked keychain is a macOS-only feature.', 2);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Confirmation for local, irreversible operations (not Fortnox mutations).
|
|
169
|
+
async function localConfirm(question, yes) {
|
|
170
|
+
if (yes)
|
|
171
|
+
return true;
|
|
172
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
173
|
+
throw new Error('Confirmation required. Re-run with --yes.');
|
|
174
|
+
}
|
|
175
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
176
|
+
try {
|
|
177
|
+
const answer = await rl.question(`${question} [y/N] `);
|
|
178
|
+
return ['y', 'yes'].includes(answer.trim().toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
rl.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
123
184
|
// --- init (interactive setup wizard) ---
|
|
124
185
|
program
|
|
125
186
|
.command('init')
|
|
126
187
|
.description('Interactive setup wizard — recommended onboarding path')
|
|
127
188
|
.option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
|
|
189
|
+
.option('--with-salary', 'Also request the Lön (salary/payroll) scope — requires the Lön permission enabled on your Fortnox app')
|
|
128
190
|
.action(async (initOpts) => {
|
|
129
|
-
const { loadCredentials, runOAuthSetup } = await import('./auth.js');
|
|
191
|
+
const { loadCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE } = await import('./auth.js');
|
|
130
192
|
const { validateProfileName } = await import('./profile-name.js');
|
|
193
|
+
// Opt-in salary scope: flag for TTY, env var for non-interactive/CI runs.
|
|
194
|
+
const withSalary = Boolean(initOpts.withSalary) || process.env.FORTNOX_WITH_SALARY === '1';
|
|
195
|
+
const scopes = withSalary ? `${SCOPES} ${SALARY_SCOPE}` : SCOPES;
|
|
131
196
|
let targetProfile;
|
|
132
197
|
try {
|
|
133
198
|
targetProfile = validateProfileName(initOpts.profile ?? resolvedProfileInfo.name);
|
|
@@ -177,6 +242,9 @@ program
|
|
|
177
242
|
console.log("You'll need a Fortnox app from developer.fortnox.se with:");
|
|
178
243
|
console.log(' - Redirect URI: http://localhost:9876/callback');
|
|
179
244
|
console.log(' - Scopes (Behörigheter): Artikel, Bokföring, Faktura, Företagsinformation, Inställningar, Kund, Leverantör, Leverantörsfaktura');
|
|
245
|
+
if (withSalary) {
|
|
246
|
+
console.log(' - Lön (krävs eftersom du kör med --with-salary)');
|
|
247
|
+
}
|
|
180
248
|
console.log(' - Service account enabled (recommended)');
|
|
181
249
|
console.log('');
|
|
182
250
|
console.log('See the README for detailed portal instructions.');
|
|
@@ -278,7 +346,7 @@ program
|
|
|
278
346
|
}
|
|
279
347
|
}
|
|
280
348
|
// Step 6: Run OAuth flow
|
|
281
|
-
await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile);
|
|
349
|
+
await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile, scopes);
|
|
282
350
|
// Step 6b: Set the active pointer if this is the first profile or no pointer exists.
|
|
283
351
|
try {
|
|
284
352
|
const idx = await readProfileIndex();
|
|
@@ -541,6 +609,24 @@ program
|
|
|
541
609
|
? 'Windows DPAPI'
|
|
542
610
|
: 'Linux Secret Service';
|
|
543
611
|
pass('Credential store', storeBackend);
|
|
612
|
+
// 2a. Dedicated YubiKey-locked keychain (macOS only)
|
|
613
|
+
if (process.platform === 'darwin') {
|
|
614
|
+
const kt = await import('./keychain-target.js');
|
|
615
|
+
const activePath = kt.activeKeychainPath();
|
|
616
|
+
if (activePath) {
|
|
617
|
+
const state = kt.keychainLockState(activePath);
|
|
618
|
+
if (state === 'locked') {
|
|
619
|
+
fail('Dedicated keychain', 'locked — run `noxctl keychain unlock` (tap your YubiKey)');
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
pass('Dedicated keychain', `active, ${state} (${activePath})`);
|
|
623
|
+
}
|
|
624
|
+
pass('ykman', kt.ykmanAvailable() ? 'installed' : 'not installed (brew install ykman)');
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
pass('Dedicated keychain', 'not enabled (using login keychain)');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
544
630
|
// 2b. Profile resolution
|
|
545
631
|
pass('Profile', `${resolvedProfileInfo.name} (source: ${resolvedProfileInfo.source})`);
|
|
546
632
|
// 2c. Active pointer health — surface a corrupt pointer file even though
|
|
@@ -574,7 +660,19 @@ program
|
|
|
574
660
|
// best-effort diagnostic — don't block doctor on filesystem issues
|
|
575
661
|
}
|
|
576
662
|
// 3. Credentials exist
|
|
577
|
-
|
|
663
|
+
let creds;
|
|
664
|
+
try {
|
|
665
|
+
creds = await loadCredentials();
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
const { KeychainLockedError } = await import('./keychain-target.js');
|
|
669
|
+
if (err instanceof KeychainLockedError) {
|
|
670
|
+
fail('Credentials', err.message);
|
|
671
|
+
console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
throw err;
|
|
675
|
+
}
|
|
578
676
|
if (!creds) {
|
|
579
677
|
fail('Credentials', `not found for profile "${resolvedProfileInfo.name}" — run \`noxctl init${resolvedProfileInfo.name === DEFAULT_PROFILE
|
|
580
678
|
? ''
|
|
@@ -624,7 +722,7 @@ program
|
|
|
624
722
|
return;
|
|
625
723
|
}
|
|
626
724
|
// 8. Scope validation — probe each scope with a lightweight GET
|
|
627
|
-
const {
|
|
725
|
+
const { effectiveScopes } = await import('./auth.js');
|
|
628
726
|
const { fortnoxRequest, FortnoxApiError } = await import('./fortnox-client.js');
|
|
629
727
|
const scopeEndpoints = {
|
|
630
728
|
article: 'articles?limit=1',
|
|
@@ -636,8 +734,11 @@ program
|
|
|
636
734
|
bookkeeping: 'vouchers?limit=1',
|
|
637
735
|
companyinformation: 'companyinformation',
|
|
638
736
|
settings: 'settings/company',
|
|
737
|
+
inbox: 'inbox',
|
|
738
|
+
connectfile: 'voucherfileconnections?limit=1',
|
|
739
|
+
salary: 'employees?limit=1',
|
|
639
740
|
};
|
|
640
|
-
const required =
|
|
741
|
+
const required = effectiveScopes(creds).split(' ');
|
|
641
742
|
const missing = [];
|
|
642
743
|
for (const scope of required) {
|
|
643
744
|
const endpoint = scopeEndpoints[scope];
|
|
@@ -647,10 +748,13 @@ program
|
|
|
647
748
|
await fortnoxRequest(endpoint);
|
|
648
749
|
}
|
|
649
750
|
catch (err) {
|
|
650
|
-
|
|
751
|
+
// A missing scope shows up as a 403, OR (for some endpoints, e.g. the
|
|
752
|
+
// archive/inbox family) a 400 whose message names the scope. Treat both
|
|
753
|
+
// as "not authorized"; other errors (500, etc.) aren't scope problems.
|
|
754
|
+
if (err instanceof FortnoxApiError &&
|
|
755
|
+
(err.statusCode === 403 || /scope/i.test(err.fortnoxMessage ?? ''))) {
|
|
651
756
|
missing.push(scope);
|
|
652
757
|
}
|
|
653
|
-
// Non-403 errors (e.g. 500) are not scope problems — ignore here
|
|
654
758
|
}
|
|
655
759
|
}
|
|
656
760
|
if (missing.length === 0) {
|
|
@@ -661,6 +765,245 @@ program
|
|
|
661
765
|
}
|
|
662
766
|
console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
|
|
663
767
|
});
|
|
768
|
+
// --- keychain (dedicated YubiKey-locked credential store, macOS only) ---
|
|
769
|
+
const keychain = program
|
|
770
|
+
.command('keychain')
|
|
771
|
+
.description('Manage the dedicated YubiKey-locked credential keychain (macOS)');
|
|
772
|
+
keychain
|
|
773
|
+
.command('status')
|
|
774
|
+
.description('Show dedicated-keychain mode, lock state, and YubiKey availability')
|
|
775
|
+
.action(async () => {
|
|
776
|
+
const kt = await import('./keychain-target.js');
|
|
777
|
+
const onDarwin = process.platform === 'darwin';
|
|
778
|
+
console.log('Keychain status\n');
|
|
779
|
+
console.log(` Platform ${process.platform}${onDarwin ? '' : ' (dedicated keychain is macOS-only)'}`);
|
|
780
|
+
if (!onDarwin)
|
|
781
|
+
return;
|
|
782
|
+
const activePath = kt.activeKeychainPath();
|
|
783
|
+
const kcPath = activePath ?? kt.dedicatedKeychainPath();
|
|
784
|
+
const lockState = kt.keychainLockState(kcPath);
|
|
785
|
+
console.log(` Dedicated mode ${activePath ? 'active' : 'inactive (using login keychain)'}`);
|
|
786
|
+
console.log(` Keychain file ${kcPath} (${lockState})`);
|
|
787
|
+
console.log(` Challenge file ${kt.readChallenge() ? kt.challengeFilePath() : 'not present'}`);
|
|
788
|
+
console.log(` ykman installed ${kt.ykmanAvailable() ? 'yes' : 'no (brew install ykman)'}`);
|
|
789
|
+
console.log(` YubiKey present ${kt.yubikeyPresent() ? 'yes' : 'no'}`);
|
|
790
|
+
const enrolledSerial = kt.readEnrolledSerial();
|
|
791
|
+
console.log(` Enrolled serial ${enrolledSerial ?? 'not recorded'}`);
|
|
792
|
+
if (enrolledSerial) {
|
|
793
|
+
const mismatch = kt.diagnoseSerialMismatch(enrolledSerial, kt.listYubikeySerials());
|
|
794
|
+
if (mismatch)
|
|
795
|
+
console.log(`\n ${mismatch}`);
|
|
796
|
+
}
|
|
797
|
+
if (activePath && lockState === 'locked') {
|
|
798
|
+
console.log('\n Locked — run `noxctl keychain unlock` (tap your YubiKey).');
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
keychain
|
|
802
|
+
.command('unlock')
|
|
803
|
+
.description('Unlock the dedicated keychain for this session (tap your YubiKey)')
|
|
804
|
+
.action(async () => {
|
|
805
|
+
requireDarwin();
|
|
806
|
+
const kt = await import('./keychain-target.js');
|
|
807
|
+
const kcPath = kt.activeKeychainPath() ?? kt.dedicatedKeychainPath();
|
|
808
|
+
const state = kt.keychainLockState(kcPath);
|
|
809
|
+
if (state === 'missing') {
|
|
810
|
+
console.error('No dedicated keychain found. Run `noxctl keychain init` first.');
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
if (state === 'unlocked') {
|
|
814
|
+
console.log('Keychain is already unlocked.');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const challenge = kt.readChallenge();
|
|
818
|
+
if (!challenge) {
|
|
819
|
+
console.error('Challenge file missing — cannot derive the unlock password. Re-run `noxctl keychain init`.');
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
// Preflight: a wrong/absent key is diagnosable from serials alone, before
|
|
823
|
+
// burning a tap on a challenge that can only fail.
|
|
824
|
+
const mismatch = kt.diagnoseSerialMismatch(kt.readEnrolledSerial(), kt.listYubikeySerials());
|
|
825
|
+
if (mismatch) {
|
|
826
|
+
console.error(mismatch);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
console.log('Tap your YubiKey when it blinks...');
|
|
830
|
+
let password;
|
|
831
|
+
try {
|
|
832
|
+
password = kt.computeChallengeResponse(challenge);
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
kt.unlockDedicatedKeychain(kcPath, password);
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
console.log('Keychain unlocked — open until your Mac sleeps.');
|
|
846
|
+
});
|
|
847
|
+
keychain
|
|
848
|
+
.command('lock')
|
|
849
|
+
.description('Lock the dedicated keychain now')
|
|
850
|
+
.action(async () => {
|
|
851
|
+
requireDarwin();
|
|
852
|
+
const kt = await import('./keychain-target.js');
|
|
853
|
+
const kcPath = kt.activeKeychainPath() ?? kt.dedicatedKeychainPath();
|
|
854
|
+
if (kt.keychainLockState(kcPath) === 'missing') {
|
|
855
|
+
console.error('No dedicated keychain found.');
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
kt.lockKeychain(kcPath);
|
|
859
|
+
console.log('Keychain locked.');
|
|
860
|
+
});
|
|
861
|
+
keychain
|
|
862
|
+
.command('init')
|
|
863
|
+
.description('Create the YubiKey-locked keychain and copy in existing credentials')
|
|
864
|
+
.action(async () => {
|
|
865
|
+
requireDarwin();
|
|
866
|
+
const kt = await import('./keychain-target.js');
|
|
867
|
+
if (!kt.ykmanAvailable()) {
|
|
868
|
+
console.error('ykman not found — install it with `brew install ykman`, then re-run.');
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
if (!kt.yubikeyPresent()) {
|
|
872
|
+
console.error('No YubiKey detected — insert it and re-run.');
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
const kcPath = kt.dedicatedKeychainPath();
|
|
876
|
+
if (kt.readChallenge()) {
|
|
877
|
+
console.error('Already initialized. Use `noxctl keychain unlock` or `noxctl keychain status`.');
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
if (kt.keychainLockState(kcPath) !== 'missing') {
|
|
881
|
+
console.error(`A keychain already exists at ${kcPath} but no challenge file is present.\n` +
|
|
882
|
+
`Remove the stale keychain (\`security delete-keychain "${kcPath}"\`) and re-run.`);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
console.log('This creates a dedicated, lock-on-sleep keychain whose password is derived');
|
|
886
|
+
console.log('from your YubiKey (OTP slot 2 challenge-response). You unlock it once per');
|
|
887
|
+
console.log('session with a tap. Existing credentials are copied in; the login-keychain');
|
|
888
|
+
console.log('originals are left in place (run `noxctl keychain seal` to remove them later).');
|
|
889
|
+
console.log('');
|
|
890
|
+
console.log('Slot 2 must already be programmed: `ykman otp chalresp --generate --touch 2`.');
|
|
891
|
+
console.log('');
|
|
892
|
+
// Collect existing credentials from the LOGIN keychain BEFORE activating
|
|
893
|
+
// dedicated mode (challenge file absent -> activeKeychainPath() is null).
|
|
894
|
+
const { readProfileIndex } = await import('./profiles.js');
|
|
895
|
+
const { loadCredentialBlob, saveCredentialBlob } = await import('./credentials-store.js');
|
|
896
|
+
const idx = await readProfileIndex();
|
|
897
|
+
const names = new Set([DEFAULT_PROFILE, ...idx.profiles.map((p) => p.name)]);
|
|
898
|
+
const blobs = [];
|
|
899
|
+
for (const name of names) {
|
|
900
|
+
const { blob } = await loadCredentialBlob(name);
|
|
901
|
+
if (blob)
|
|
902
|
+
blobs.push({ profile: name, blob });
|
|
903
|
+
}
|
|
904
|
+
console.log(`Found ${blobs.length} credential set(s) to copy: ${blobs.map((b) => b.profile).join(', ') || '(none)'}`);
|
|
905
|
+
console.log('');
|
|
906
|
+
// Derive the keychain password from the YubiKey (requires a tap). Done
|
|
907
|
+
// before creating the keychain so a missed tap leaves nothing behind.
|
|
908
|
+
const challenge = kt.generateChallenge();
|
|
909
|
+
console.log('Tap your YubiKey when it blinks...');
|
|
910
|
+
let password;
|
|
911
|
+
try {
|
|
912
|
+
password = kt.computeChallengeResponse(challenge);
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
const created = kt.createDedicatedKeychain(kcPath, password);
|
|
919
|
+
if (created === 'exists') {
|
|
920
|
+
console.error(`Unexpected: keychain already exists at ${kcPath}. Aborting.`);
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
kt.setLockOnSleep(kcPath);
|
|
924
|
+
// Activate dedicated mode: now activeKeychainPath() resolves to kcPath and
|
|
925
|
+
// saveCredentialBlob targets the new (still-unlocked) keychain.
|
|
926
|
+
kt.writeChallenge(challenge);
|
|
927
|
+
// Remember which physical key answered the enrollment challenge so unlock
|
|
928
|
+
// can distinguish "wrong key inserted" from "missed tap" later.
|
|
929
|
+
const serials = kt.listYubikeySerials();
|
|
930
|
+
if (serials.length === 1) {
|
|
931
|
+
kt.writeEnrolledSerial(serials[0]);
|
|
932
|
+
console.log(`Enrolled against YubiKey serial ${serials[0]}.`);
|
|
933
|
+
}
|
|
934
|
+
else if (serials.length > 1) {
|
|
935
|
+
console.log(`Multiple YubiKeys present (${serials.join(', ')}) — not recording an enrolled serial.`);
|
|
936
|
+
}
|
|
937
|
+
let copied = 0;
|
|
938
|
+
for (const { profile, blob } of blobs) {
|
|
939
|
+
try {
|
|
940
|
+
await saveCredentialBlob(blob, profile);
|
|
941
|
+
copied++;
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
console.error(` ! failed to copy "${profile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
console.log('');
|
|
948
|
+
console.log(`Done. Created ${kcPath}, copied ${copied}/${blobs.length} credential set(s).`);
|
|
949
|
+
console.log('The keychain is unlocked now and will lock when your Mac sleeps.');
|
|
950
|
+
console.log('Next session: `noxctl keychain unlock` (one tap).');
|
|
951
|
+
console.log('When you trust it: `noxctl keychain seal` to delete the login-keychain copies.');
|
|
952
|
+
});
|
|
953
|
+
keychain
|
|
954
|
+
.command('seal')
|
|
955
|
+
.description('Delete the login-keychain credential copies left by `init` (irreversible)')
|
|
956
|
+
.option('--yes', 'Skip the confirmation prompt')
|
|
957
|
+
.action(async (opts) => {
|
|
958
|
+
requireDarwin();
|
|
959
|
+
const kt = await import('./keychain-target.js');
|
|
960
|
+
const activePath = kt.activeKeychainPath();
|
|
961
|
+
if (!activePath) {
|
|
962
|
+
console.error('Dedicated keychain mode is not active — nothing to seal. Run `noxctl keychain init` first.');
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
if (kt.keychainLockState(activePath) === 'locked') {
|
|
966
|
+
console.error('Dedicated keychain is locked — run `noxctl keychain unlock` first so the copies can be verified before the originals are deleted.');
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
const { readProfileIndex } = await import('./profiles.js');
|
|
970
|
+
const { LEGACY_KEYCHAIN_ACCOUNT, keychainAccount } = await import('./profile-name.js');
|
|
971
|
+
const { loadCredentialBlob } = await import('./credentials-store.js');
|
|
972
|
+
const idx = await readProfileIndex();
|
|
973
|
+
const uniqueNames = [
|
|
974
|
+
...new Set([DEFAULT_PROFILE, ...idx.profiles.map((p) => p.name)].map((n) => n.toLowerCase())),
|
|
975
|
+
];
|
|
976
|
+
// Verify the dedicated keychain actually holds each profile's creds before
|
|
977
|
+
// deleting the login originals — never strand the user with no copy.
|
|
978
|
+
const verified = [];
|
|
979
|
+
for (const name of uniqueNames) {
|
|
980
|
+
const { blob } = await loadCredentialBlob(name);
|
|
981
|
+
if (blob)
|
|
982
|
+
verified.push(name);
|
|
983
|
+
}
|
|
984
|
+
if (verified.length === 0) {
|
|
985
|
+
console.error('The dedicated keychain holds no credentials — refusing to delete the login copies.');
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
console.log(`Verified ${verified.length} credential set(s) in the dedicated keychain: ${verified.join(', ')}`);
|
|
989
|
+
const confirmed = await localConfirm('Delete the login-keychain copies now? This cannot be undone.', opts.yes);
|
|
990
|
+
if (!confirmed) {
|
|
991
|
+
console.log('Aborted. Login-keychain copies left in place.');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// Accounts init may have written to the login keychain: the legacy
|
|
995
|
+
// "default" account plus profile:<name> for every known profile.
|
|
996
|
+
const accounts = new Set([LEGACY_KEYCHAIN_ACCOUNT]);
|
|
997
|
+
for (const name of uniqueNames)
|
|
998
|
+
accounts.add(keychainAccount(name));
|
|
999
|
+
let deleted = 0;
|
|
1000
|
+
for (const account of accounts) {
|
|
1001
|
+
if (kt.deleteLoginSecret(account))
|
|
1002
|
+
deleted++;
|
|
1003
|
+
}
|
|
1004
|
+
console.log(`Deleted ${deleted} login-keychain entr${deleted === 1 ? 'y' : 'ies'}.`);
|
|
1005
|
+
console.log('Credentials now live only in the YubiKey-locked keychain.');
|
|
1006
|
+
});
|
|
664
1007
|
// --- serve (default command) ---
|
|
665
1008
|
program
|
|
666
1009
|
.command('serve', { isDefault: true })
|
|
@@ -678,6 +1021,7 @@ invoices
|
|
|
678
1021
|
.option('--customer <number>', 'Filter by customer number')
|
|
679
1022
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
680
1023
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1024
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
681
1025
|
.option('--page <number>', 'Page number', parseInt)
|
|
682
1026
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
683
1027
|
.option('-a, --all', 'Fetch all pages')
|
|
@@ -686,8 +1030,7 @@ invoices
|
|
|
686
1030
|
const data = await listInvoices({
|
|
687
1031
|
filter: opts.filter,
|
|
688
1032
|
customerNumber: opts.customer,
|
|
689
|
-
|
|
690
|
-
toDate: opts.to,
|
|
1033
|
+
...fromToParams(opts),
|
|
691
1034
|
page: opts.page,
|
|
692
1035
|
limit: opts.limit,
|
|
693
1036
|
all: opts.all,
|
|
@@ -701,7 +1044,7 @@ invoices
|
|
|
701
1044
|
.action(async (documentNumber) => {
|
|
702
1045
|
const { getInvoice } = await import('./operations/invoices.js');
|
|
703
1046
|
const data = await getInvoice(documentNumber);
|
|
704
|
-
outputDetail(data, invoiceDetailColumns, json());
|
|
1047
|
+
outputDetail(data, invoiceDetailColumns, json(), 'Invoice');
|
|
705
1048
|
});
|
|
706
1049
|
invoices
|
|
707
1050
|
.command('create')
|
|
@@ -727,7 +1070,7 @@ Examples:
|
|
|
727
1070
|
return;
|
|
728
1071
|
}
|
|
729
1072
|
const data = await createInvoice(params);
|
|
730
|
-
outputDetail(data, invoiceDetailColumns, json());
|
|
1073
|
+
outputDetail(data, invoiceDetailColumns, json(), 'Invoice');
|
|
731
1074
|
});
|
|
732
1075
|
invoices
|
|
733
1076
|
.command('update <documentNumber>')
|
|
@@ -747,7 +1090,7 @@ Examples:
|
|
|
747
1090
|
return;
|
|
748
1091
|
}
|
|
749
1092
|
const data = await updateInvoice(documentNumber, fields);
|
|
750
|
-
outputDetail(data, invoiceDetailColumns, json());
|
|
1093
|
+
outputDetail(data, invoiceDetailColumns, json(), 'Invoice');
|
|
751
1094
|
});
|
|
752
1095
|
invoices
|
|
753
1096
|
.command('send <documentNumber>')
|
|
@@ -769,7 +1112,7 @@ invoices
|
|
|
769
1112
|
? { subject: opts.subject, body: opts.body, bcc: opts.bcc }
|
|
770
1113
|
: undefined;
|
|
771
1114
|
const data = await sendInvoice(documentNumber, opts.method, emailOptions);
|
|
772
|
-
outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns);
|
|
1115
|
+
outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
773
1116
|
});
|
|
774
1117
|
invoices
|
|
775
1118
|
.command('bookkeep <documentNumber>')
|
|
@@ -782,7 +1125,7 @@ invoices
|
|
|
782
1125
|
return;
|
|
783
1126
|
}
|
|
784
1127
|
const data = await bookkeepInvoice(documentNumber);
|
|
785
|
-
outputConfirmation(`Invoice ${documentNumber} bookkeept.`, json(), data, invoiceConfirmColumns);
|
|
1128
|
+
outputConfirmation(`Invoice ${documentNumber} bookkeept.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
786
1129
|
});
|
|
787
1130
|
invoices
|
|
788
1131
|
.command('credit <documentNumber>')
|
|
@@ -795,21 +1138,26 @@ invoices
|
|
|
795
1138
|
return;
|
|
796
1139
|
}
|
|
797
1140
|
const data = await creditInvoice(documentNumber);
|
|
798
|
-
outputConfirmation(`Invoice ${documentNumber} credited.`, json(), data, invoiceConfirmColumns);
|
|
1141
|
+
outputConfirmation(`Invoice ${documentNumber} credited.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
799
1142
|
});
|
|
800
1143
|
// --- tax ---
|
|
801
1144
|
const tax = program.command('tax').description('Tax operations');
|
|
802
1145
|
tax
|
|
803
1146
|
.command('report')
|
|
804
1147
|
.description('Generate VAT tax report for a period')
|
|
805
|
-
.
|
|
806
|
-
.
|
|
1148
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1149
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1150
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
807
1151
|
.option('--year <number>', 'Financial year', parseInt)
|
|
808
1152
|
.action(async (opts) => {
|
|
809
1153
|
const { generateTaxReport } = await import('./operations/tax.js');
|
|
1154
|
+
const range = fromToParams(opts);
|
|
1155
|
+
if (!range.fromDate || !range.toDate) {
|
|
1156
|
+
fail('tax report requires a period: --from/--to or --period.', 2);
|
|
1157
|
+
}
|
|
810
1158
|
const data = await generateTaxReport({
|
|
811
|
-
fromDate:
|
|
812
|
-
toDate:
|
|
1159
|
+
fromDate: range.fromDate,
|
|
1160
|
+
toDate: range.toDate,
|
|
813
1161
|
financialYear: opts.year,
|
|
814
1162
|
});
|
|
815
1163
|
if (json()) {
|
|
@@ -830,12 +1178,12 @@ reports
|
|
|
830
1178
|
.option('--year <number>', 'Financial year', parseInt)
|
|
831
1179
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
832
1180
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1181
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
833
1182
|
.action(async (opts) => {
|
|
834
1183
|
const { getIncomeStatement } = await import('./operations/financial-reports.js');
|
|
835
1184
|
const data = await getIncomeStatement({
|
|
836
1185
|
financialYear: opts.year,
|
|
837
|
-
|
|
838
|
-
toDate: opts.to,
|
|
1186
|
+
...fromToParams(opts),
|
|
839
1187
|
});
|
|
840
1188
|
if (json()) {
|
|
841
1189
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -907,7 +1255,7 @@ customers
|
|
|
907
1255
|
.action(async (customerNumber) => {
|
|
908
1256
|
const { getCustomer } = await import('./operations/customers.js');
|
|
909
1257
|
const data = await getCustomer(customerNumber);
|
|
910
|
-
outputDetail(data, customerDetailColumns, json());
|
|
1258
|
+
outputDetail(data, customerDetailColumns, json(), 'Customer');
|
|
911
1259
|
});
|
|
912
1260
|
customers
|
|
913
1261
|
.command('create')
|
|
@@ -932,7 +1280,7 @@ Examples:
|
|
|
932
1280
|
return;
|
|
933
1281
|
}
|
|
934
1282
|
const data = await createCustomer(params);
|
|
935
|
-
outputDetail(data, customerDetailColumns, json());
|
|
1283
|
+
outputDetail(data, customerDetailColumns, json(), 'Customer');
|
|
936
1284
|
});
|
|
937
1285
|
customers
|
|
938
1286
|
.command('update <customerNumber>')
|
|
@@ -951,7 +1299,7 @@ Examples:
|
|
|
951
1299
|
return;
|
|
952
1300
|
}
|
|
953
1301
|
const data = await updateCustomer(customerNumber, fields);
|
|
954
|
-
outputDetail(data, customerDetailColumns, json());
|
|
1302
|
+
outputDetail(data, customerDetailColumns, json(), 'Customer');
|
|
955
1303
|
});
|
|
956
1304
|
// --- articles ---
|
|
957
1305
|
const articles = program.command('articles').description('Article operations');
|
|
@@ -979,7 +1327,7 @@ articles
|
|
|
979
1327
|
.action(async (articleNumber) => {
|
|
980
1328
|
const { getArticle } = await import('./operations/articles.js');
|
|
981
1329
|
const data = await getArticle(articleNumber);
|
|
982
|
-
outputDetail(data, articleDetailColumns, json());
|
|
1330
|
+
outputDetail(data, articleDetailColumns, json(), 'Article');
|
|
983
1331
|
});
|
|
984
1332
|
articles
|
|
985
1333
|
.command('create')
|
|
@@ -1007,7 +1355,7 @@ Examples:
|
|
|
1007
1355
|
return;
|
|
1008
1356
|
}
|
|
1009
1357
|
const data = await createArticle(params);
|
|
1010
|
-
outputDetail(data, articleDetailColumns, json());
|
|
1358
|
+
outputDetail(data, articleDetailColumns, json(), 'Article');
|
|
1011
1359
|
});
|
|
1012
1360
|
articles
|
|
1013
1361
|
.command('update <articleNumber>')
|
|
@@ -1026,7 +1374,7 @@ Examples:
|
|
|
1026
1374
|
return;
|
|
1027
1375
|
}
|
|
1028
1376
|
const data = await updateArticle(articleNumber, fields);
|
|
1029
|
-
outputDetail(data, articleDetailColumns, json());
|
|
1377
|
+
outputDetail(data, articleDetailColumns, json(), 'Article');
|
|
1030
1378
|
});
|
|
1031
1379
|
// --- suppliers ---
|
|
1032
1380
|
const suppliers = program.command('suppliers').description('Supplier operations');
|
|
@@ -1054,7 +1402,7 @@ suppliers
|
|
|
1054
1402
|
.action(async (supplierNumber) => {
|
|
1055
1403
|
const { getSupplier } = await import('./operations/suppliers.js');
|
|
1056
1404
|
const data = await getSupplier(supplierNumber);
|
|
1057
|
-
outputDetail(data, supplierDetailColumns, json());
|
|
1405
|
+
outputDetail(data, supplierDetailColumns, json(), 'Supplier');
|
|
1058
1406
|
});
|
|
1059
1407
|
suppliers
|
|
1060
1408
|
.command('create')
|
|
@@ -1079,7 +1427,7 @@ Examples:
|
|
|
1079
1427
|
return;
|
|
1080
1428
|
}
|
|
1081
1429
|
const data = await createSupplier(params);
|
|
1082
|
-
outputDetail(data, supplierDetailColumns, json());
|
|
1430
|
+
outputDetail(data, supplierDetailColumns, json(), 'Supplier');
|
|
1083
1431
|
});
|
|
1084
1432
|
suppliers
|
|
1085
1433
|
.command('update <supplierNumber>')
|
|
@@ -1098,7 +1446,7 @@ Examples:
|
|
|
1098
1446
|
return;
|
|
1099
1447
|
}
|
|
1100
1448
|
const data = await updateSupplier(supplierNumber, fields);
|
|
1101
|
-
outputDetail(data, supplierDetailColumns, json());
|
|
1449
|
+
outputDetail(data, supplierDetailColumns, json(), 'Supplier');
|
|
1102
1450
|
});
|
|
1103
1451
|
// --- supplier-invoices ---
|
|
1104
1452
|
const supplierInvoices = program
|
|
@@ -1112,6 +1460,7 @@ supplierInvoices
|
|
|
1112
1460
|
.option('--supplier <number>', 'Filter by supplier number')
|
|
1113
1461
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1114
1462
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1463
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
1115
1464
|
.option('--page <number>', 'Page number', parseInt)
|
|
1116
1465
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
1117
1466
|
.option('-a, --all', 'Fetch all pages')
|
|
@@ -1120,8 +1469,7 @@ supplierInvoices
|
|
|
1120
1469
|
const data = await listSupplierInvoices({
|
|
1121
1470
|
filter: opts.filter,
|
|
1122
1471
|
supplierNumber: opts.supplier,
|
|
1123
|
-
|
|
1124
|
-
toDate: opts.to,
|
|
1472
|
+
...fromToParams(opts),
|
|
1125
1473
|
page: opts.page,
|
|
1126
1474
|
limit: opts.limit,
|
|
1127
1475
|
all: opts.all,
|
|
@@ -1135,7 +1483,7 @@ supplierInvoices
|
|
|
1135
1483
|
.action(async (givenNumber) => {
|
|
1136
1484
|
const { getSupplierInvoice } = await import('./operations/supplier-invoices.js');
|
|
1137
1485
|
const data = await getSupplierInvoice(givenNumber);
|
|
1138
|
-
outputDetail(data, supplierInvoiceDetailColumns, json());
|
|
1486
|
+
outputDetail(data, supplierInvoiceDetailColumns, json(), 'SupplierInvoice');
|
|
1139
1487
|
});
|
|
1140
1488
|
supplierInvoices
|
|
1141
1489
|
.command('create')
|
|
@@ -1158,7 +1506,7 @@ Examples:
|
|
|
1158
1506
|
return;
|
|
1159
1507
|
}
|
|
1160
1508
|
const data = await createSupplierInvoice(params);
|
|
1161
|
-
outputDetail(data, supplierInvoiceDetailColumns, json());
|
|
1509
|
+
outputDetail(data, supplierInvoiceDetailColumns, json(), 'SupplierInvoice');
|
|
1162
1510
|
});
|
|
1163
1511
|
supplierInvoices
|
|
1164
1512
|
.command('bookkeep <givenNumber>')
|
|
@@ -1171,7 +1519,7 @@ supplierInvoices
|
|
|
1171
1519
|
return;
|
|
1172
1520
|
}
|
|
1173
1521
|
const data = await bookkeepSupplierInvoice(givenNumber);
|
|
1174
|
-
outputConfirmation(`Supplier invoice ${givenNumber} bookkeept.`, json(), data, supplierInvoiceConfirmColumns);
|
|
1522
|
+
outputConfirmation(`Supplier invoice ${givenNumber} bookkeept.`, json(), data, supplierInvoiceConfirmColumns, 'SupplierInvoice');
|
|
1175
1523
|
});
|
|
1176
1524
|
// --- company ---
|
|
1177
1525
|
const company = program.command('company').description('Company operations');
|
|
@@ -1181,7 +1529,7 @@ company
|
|
|
1181
1529
|
.action(async () => {
|
|
1182
1530
|
const { getCompanyInfo } = await import('./operations/company.js');
|
|
1183
1531
|
const data = await getCompanyInfo();
|
|
1184
|
-
outputDetail(data, companyDetailColumns, json());
|
|
1532
|
+
outputDetail(data, companyDetailColumns, json(), 'CompanyInformation');
|
|
1185
1533
|
});
|
|
1186
1534
|
// --- vouchers ---
|
|
1187
1535
|
const vouchers = program.command('vouchers').description('Voucher operations');
|
|
@@ -1191,6 +1539,7 @@ vouchers
|
|
|
1191
1539
|
.option('--series <series>', 'Voucher series (e.g. "A")')
|
|
1192
1540
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1193
1541
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1542
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
1194
1543
|
.option('--year <number>', 'Financial year', parseInt)
|
|
1195
1544
|
.option('--page <number>', 'Page number', parseInt)
|
|
1196
1545
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
@@ -1199,8 +1548,7 @@ vouchers
|
|
|
1199
1548
|
const { listVouchers } = await import('./operations/vouchers.js');
|
|
1200
1549
|
const data = await listVouchers({
|
|
1201
1550
|
series: opts.series,
|
|
1202
|
-
|
|
1203
|
-
toDate: opts.to,
|
|
1551
|
+
...fromToParams(opts),
|
|
1204
1552
|
financialYear: opts.year,
|
|
1205
1553
|
page: opts.page,
|
|
1206
1554
|
limit: opts.limit,
|
|
@@ -1217,7 +1565,7 @@ vouchers
|
|
|
1217
1565
|
const { getVoucher } = await import('./operations/vouchers.js');
|
|
1218
1566
|
const data = await getVoucher(series, voucherNumber, opts.year);
|
|
1219
1567
|
if (json()) {
|
|
1220
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1568
|
+
console.log(JSON.stringify({ Voucher: data }, null, 2));
|
|
1221
1569
|
}
|
|
1222
1570
|
else {
|
|
1223
1571
|
outputDetail(data, voucherDetailColumns, false);
|
|
@@ -1247,7 +1595,35 @@ Examples:
|
|
|
1247
1595
|
return;
|
|
1248
1596
|
}
|
|
1249
1597
|
const data = await createVoucher(params);
|
|
1250
|
-
outputDetail(data, voucherDetailColumns, json());
|
|
1598
|
+
outputDetail(data, voucherDetailColumns, json(), 'Voucher');
|
|
1599
|
+
});
|
|
1600
|
+
vouchers
|
|
1601
|
+
.command('attach <series> <number> <files...>')
|
|
1602
|
+
.description('Upload receipt/underlag files and link them to a voucher')
|
|
1603
|
+
.option('--year <number>', 'Financial year (resolved from the voucher date if omitted)', parseInt)
|
|
1604
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1605
|
+
.option('--dry-run', 'Preview without uploading')
|
|
1606
|
+
.addHelpText('after', `
|
|
1607
|
+
Examples:
|
|
1608
|
+
noxctl vouchers attach A 60 receipt.pdf
|
|
1609
|
+
noxctl vouchers attach A 61 ica.pdf lunch.jpg --year 4`)
|
|
1610
|
+
.action(async (series, voucherNumber, files, opts) => {
|
|
1611
|
+
const { attachVoucherFiles } = await import('./operations/vouchers.js');
|
|
1612
|
+
if (!(await confirmMutation(`Attach ${files.length} file(s) to voucher ${series}/${voucherNumber}`, opts, { files, year: opts.year }))) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const results = await attachVoucherFiles({
|
|
1616
|
+
series,
|
|
1617
|
+
voucherNumber,
|
|
1618
|
+
filePaths: files,
|
|
1619
|
+
financialYear: opts.year,
|
|
1620
|
+
});
|
|
1621
|
+
if (json()) {
|
|
1622
|
+
console.log(JSON.stringify({ Attachments: results }, null, 2));
|
|
1623
|
+
}
|
|
1624
|
+
else {
|
|
1625
|
+
outputList(results, voucherAttachmentColumns, false, results);
|
|
1626
|
+
}
|
|
1251
1627
|
});
|
|
1252
1628
|
// --- invoice-payments ---
|
|
1253
1629
|
const invoicePayments = program
|
|
@@ -1278,7 +1654,7 @@ invoicePayments
|
|
|
1278
1654
|
.action(async (paymentNumber) => {
|
|
1279
1655
|
const { getInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
1280
1656
|
const data = await getInvoicePayment(paymentNumber);
|
|
1281
|
-
outputDetail(data, invoicePaymentDetailColumns, json());
|
|
1657
|
+
outputDetail(data, invoicePaymentDetailColumns, json(), 'InvoicePayment');
|
|
1282
1658
|
});
|
|
1283
1659
|
invoicePayments
|
|
1284
1660
|
.command('create')
|
|
@@ -1305,7 +1681,7 @@ Examples:
|
|
|
1305
1681
|
return;
|
|
1306
1682
|
}
|
|
1307
1683
|
const data = await createInvoicePayment(params);
|
|
1308
|
-
outputDetail(data, invoicePaymentDetailColumns, json());
|
|
1684
|
+
outputDetail(data, invoicePaymentDetailColumns, json(), 'InvoicePayment');
|
|
1309
1685
|
});
|
|
1310
1686
|
invoicePayments
|
|
1311
1687
|
.command('delete <paymentNumber>')
|
|
@@ -1318,7 +1694,7 @@ invoicePayments
|
|
|
1318
1694
|
return;
|
|
1319
1695
|
}
|
|
1320
1696
|
await deleteInvoicePayment(paymentNumber);
|
|
1321
|
-
outputConfirmation(`Invoice payment ${paymentNumber} deleted.`, json(), {});
|
|
1697
|
+
outputConfirmation(`Invoice payment ${paymentNumber} deleted.`, json(), { Number: paymentNumber, deleted: true }, undefined, 'InvoicePayment');
|
|
1322
1698
|
});
|
|
1323
1699
|
invoicePayments
|
|
1324
1700
|
.command('bookkeep <paymentNumber>')
|
|
@@ -1331,7 +1707,7 @@ invoicePayments
|
|
|
1331
1707
|
return;
|
|
1332
1708
|
}
|
|
1333
1709
|
const result = await bookkeepInvoicePayment(paymentNumber);
|
|
1334
|
-
outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result);
|
|
1710
|
+
outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result, undefined, 'InvoicePayment');
|
|
1335
1711
|
});
|
|
1336
1712
|
// --- supplier-invoice-payments ---
|
|
1337
1713
|
const supplierInvoicePayments = program
|
|
@@ -1362,7 +1738,7 @@ supplierInvoicePayments
|
|
|
1362
1738
|
.action(async (paymentNumber) => {
|
|
1363
1739
|
const { getSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
1364
1740
|
const data = await getSupplierInvoicePayment(paymentNumber);
|
|
1365
|
-
outputDetail(data, supplierInvoicePaymentDetailColumns, json());
|
|
1741
|
+
outputDetail(data, supplierInvoicePaymentDetailColumns, json(), 'SupplierInvoicePayment');
|
|
1366
1742
|
});
|
|
1367
1743
|
supplierInvoicePayments
|
|
1368
1744
|
.command('create')
|
|
@@ -1387,7 +1763,7 @@ Examples:
|
|
|
1387
1763
|
return;
|
|
1388
1764
|
}
|
|
1389
1765
|
const data = await createSupplierInvoicePayment(params);
|
|
1390
|
-
outputDetail(data, supplierInvoicePaymentDetailColumns, json());
|
|
1766
|
+
outputDetail(data, supplierInvoicePaymentDetailColumns, json(), 'SupplierInvoicePayment');
|
|
1391
1767
|
});
|
|
1392
1768
|
supplierInvoicePayments
|
|
1393
1769
|
.command('delete <paymentNumber>')
|
|
@@ -1400,7 +1776,7 @@ supplierInvoicePayments
|
|
|
1400
1776
|
return;
|
|
1401
1777
|
}
|
|
1402
1778
|
await deleteSupplierInvoicePayment(paymentNumber);
|
|
1403
|
-
outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), {});
|
|
1779
|
+
outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), { Number: paymentNumber, deleted: true }, undefined, 'SupplierInvoicePayment');
|
|
1404
1780
|
});
|
|
1405
1781
|
// --- offers ---
|
|
1406
1782
|
const offers = program.command('offers').description('Offer/quote operations (offerter)');
|
|
@@ -1411,6 +1787,7 @@ offers
|
|
|
1411
1787
|
.option('--customer <number>', 'Filter by customer number')
|
|
1412
1788
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1413
1789
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1790
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
1414
1791
|
.option('--page <number>', 'Page number', parseInt)
|
|
1415
1792
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
1416
1793
|
.option('-a, --all', 'Fetch all pages')
|
|
@@ -1419,8 +1796,7 @@ offers
|
|
|
1419
1796
|
const data = await listOffers({
|
|
1420
1797
|
filter: opts.filter,
|
|
1421
1798
|
customerNumber: opts.customer,
|
|
1422
|
-
|
|
1423
|
-
toDate: opts.to,
|
|
1799
|
+
...fromToParams(opts),
|
|
1424
1800
|
page: opts.page,
|
|
1425
1801
|
limit: opts.limit,
|
|
1426
1802
|
all: opts.all,
|
|
@@ -1434,7 +1810,7 @@ offers
|
|
|
1434
1810
|
.action(async (documentNumber) => {
|
|
1435
1811
|
const { getOffer } = await import('./operations/offers.js');
|
|
1436
1812
|
const data = await getOffer(documentNumber);
|
|
1437
|
-
outputDetail(data, offerDetailColumns, json());
|
|
1813
|
+
outputDetail(data, offerDetailColumns, json(), 'Offer');
|
|
1438
1814
|
});
|
|
1439
1815
|
offers
|
|
1440
1816
|
.command('create')
|
|
@@ -1457,7 +1833,7 @@ Examples:
|
|
|
1457
1833
|
return;
|
|
1458
1834
|
}
|
|
1459
1835
|
const data = await createOffer(params);
|
|
1460
|
-
outputDetail(data, offerDetailColumns, json());
|
|
1836
|
+
outputDetail(data, offerDetailColumns, json(), 'Offer');
|
|
1461
1837
|
});
|
|
1462
1838
|
offers
|
|
1463
1839
|
.command('update <documentNumber>')
|
|
@@ -1473,7 +1849,7 @@ offers
|
|
|
1473
1849
|
return;
|
|
1474
1850
|
}
|
|
1475
1851
|
const data = await updateOffer(documentNumber, fields);
|
|
1476
|
-
outputDetail(data, offerDetailColumns, json());
|
|
1852
|
+
outputDetail(data, offerDetailColumns, json(), 'Offer');
|
|
1477
1853
|
});
|
|
1478
1854
|
offers
|
|
1479
1855
|
.command('create-invoice <documentNumber>')
|
|
@@ -1486,7 +1862,7 @@ offers
|
|
|
1486
1862
|
return;
|
|
1487
1863
|
}
|
|
1488
1864
|
const data = await createInvoiceFromOffer(documentNumber);
|
|
1489
|
-
outputConfirmation(`Invoice created from offer ${documentNumber}.`, json(), data, invoiceConfirmColumns);
|
|
1865
|
+
outputConfirmation(`Invoice created from offer ${documentNumber}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1490
1866
|
});
|
|
1491
1867
|
offers
|
|
1492
1868
|
.command('create-order <documentNumber>')
|
|
@@ -1499,7 +1875,7 @@ offers
|
|
|
1499
1875
|
return;
|
|
1500
1876
|
}
|
|
1501
1877
|
const data = await createOrderFromOffer(documentNumber);
|
|
1502
|
-
outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data);
|
|
1878
|
+
outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data, undefined, 'Order');
|
|
1503
1879
|
});
|
|
1504
1880
|
// --- orders ---
|
|
1505
1881
|
const orders = program.command('orders').description('Order operations (ordrar)');
|
|
@@ -1510,6 +1886,7 @@ orders
|
|
|
1510
1886
|
.option('--customer <number>', 'Filter by customer number')
|
|
1511
1887
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1512
1888
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1889
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, this-quarter, last-quarter, this-month, last-month, ytd, this-year, last-year, or a bare year. Mutually exclusive with --from/--to.')
|
|
1513
1890
|
.option('--page <number>', 'Page number', parseInt)
|
|
1514
1891
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
1515
1892
|
.option('-a, --all', 'Fetch all pages')
|
|
@@ -1518,8 +1895,7 @@ orders
|
|
|
1518
1895
|
const data = await listOrders({
|
|
1519
1896
|
filter: opts.filter,
|
|
1520
1897
|
customerNumber: opts.customer,
|
|
1521
|
-
|
|
1522
|
-
toDate: opts.to,
|
|
1898
|
+
...fromToParams(opts),
|
|
1523
1899
|
page: opts.page,
|
|
1524
1900
|
limit: opts.limit,
|
|
1525
1901
|
all: opts.all,
|
|
@@ -1533,7 +1909,7 @@ orders
|
|
|
1533
1909
|
.action(async (documentNumber) => {
|
|
1534
1910
|
const { getOrder } = await import('./operations/orders.js');
|
|
1535
1911
|
const data = await getOrder(documentNumber);
|
|
1536
|
-
outputDetail(data, orderDetailColumns, json());
|
|
1912
|
+
outputDetail(data, orderDetailColumns, json(), 'Order');
|
|
1537
1913
|
});
|
|
1538
1914
|
orders
|
|
1539
1915
|
.command('create')
|
|
@@ -1556,7 +1932,7 @@ Examples:
|
|
|
1556
1932
|
return;
|
|
1557
1933
|
}
|
|
1558
1934
|
const data = await createOrder(params);
|
|
1559
|
-
outputDetail(data, orderDetailColumns, json());
|
|
1935
|
+
outputDetail(data, orderDetailColumns, json(), 'Order');
|
|
1560
1936
|
});
|
|
1561
1937
|
orders
|
|
1562
1938
|
.command('update <documentNumber>')
|
|
@@ -1572,7 +1948,7 @@ orders
|
|
|
1572
1948
|
return;
|
|
1573
1949
|
}
|
|
1574
1950
|
const data = await updateOrder(documentNumber, fields);
|
|
1575
|
-
outputDetail(data, orderDetailColumns, json());
|
|
1951
|
+
outputDetail(data, orderDetailColumns, json(), 'Order');
|
|
1576
1952
|
});
|
|
1577
1953
|
orders
|
|
1578
1954
|
.command('create-invoice <documentNumber>')
|
|
@@ -1585,7 +1961,7 @@ orders
|
|
|
1585
1961
|
return;
|
|
1586
1962
|
}
|
|
1587
1963
|
const data = await createInvoiceFromOrder(documentNumber);
|
|
1588
|
-
outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns);
|
|
1964
|
+
outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1589
1965
|
});
|
|
1590
1966
|
// --- projects ---
|
|
1591
1967
|
const projects = program.command('projects').description('Project operations');
|
|
@@ -1611,7 +1987,7 @@ projects
|
|
|
1611
1987
|
.action(async (projectNumber) => {
|
|
1612
1988
|
const { getProject } = await import('./operations/projects.js');
|
|
1613
1989
|
const data = await getProject(projectNumber);
|
|
1614
|
-
outputDetail(data, projectDetailColumns, json());
|
|
1990
|
+
outputDetail(data, projectDetailColumns, json(), 'Project');
|
|
1615
1991
|
});
|
|
1616
1992
|
projects
|
|
1617
1993
|
.command('create')
|
|
@@ -1638,7 +2014,7 @@ projects
|
|
|
1638
2014
|
return;
|
|
1639
2015
|
}
|
|
1640
2016
|
const data = await createProject(params);
|
|
1641
|
-
outputDetail(data, projectDetailColumns, json());
|
|
2017
|
+
outputDetail(data, projectDetailColumns, json(), 'Project');
|
|
1642
2018
|
});
|
|
1643
2019
|
projects
|
|
1644
2020
|
.command('update <projectNumber>')
|
|
@@ -1654,7 +2030,7 @@ projects
|
|
|
1654
2030
|
return;
|
|
1655
2031
|
}
|
|
1656
2032
|
const data = await updateProject(projectNumber, fields);
|
|
1657
|
-
outputDetail(data, projectDetailColumns, json());
|
|
2033
|
+
outputDetail(data, projectDetailColumns, json(), 'Project');
|
|
1658
2034
|
});
|
|
1659
2035
|
// --- cost centers ---
|
|
1660
2036
|
const costcenters = program.command('costcenters').description('Cost center operations');
|
|
@@ -1680,7 +2056,7 @@ costcenters
|
|
|
1680
2056
|
.action(async (code) => {
|
|
1681
2057
|
const { getCostCenter } = await import('./operations/costcenters.js');
|
|
1682
2058
|
const data = await getCostCenter(code);
|
|
1683
|
-
outputDetail(data, costCenterDetailColumns, json());
|
|
2059
|
+
outputDetail(data, costCenterDetailColumns, json(), 'CostCenter');
|
|
1684
2060
|
});
|
|
1685
2061
|
costcenters
|
|
1686
2062
|
.command('create')
|
|
@@ -1706,7 +2082,7 @@ costcenters
|
|
|
1706
2082
|
return;
|
|
1707
2083
|
}
|
|
1708
2084
|
const data = await createCostCenter(params);
|
|
1709
|
-
outputDetail(data, costCenterDetailColumns, json());
|
|
2085
|
+
outputDetail(data, costCenterDetailColumns, json(), 'CostCenter');
|
|
1710
2086
|
});
|
|
1711
2087
|
costcenters
|
|
1712
2088
|
.command('update <code>')
|
|
@@ -1722,7 +2098,7 @@ costcenters
|
|
|
1722
2098
|
return;
|
|
1723
2099
|
}
|
|
1724
2100
|
const data = await updateCostCenter(code, fields);
|
|
1725
|
-
outputDetail(data, costCenterDetailColumns, json());
|
|
2101
|
+
outputDetail(data, costCenterDetailColumns, json(), 'CostCenter');
|
|
1726
2102
|
});
|
|
1727
2103
|
costcenters
|
|
1728
2104
|
.command('delete <code>')
|
|
@@ -1735,7 +2111,384 @@ costcenters
|
|
|
1735
2111
|
return;
|
|
1736
2112
|
}
|
|
1737
2113
|
await deleteCostCenter(code);
|
|
1738
|
-
|
|
2114
|
+
outputConfirmation(`Cost center ${code} deleted.`, json(), { Code: code, deleted: true }, undefined, 'CostCenter');
|
|
2115
|
+
});
|
|
2116
|
+
// --- employees (payroll / Lön) ---
|
|
2117
|
+
const employees = program
|
|
2118
|
+
.command('employees')
|
|
2119
|
+
.description('Employee operations (requires the Lön scope — see `noxctl init --with-salary`)');
|
|
2120
|
+
employees
|
|
2121
|
+
.command('list')
|
|
2122
|
+
.description('List employees')
|
|
2123
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
2124
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
2125
|
+
.option('-a, --all', 'Fetch all pages')
|
|
2126
|
+
.action(async (opts) => {
|
|
2127
|
+
const { listEmployees } = await import('./operations/employees.js');
|
|
2128
|
+
const data = await listEmployees({
|
|
2129
|
+
page: opts.page,
|
|
2130
|
+
limit: opts.limit,
|
|
2131
|
+
all: opts.all,
|
|
2132
|
+
});
|
|
2133
|
+
const envelope = data;
|
|
2134
|
+
outputList(envelope.Employees ?? [], employeeListColumns, json(), data, envelope.MetaInformation);
|
|
2135
|
+
});
|
|
2136
|
+
employees
|
|
2137
|
+
.command('get <employeeId>')
|
|
2138
|
+
.description('Get a single employee')
|
|
2139
|
+
.action(async (employeeId) => {
|
|
2140
|
+
const { getEmployee } = await import('./operations/employees.js');
|
|
2141
|
+
const data = await getEmployee(employeeId);
|
|
2142
|
+
outputDetail(data, employeeDetailColumns, json(), 'Employee');
|
|
2143
|
+
});
|
|
2144
|
+
employees
|
|
2145
|
+
.command('create')
|
|
2146
|
+
.description('Create an employee. Tip: set --employment-form, --personel-type and --salary-form, otherwise Fortnox cannot assign an employment agreement (företagsavtal).')
|
|
2147
|
+
.requiredOption('--first-name <name>', 'First name')
|
|
2148
|
+
.requiredOption('--last-name <name>', 'Last name')
|
|
2149
|
+
.requiredOption('--email <email>', 'Email address')
|
|
2150
|
+
.option('--employment-form <form>', 'Employment form: TV, PRO, TID, SVT, VIK, PRJ, PRA, FER, SES, NEJ')
|
|
2151
|
+
.option('--personel-type <type>', 'Personnel type: TJM (tjänsteman) or ARB (arbetare)')
|
|
2152
|
+
.option('--salary-form <form>', 'Salary form: MAN (monthly) or TIM (hourly)')
|
|
2153
|
+
.option('--employment-date <date>', 'Employment start date (YYYY-MM-DD)')
|
|
2154
|
+
.option('--monthly-salary <amount>', 'Monthly salary')
|
|
2155
|
+
.option('--hourly-pay <amount>', 'Hourly pay')
|
|
2156
|
+
.option('--input <file>', 'Additional employee data as JSON file (or - for stdin)')
|
|
2157
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2158
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2159
|
+
.action(async (opts) => {
|
|
2160
|
+
const { createEmployee } = await import('./operations/employees.js');
|
|
2161
|
+
let input = {};
|
|
2162
|
+
if (opts.input) {
|
|
2163
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2164
|
+
input = JSON.parse(raw);
|
|
2165
|
+
}
|
|
2166
|
+
const params = {
|
|
2167
|
+
...input,
|
|
2168
|
+
FirstName: opts.firstName,
|
|
2169
|
+
LastName: opts.lastName,
|
|
2170
|
+
Email: opts.email,
|
|
2171
|
+
};
|
|
2172
|
+
if (opts.employmentForm !== undefined)
|
|
2173
|
+
params.EmploymentForm = opts.employmentForm;
|
|
2174
|
+
if (opts.personelType !== undefined)
|
|
2175
|
+
params.PersonelType = opts.personelType;
|
|
2176
|
+
if (opts.salaryForm !== undefined)
|
|
2177
|
+
params.SalaryForm = opts.salaryForm;
|
|
2178
|
+
if (opts.employmentDate !== undefined)
|
|
2179
|
+
params.EmploymentDate = opts.employmentDate;
|
|
2180
|
+
if (opts.monthlySalary !== undefined)
|
|
2181
|
+
params.MonthlySalary = opts.monthlySalary;
|
|
2182
|
+
if (opts.hourlyPay !== undefined)
|
|
2183
|
+
params.HourlyPay = opts.hourlyPay;
|
|
2184
|
+
if (!(await confirmMutation(`Create employee "${opts.firstName} ${opts.lastName}"`, opts, {
|
|
2185
|
+
Employee: params,
|
|
2186
|
+
}))) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const data = await createEmployee(params);
|
|
2190
|
+
outputDetail(data, employeeDetailColumns, json(), 'Employee');
|
|
2191
|
+
});
|
|
2192
|
+
employees
|
|
2193
|
+
.command('update <employeeId>')
|
|
2194
|
+
.description('Update an employee')
|
|
2195
|
+
.requiredOption('--input <file>', 'Employee data as JSON file (or - for stdin)')
|
|
2196
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2197
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2198
|
+
.action(async (employeeId, opts) => {
|
|
2199
|
+
const { updateEmployee } = await import('./operations/employees.js');
|
|
2200
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2201
|
+
const fields = JSON.parse(raw);
|
|
2202
|
+
if (!(await confirmMutation(`Update employee ${employeeId}`, opts, { Employee: fields }))) {
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
const data = await updateEmployee(employeeId, fields);
|
|
2206
|
+
outputDetail(data, employeeDetailColumns, json(), 'Employee');
|
|
2207
|
+
});
|
|
2208
|
+
// --- salary transactions (Lön) ---
|
|
2209
|
+
const salarytransactions = program
|
|
2210
|
+
.command('salary-transactions')
|
|
2211
|
+
.description('Salary transaction operations (requires the Lön scope)');
|
|
2212
|
+
salarytransactions
|
|
2213
|
+
.command('list')
|
|
2214
|
+
.description('List salary transactions')
|
|
2215
|
+
.option('--employee <id>', 'Filter by employee ID')
|
|
2216
|
+
.option('--date <date>', 'Filter by date (YYYY-MM-DD)')
|
|
2217
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
2218
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
2219
|
+
.option('-a, --all', 'Fetch all pages')
|
|
2220
|
+
.action(async (opts) => {
|
|
2221
|
+
const { listSalaryTransactions } = await import('./operations/salarytransactions.js');
|
|
2222
|
+
const data = await listSalaryTransactions({
|
|
2223
|
+
employeeId: opts.employee,
|
|
2224
|
+
date: opts.date,
|
|
2225
|
+
page: opts.page,
|
|
2226
|
+
limit: opts.limit,
|
|
2227
|
+
all: opts.all,
|
|
2228
|
+
});
|
|
2229
|
+
const envelope = data;
|
|
2230
|
+
outputList(envelope.SalaryTransactions ?? [], salaryTransactionListColumns, json(), data, envelope.MetaInformation);
|
|
2231
|
+
});
|
|
2232
|
+
salarytransactions
|
|
2233
|
+
.command('get <salaryRow>')
|
|
2234
|
+
.description('Get a single salary transaction')
|
|
2235
|
+
.action(async (salaryRow) => {
|
|
2236
|
+
const { getSalaryTransaction } = await import('./operations/salarytransactions.js');
|
|
2237
|
+
const data = await getSalaryTransaction(salaryRow);
|
|
2238
|
+
outputDetail(data, salaryTransactionDetailColumns, json(), 'SalaryTransaction');
|
|
2239
|
+
});
|
|
2240
|
+
salarytransactions
|
|
2241
|
+
.command('create')
|
|
2242
|
+
.description('Create a salary transaction')
|
|
2243
|
+
.requiredOption('--employee <id>', 'Employee ID')
|
|
2244
|
+
.requiredOption('--salary-code <code>', 'Salary code')
|
|
2245
|
+
.requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
|
|
2246
|
+
.option('--amount <amount>', 'Amount')
|
|
2247
|
+
.option('--input <file>', 'Additional data as JSON file (or - for stdin)')
|
|
2248
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2249
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2250
|
+
.action(async (opts) => {
|
|
2251
|
+
const { createSalaryTransaction } = await import('./operations/salarytransactions.js');
|
|
2252
|
+
let input = {};
|
|
2253
|
+
if (opts.input) {
|
|
2254
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2255
|
+
input = JSON.parse(raw);
|
|
2256
|
+
}
|
|
2257
|
+
const params = {
|
|
2258
|
+
...input,
|
|
2259
|
+
EmployeeId: opts.employee,
|
|
2260
|
+
SalaryCode: opts.salaryCode,
|
|
2261
|
+
Date: opts.date,
|
|
2262
|
+
};
|
|
2263
|
+
if (opts.amount !== undefined)
|
|
2264
|
+
params.Amount = opts.amount;
|
|
2265
|
+
if (!(await confirmMutation(`Create salary transaction for employee ${opts.employee}`, opts, {
|
|
2266
|
+
SalaryTransaction: params,
|
|
2267
|
+
}))) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const data = await createSalaryTransaction(params);
|
|
2271
|
+
outputDetail(data, salaryTransactionDetailColumns, json(), 'SalaryTransaction');
|
|
2272
|
+
});
|
|
2273
|
+
salarytransactions
|
|
2274
|
+
.command('delete <salaryRow>')
|
|
2275
|
+
.description('Delete a salary transaction')
|
|
2276
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2277
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2278
|
+
.action(async (salaryRow, opts) => {
|
|
2279
|
+
const { deleteSalaryTransaction } = await import('./operations/salarytransactions.js');
|
|
2280
|
+
if (!(await confirmMutation(`Delete salary transaction ${salaryRow}`, opts))) {
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
await deleteSalaryTransaction(salaryRow);
|
|
2284
|
+
outputConfirmation(`Salary transaction ${salaryRow} deleted.`, json(), { SalaryRow: salaryRow, deleted: true }, undefined, 'SalaryTransaction');
|
|
2285
|
+
});
|
|
2286
|
+
// --- attendance transactions (närvaro / Lön) ---
|
|
2287
|
+
const attendancetransactions = program
|
|
2288
|
+
.command('attendance-transactions')
|
|
2289
|
+
.description('Attendance transaction operations (requires the Lön scope)');
|
|
2290
|
+
attendancetransactions
|
|
2291
|
+
.command('list')
|
|
2292
|
+
.description('List attendance transactions')
|
|
2293
|
+
.option('--employee <id>', 'Filter by employee ID')
|
|
2294
|
+
.option('--date <date>', 'Filter by date (YYYY-MM-DD)')
|
|
2295
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
2296
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
2297
|
+
.option('-a, --all', 'Fetch all pages')
|
|
2298
|
+
.action(async (opts) => {
|
|
2299
|
+
const { listAttendanceTransactions } = await import('./operations/attendancetransactions.js');
|
|
2300
|
+
const data = await listAttendanceTransactions({
|
|
2301
|
+
employeeId: opts.employee,
|
|
2302
|
+
date: opts.date,
|
|
2303
|
+
page: opts.page,
|
|
2304
|
+
limit: opts.limit,
|
|
2305
|
+
all: opts.all,
|
|
2306
|
+
});
|
|
2307
|
+
const envelope = data;
|
|
2308
|
+
outputList(envelope.AttendanceTransactions ?? [], attendanceTransactionListColumns, json(), data, envelope.MetaInformation);
|
|
2309
|
+
});
|
|
2310
|
+
attendancetransactions
|
|
2311
|
+
.command('get <id>')
|
|
2312
|
+
.description('Get a single attendance transaction')
|
|
2313
|
+
.action(async (id) => {
|
|
2314
|
+
const { getAttendanceTransaction } = await import('./operations/attendancetransactions.js');
|
|
2315
|
+
const data = await getAttendanceTransaction(id);
|
|
2316
|
+
outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
|
|
2317
|
+
});
|
|
2318
|
+
attendancetransactions
|
|
2319
|
+
.command('create')
|
|
2320
|
+
.description('Create an attendance transaction')
|
|
2321
|
+
.requiredOption('--employee <id>', 'Employee ID')
|
|
2322
|
+
.requiredOption('--cause-code <code>', 'Cause code (e.g. ARB, FLX, OT1)')
|
|
2323
|
+
.requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
|
|
2324
|
+
.option('--hours <hours>', 'Hours')
|
|
2325
|
+
.option('--input <file>', 'Additional data as JSON file (or - for stdin)')
|
|
2326
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2327
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2328
|
+
.action(async (opts) => {
|
|
2329
|
+
const { createAttendanceTransaction } = await import('./operations/attendancetransactions.js');
|
|
2330
|
+
let input = {};
|
|
2331
|
+
if (opts.input) {
|
|
2332
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2333
|
+
input = JSON.parse(raw);
|
|
2334
|
+
}
|
|
2335
|
+
const params = {
|
|
2336
|
+
...input,
|
|
2337
|
+
EmployeeId: opts.employee,
|
|
2338
|
+
CauseCode: opts.causeCode,
|
|
2339
|
+
Date: opts.date,
|
|
2340
|
+
};
|
|
2341
|
+
if (opts.hours !== undefined)
|
|
2342
|
+
params.Hours = opts.hours;
|
|
2343
|
+
if (!(await confirmMutation(`Create attendance transaction for employee ${opts.employee}`, opts, {
|
|
2344
|
+
AttendanceTransaction: params,
|
|
2345
|
+
}))) {
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
const data = await createAttendanceTransaction(params);
|
|
2349
|
+
outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
|
|
2350
|
+
});
|
|
2351
|
+
attendancetransactions
|
|
2352
|
+
.command('delete <id>')
|
|
2353
|
+
.description('Delete an attendance transaction')
|
|
2354
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2355
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2356
|
+
.action(async (id, opts) => {
|
|
2357
|
+
const { deleteAttendanceTransaction } = await import('./operations/attendancetransactions.js');
|
|
2358
|
+
if (!(await confirmMutation(`Delete attendance transaction ${id}`, opts))) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
await deleteAttendanceTransaction(id);
|
|
2362
|
+
outputConfirmation(`Attendance transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AttendanceTransaction');
|
|
2363
|
+
});
|
|
2364
|
+
// --- absence transactions (frånvaro / Lön) ---
|
|
2365
|
+
const absencetransactions = program
|
|
2366
|
+
.command('absence-transactions')
|
|
2367
|
+
.description('Absence transaction operations (requires the Lön scope)');
|
|
2368
|
+
absencetransactions
|
|
2369
|
+
.command('list')
|
|
2370
|
+
.description('List absence transactions')
|
|
2371
|
+
.option('--employee <id>', 'Filter by employee ID')
|
|
2372
|
+
.option('--date <date>', 'Filter by date (YYYY-MM-DD)')
|
|
2373
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
2374
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
2375
|
+
.option('-a, --all', 'Fetch all pages')
|
|
2376
|
+
.action(async (opts) => {
|
|
2377
|
+
const { listAbsenceTransactions } = await import('./operations/absencetransactions.js');
|
|
2378
|
+
const data = await listAbsenceTransactions({
|
|
2379
|
+
employeeId: opts.employee,
|
|
2380
|
+
date: opts.date,
|
|
2381
|
+
page: opts.page,
|
|
2382
|
+
limit: opts.limit,
|
|
2383
|
+
all: opts.all,
|
|
2384
|
+
});
|
|
2385
|
+
const envelope = data;
|
|
2386
|
+
outputList(envelope.AbsenceTransactions ?? [], absenceTransactionListColumns, json(), data, envelope.MetaInformation);
|
|
2387
|
+
});
|
|
2388
|
+
absencetransactions
|
|
2389
|
+
.command('get <id>')
|
|
2390
|
+
.description('Get a single absence transaction')
|
|
2391
|
+
.action(async (id) => {
|
|
2392
|
+
const { getAbsenceTransaction } = await import('./operations/absencetransactions.js');
|
|
2393
|
+
const data = await getAbsenceTransaction(id);
|
|
2394
|
+
outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
|
|
2395
|
+
});
|
|
2396
|
+
absencetransactions
|
|
2397
|
+
.command('create')
|
|
2398
|
+
.description('Create an absence transaction')
|
|
2399
|
+
.requiredOption('--employee <id>', 'Employee ID')
|
|
2400
|
+
.requiredOption('--cause-code <code>', 'Cause code (e.g. SEM, SJK, VAB)')
|
|
2401
|
+
.requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
|
|
2402
|
+
.option('--hours <hours>', 'Hours (number, e.g. 8)', parseFloat)
|
|
2403
|
+
.option('--extent <extent>', 'Extent / percentage absent (number, e.g. 50)', parseFloat)
|
|
2404
|
+
.option('--input <file>', 'Additional data as JSON file (or - for stdin)')
|
|
2405
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2406
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2407
|
+
.action(async (opts) => {
|
|
2408
|
+
const { createAbsenceTransaction } = await import('./operations/absencetransactions.js');
|
|
2409
|
+
let input = {};
|
|
2410
|
+
if (opts.input) {
|
|
2411
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2412
|
+
input = JSON.parse(raw);
|
|
2413
|
+
}
|
|
2414
|
+
const params = {
|
|
2415
|
+
...input,
|
|
2416
|
+
EmployeeId: opts.employee,
|
|
2417
|
+
CauseCode: opts.causeCode,
|
|
2418
|
+
Date: opts.date,
|
|
2419
|
+
};
|
|
2420
|
+
if (opts.hours !== undefined)
|
|
2421
|
+
params.Hours = opts.hours;
|
|
2422
|
+
if (opts.extent !== undefined)
|
|
2423
|
+
params.Extent = opts.extent;
|
|
2424
|
+
if (!(await confirmMutation(`Create absence transaction for employee ${opts.employee}`, opts, {
|
|
2425
|
+
AbsenceTransaction: params,
|
|
2426
|
+
}))) {
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const data = await createAbsenceTransaction(params);
|
|
2430
|
+
outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
|
|
2431
|
+
});
|
|
2432
|
+
absencetransactions
|
|
2433
|
+
.command('delete <id>')
|
|
2434
|
+
.description('Delete an absence transaction')
|
|
2435
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2436
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2437
|
+
.action(async (id, opts) => {
|
|
2438
|
+
const { deleteAbsenceTransaction } = await import('./operations/absencetransactions.js');
|
|
2439
|
+
if (!(await confirmMutation(`Delete absence transaction ${id}`, opts))) {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
await deleteAbsenceTransaction(id);
|
|
2443
|
+
outputConfirmation(`Absence transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AbsenceTransaction');
|
|
2444
|
+
});
|
|
2445
|
+
// --- schedule times (schematider / Lön) ---
|
|
2446
|
+
const scheduletimes = program
|
|
2447
|
+
.command('schedule-times')
|
|
2448
|
+
.description('Schedule time operations (requires the Lön scope)');
|
|
2449
|
+
scheduletimes
|
|
2450
|
+
.command('get <employeeId> <date>')
|
|
2451
|
+
.description('Get the schedule for an employee on a date (YYYY-MM-DD)')
|
|
2452
|
+
.action(async (employeeId, date) => {
|
|
2453
|
+
const { getScheduleTime } = await import('./operations/scheduletimes.js');
|
|
2454
|
+
const data = await getScheduleTime(employeeId, date);
|
|
2455
|
+
outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
|
|
2456
|
+
});
|
|
2457
|
+
scheduletimes
|
|
2458
|
+
.command('update <employeeId> <date>')
|
|
2459
|
+
.description('Update the schedule for an employee on a date')
|
|
2460
|
+
.requiredOption('--input <file>', 'Schedule data as JSON file (or - for stdin)')
|
|
2461
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2462
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2463
|
+
.action(async (employeeId, date, opts) => {
|
|
2464
|
+
const { updateScheduleTime } = await import('./operations/scheduletimes.js');
|
|
2465
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2466
|
+
const fields = JSON.parse(raw);
|
|
2467
|
+
if (!(await confirmMutation(`Update schedule for ${employeeId} on ${date}`, opts, {
|
|
2468
|
+
ScheduleTime: fields,
|
|
2469
|
+
}))) {
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
const data = await updateScheduleTime(employeeId, date, fields);
|
|
2473
|
+
outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
|
|
2474
|
+
});
|
|
2475
|
+
scheduletimes
|
|
2476
|
+
.command('reset-day <employeeId> <date>')
|
|
2477
|
+
.description('Update the schedule and reset the day for an employee on a date')
|
|
2478
|
+
.requiredOption('--input <file>', 'Schedule data as JSON file (or - for stdin)')
|
|
2479
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2480
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2481
|
+
.action(async (employeeId, date, opts) => {
|
|
2482
|
+
const { resetScheduleTimeDay } = await import('./operations/scheduletimes.js');
|
|
2483
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2484
|
+
const fields = JSON.parse(raw);
|
|
2485
|
+
if (!(await confirmMutation(`Reset schedule day for ${employeeId} on ${date}`, opts, {
|
|
2486
|
+
ScheduleTime: fields,
|
|
2487
|
+
}))) {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const data = await resetScheduleTimeDay(employeeId, date, fields);
|
|
2491
|
+
outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
|
|
1739
2492
|
});
|
|
1740
2493
|
// --- tax reductions (ROT/RUT) ---
|
|
1741
2494
|
const taxreductions = program
|
|
@@ -1765,7 +2518,7 @@ taxreductions
|
|
|
1765
2518
|
.action(async (id) => {
|
|
1766
2519
|
const { getTaxReduction } = await import('./operations/taxreductions.js');
|
|
1767
2520
|
const data = await getTaxReduction(parseInt(id, 10));
|
|
1768
|
-
outputDetail(data, taxReductionDetailColumns, json());
|
|
2521
|
+
outputDetail(data, taxReductionDetailColumns, json(), 'TaxReduction');
|
|
1769
2522
|
});
|
|
1770
2523
|
taxreductions
|
|
1771
2524
|
.command('create')
|
|
@@ -1800,7 +2553,7 @@ taxreductions
|
|
|
1800
2553
|
return;
|
|
1801
2554
|
}
|
|
1802
2555
|
const data = await createTaxReduction(params);
|
|
1803
|
-
outputDetail(data, taxReductionDetailColumns, json());
|
|
2556
|
+
outputDetail(data, taxReductionDetailColumns, json(), 'TaxReduction');
|
|
1804
2557
|
});
|
|
1805
2558
|
// --- price lists ---
|
|
1806
2559
|
const pricelists = program.command('pricelists').description('Price list operations');
|
|
@@ -1826,7 +2579,7 @@ pricelists
|
|
|
1826
2579
|
.action(async (code) => {
|
|
1827
2580
|
const { getPriceList } = await import('./operations/pricelists.js');
|
|
1828
2581
|
const data = await getPriceList(code);
|
|
1829
|
-
outputDetail(data, priceListDetailColumns, json());
|
|
2582
|
+
outputDetail(data, priceListDetailColumns, json(), 'PriceList');
|
|
1830
2583
|
});
|
|
1831
2584
|
pricelists
|
|
1832
2585
|
.command('create')
|
|
@@ -1852,7 +2605,7 @@ pricelists
|
|
|
1852
2605
|
return;
|
|
1853
2606
|
}
|
|
1854
2607
|
const data = await createPriceList(params);
|
|
1855
|
-
outputDetail(data, priceListDetailColumns, json());
|
|
2608
|
+
outputDetail(data, priceListDetailColumns, json(), 'PriceList');
|
|
1856
2609
|
});
|
|
1857
2610
|
pricelists
|
|
1858
2611
|
.command('update <code>')
|
|
@@ -1868,7 +2621,7 @@ pricelists
|
|
|
1868
2621
|
return;
|
|
1869
2622
|
}
|
|
1870
2623
|
const data = await updatePriceList(code, fields);
|
|
1871
|
-
outputDetail(data, priceListDetailColumns, json());
|
|
2624
|
+
outputDetail(data, priceListDetailColumns, json(), 'PriceList');
|
|
1872
2625
|
});
|
|
1873
2626
|
// --- prices ---
|
|
1874
2627
|
const prices = program.command('prices').description('Price operations within price lists');
|
|
@@ -1899,7 +2652,7 @@ prices
|
|
|
1899
2652
|
.action(async (opts) => {
|
|
1900
2653
|
const { getPrice } = await import('./operations/pricelists.js');
|
|
1901
2654
|
const data = await getPrice(opts.pricelist, opts.article, opts.fromQuantity);
|
|
1902
|
-
outputDetail(data, priceDetailColumns, json());
|
|
2655
|
+
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
1903
2656
|
});
|
|
1904
2657
|
prices
|
|
1905
2658
|
.command('update')
|
|
@@ -1920,25 +2673,295 @@ prices
|
|
|
1920
2673
|
return;
|
|
1921
2674
|
}
|
|
1922
2675
|
const data = await updatePrice(opts.pricelist, opts.article, fields, opts.fromQuantity);
|
|
1923
|
-
outputDetail(data, priceDetailColumns, json());
|
|
2676
|
+
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
1924
2677
|
});
|
|
1925
|
-
//
|
|
1926
|
-
program
|
|
2678
|
+
// --- contracts ---
|
|
2679
|
+
const contracts = program
|
|
2680
|
+
.command('contracts')
|
|
2681
|
+
.description('Contract operations (avtal — recurring invoicing)');
|
|
2682
|
+
contracts
|
|
2683
|
+
.command('list')
|
|
2684
|
+
.description('List/filter contracts')
|
|
2685
|
+
.option('--filter <filter>', 'Filter: active, inactive, finished')
|
|
2686
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
2687
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
2688
|
+
.option('-a, --all', 'Fetch all pages')
|
|
2689
|
+
.action(async (opts) => {
|
|
2690
|
+
const { listContracts } = await import('./operations/contracts.js');
|
|
2691
|
+
const data = await listContracts({
|
|
2692
|
+
filter: opts.filter,
|
|
2693
|
+
page: opts.page,
|
|
2694
|
+
limit: opts.limit,
|
|
2695
|
+
all: opts.all,
|
|
2696
|
+
});
|
|
2697
|
+
outputList(data.Contracts ?? [], contractListColumns, json(), data, data.MetaInformation);
|
|
2698
|
+
});
|
|
2699
|
+
contracts
|
|
2700
|
+
.command('get <documentNumber>')
|
|
2701
|
+
.description('Get a single contract')
|
|
2702
|
+
.action(async (documentNumber) => {
|
|
2703
|
+
const { getContract } = await import('./operations/contracts.js');
|
|
2704
|
+
const data = await getContract(documentNumber);
|
|
2705
|
+
outputDetail(data, contractDetailColumns, json(), 'Contract');
|
|
2706
|
+
});
|
|
2707
|
+
contracts
|
|
2708
|
+
.command('create')
|
|
2709
|
+
.description('Create a contract (recurring invoicing)')
|
|
2710
|
+
.requiredOption('--customer <number>', 'Customer number')
|
|
2711
|
+
.requiredOption('--input <file>', 'Contract data as JSON file (or - for stdin)')
|
|
2712
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2713
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2714
|
+
.addHelpText('after', `
|
|
2715
|
+
Examples:
|
|
2716
|
+
echo '{"InvoiceRows":[{"Description":"Hosting","DeliveredQuantity":1,"Price":500,"AccountNumber":3001,"VAT":25}],"PeriodStart":"2026-07-01","PeriodEnd":"2027-06-30","InvoiceInterval":3,"ContractLength":12}' | noxctl contracts create --customer 25 --input - --dry-run`)
|
|
2717
|
+
.action(async (opts) => {
|
|
2718
|
+
const { createContract } = await import('./operations/contracts.js');
|
|
2719
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2720
|
+
const input = JSON.parse(raw);
|
|
2721
|
+
const params = { CustomerNumber: opts.customer, ...input };
|
|
2722
|
+
if (!(await confirmMutation(`Create contract for customer ${opts.customer}`, opts, {
|
|
2723
|
+
Contract: params,
|
|
2724
|
+
}))) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
const data = await createContract(params);
|
|
2728
|
+
outputDetail(data, contractDetailColumns, json(), 'Contract');
|
|
2729
|
+
});
|
|
2730
|
+
contracts
|
|
2731
|
+
.command('update <documentNumber>')
|
|
2732
|
+
.description('Update a contract')
|
|
2733
|
+
.requiredOption('--input <file>', 'Fields to update as JSON file (or - for stdin)')
|
|
2734
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2735
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2736
|
+
.action(async (documentNumber, opts) => {
|
|
2737
|
+
const { updateContract } = await import('./operations/contracts.js');
|
|
2738
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2739
|
+
const fields = JSON.parse(raw);
|
|
2740
|
+
if (!(await confirmMutation(`Update contract ${documentNumber}`, opts, { Contract: fields }))) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const data = await updateContract(documentNumber, fields);
|
|
2744
|
+
outputDetail(data, contractDetailColumns, json(), 'Contract');
|
|
2745
|
+
});
|
|
2746
|
+
contracts
|
|
2747
|
+
.command('finish <documentNumber>')
|
|
2748
|
+
.description('Finish a contract — no further invoices will be created')
|
|
2749
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2750
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2751
|
+
.action(async (documentNumber, opts) => {
|
|
2752
|
+
const { finishContract } = await import('./operations/contracts.js');
|
|
2753
|
+
if (!(await confirmMutation(`Finish contract ${documentNumber}`, opts))) {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
const data = await finishContract(documentNumber);
|
|
2757
|
+
outputConfirmation(`Contract ${documentNumber} finished.`, json(), data, contractDetailColumns, 'Contract');
|
|
2758
|
+
});
|
|
2759
|
+
contracts
|
|
2760
|
+
.command('create-invoice <documentNumber>')
|
|
2761
|
+
.description('Create the next invoice from a contract immediately')
|
|
2762
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2763
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2764
|
+
.action(async (documentNumber, opts) => {
|
|
2765
|
+
const { createInvoiceFromContract } = await import('./operations/contracts.js');
|
|
2766
|
+
if (!(await confirmMutation(`Create invoice from contract ${documentNumber}`, opts))) {
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
const data = await createInvoiceFromContract(documentNumber);
|
|
2770
|
+
outputConfirmation(`Invoice created from contract ${documentNumber}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
2771
|
+
});
|
|
2772
|
+
contracts
|
|
2773
|
+
.command('increase-invoice-count <documentNumber>')
|
|
2774
|
+
.description('Extend a non-continuous contract by one invoice')
|
|
2775
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2776
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2777
|
+
.action(async (documentNumber, opts) => {
|
|
2778
|
+
const { increaseInvoiceCount } = await import('./operations/contracts.js');
|
|
2779
|
+
if (!(await confirmMutation(`Increase invoice count for contract ${documentNumber}`, opts))) {
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const data = await increaseInvoiceCount(documentNumber);
|
|
2783
|
+
outputConfirmation(`Invoice count increased for contract ${documentNumber}.`, json(), data, contractDetailColumns, 'Contract');
|
|
2784
|
+
});
|
|
2785
|
+
// --- financial years ---
|
|
2786
|
+
const financialYears = program
|
|
2787
|
+
.command('financial-years')
|
|
2788
|
+
.description('Financial year and locked period operations');
|
|
2789
|
+
financialYears
|
|
2790
|
+
.command('list')
|
|
2791
|
+
.description('List financial years (räkenskapsår)')
|
|
2792
|
+
.option('--date <date>', 'Find the financial year containing this date (YYYY-MM-DD)')
|
|
2793
|
+
.action(async (opts) => {
|
|
2794
|
+
const { listFinancialYears } = await import('./operations/financial-years.js');
|
|
2795
|
+
const data = await listFinancialYears({ date: opts.date });
|
|
2796
|
+
outputList(data.FinancialYears ?? [], financialYearListColumns, json(), data, data.MetaInformation);
|
|
2797
|
+
});
|
|
2798
|
+
financialYears
|
|
2799
|
+
.command('get <id>')
|
|
2800
|
+
.description('Get a single financial year')
|
|
2801
|
+
.action(async (id) => {
|
|
2802
|
+
const { getFinancialYear } = await import('./operations/financial-years.js');
|
|
2803
|
+
const data = await getFinancialYear(parseInt(id, 10));
|
|
2804
|
+
outputDetail(data, financialYearDetailColumns, json(), 'FinancialYear');
|
|
2805
|
+
});
|
|
2806
|
+
financialYears
|
|
2807
|
+
.command('locked-period')
|
|
2808
|
+
.description('Show the locked period (bokföring låst t.o.m.)')
|
|
2809
|
+
.action(async () => {
|
|
2810
|
+
const { getLockedPeriod } = await import('./operations/financial-years.js');
|
|
2811
|
+
const data = await getLockedPeriod();
|
|
2812
|
+
if (json()) {
|
|
2813
|
+
console.log(JSON.stringify({ LockedPeriod: data }, null, 2));
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (!data.EndDate) {
|
|
2817
|
+
console.log('No period is locked.');
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
outputDetail(data, lockedPeriodDetailColumns, false);
|
|
2821
|
+
});
|
|
2822
|
+
// --- analytics ---
|
|
2823
|
+
const analytics = program
|
|
2824
|
+
.command('analytics')
|
|
2825
|
+
.description('Precomputed analytics views (overdue, unpaid, top customers, VAT)');
|
|
2826
|
+
analytics
|
|
2827
|
+
.command('overdue')
|
|
2828
|
+
.description('Overdue invoices summary')
|
|
2829
|
+
.action(async () => {
|
|
2830
|
+
const { getOverdueSummary } = await import('./operations/analytics.js');
|
|
2831
|
+
const summary = await getOverdueSummary();
|
|
2832
|
+
if (json()) {
|
|
2833
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (summary.count === 0) {
|
|
2837
|
+
console.log('No overdue invoices.');
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
console.log(`Overdue: ${summary.count} invoice(s), ${summary.totalBalance.toFixed(2)} outstanding. Oldest due ${summary.oldestDueDate}.\n`);
|
|
2841
|
+
outputList(summary.invoices, invoiceListColumns, false, summary.invoices);
|
|
2842
|
+
});
|
|
2843
|
+
analytics
|
|
2844
|
+
.command('unpaid')
|
|
2845
|
+
.description('Unpaid totals (outstanding receivables)')
|
|
2846
|
+
.action(async () => {
|
|
2847
|
+
const { getUnpaidTotals } = await import('./operations/analytics.js');
|
|
2848
|
+
const s = await getUnpaidTotals();
|
|
2849
|
+
if (json()) {
|
|
2850
|
+
console.log(JSON.stringify(s, null, 2));
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
console.log(`Unpaid: ${s.count} invoice(s), ${s.totalBalance.toFixed(2)} outstanding.`);
|
|
2854
|
+
console.log(`Overdue: ${s.overdueCount} invoice(s), ${s.overdueBalance.toFixed(2)}.`);
|
|
2855
|
+
});
|
|
2856
|
+
analytics
|
|
2857
|
+
.command('top-customers')
|
|
2858
|
+
.description('Top customers by invoiced amount')
|
|
2859
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
2860
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
2861
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, last-quarter, ytd, ... Mutually exclusive with --from/--to.')
|
|
2862
|
+
.option('--limit <number>', 'Number of customers (default 10)', parseInt)
|
|
2863
|
+
.action(async (opts) => {
|
|
2864
|
+
const { getTopCustomers } = await import('./operations/analytics.js');
|
|
2865
|
+
const range = fromToParams(opts);
|
|
2866
|
+
const result = await getTopCustomers({
|
|
2867
|
+
fromDate: range.fromDate,
|
|
2868
|
+
toDate: range.toDate,
|
|
2869
|
+
limit: opts.limit,
|
|
2870
|
+
});
|
|
2871
|
+
if (json()) {
|
|
2872
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
outputList(result.customers.map((c) => ({ ...c })), topCustomerColumns, false, result.customers);
|
|
2876
|
+
});
|
|
2877
|
+
analytics
|
|
2878
|
+
.command('vat')
|
|
2879
|
+
.description('VAT summary for a period (net VAT position)')
|
|
2880
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
2881
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
2882
|
+
.option('--period <period>', 'Natural period (calendar-year): Q1, 2025-Q3, march/mars, last-quarter, ytd, ... Mutually exclusive with --from/--to.')
|
|
2883
|
+
.option('--year <number>', 'Financial year', parseInt)
|
|
2884
|
+
.action(async (opts) => {
|
|
2885
|
+
const { getVatSummary } = await import('./operations/analytics.js');
|
|
2886
|
+
const range = fromToParams(opts);
|
|
2887
|
+
if (!range.fromDate || !range.toDate) {
|
|
2888
|
+
fail('analytics vat requires a period: --from/--to or --period.', 2);
|
|
2889
|
+
}
|
|
2890
|
+
const summary = await getVatSummary({
|
|
2891
|
+
fromDate: range.fromDate,
|
|
2892
|
+
toDate: range.toDate,
|
|
2893
|
+
financialYear: opts.year,
|
|
2894
|
+
});
|
|
2895
|
+
if (json()) {
|
|
2896
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
console.log(formatTaxReport(summary));
|
|
2900
|
+
console.log(`\nNet VAT: ${summary.netVat.toFixed(2)} (negative = owed to Skatteverket)`);
|
|
2901
|
+
});
|
|
2902
|
+
// --- dashboard ---
|
|
2903
|
+
program
|
|
2904
|
+
.command('dashboard')
|
|
2905
|
+
.description('At-a-glance summary: recent invoices, outstanding, overdue, monthly revenue')
|
|
2906
|
+
.option('--months <number>', 'Months of revenue history (default 6)', parseInt)
|
|
2907
|
+
.action(async (opts) => {
|
|
2908
|
+
const { getDashboard } = await import('./operations/analytics.js');
|
|
2909
|
+
const dash = await getDashboard({ months: opts.months });
|
|
2910
|
+
if (json()) {
|
|
2911
|
+
console.log(JSON.stringify(dash, null, 2));
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
console.log('OUTSTANDING');
|
|
2915
|
+
console.log(` Unpaid: ${dash.unpaid.count} invoice(s), ${dash.unpaid.totalBalance.toFixed(2)}`);
|
|
2916
|
+
console.log(` Overdue: ${dash.overdue.count} invoice(s), ${dash.overdue.totalBalance.toFixed(2)}` +
|
|
2917
|
+
(dash.overdue.oldestDueDate ? ` (oldest due ${dash.overdue.oldestDueDate})` : ''));
|
|
2918
|
+
if (dash.overdue.count > 0) {
|
|
2919
|
+
console.log('\nOVERDUE INVOICES');
|
|
2920
|
+
outputList(dash.overdue.invoices, invoiceListColumns, false, dash.overdue.invoices);
|
|
2921
|
+
}
|
|
2922
|
+
console.log('\nRECENT INVOICES');
|
|
2923
|
+
outputList(dash.recentInvoices, invoiceListColumns, false, dash.recentInvoices);
|
|
2924
|
+
console.log('\nMONTHLY INVOICED');
|
|
2925
|
+
outputList(dash.monthlyRevenue.map((m) => ({ ...m })), monthlyRevenueColumns, false, dash.monthlyRevenue);
|
|
2926
|
+
});
|
|
2927
|
+
// --- completion ---
|
|
2928
|
+
program
|
|
2929
|
+
.command('completion <shell>')
|
|
2930
|
+
.description('Generate shell completion script (bash, zsh, fish)')
|
|
2931
|
+
.addHelpText('after', `
|
|
2932
|
+
Examples:
|
|
2933
|
+
noxctl completion bash > /usr/local/etc/bash_completion.d/noxctl
|
|
2934
|
+
noxctl completion zsh > "\${fpath[1]}/_noxctl"
|
|
2935
|
+
noxctl completion fish > ~/.config/fish/completions/noxctl.fish`)
|
|
2936
|
+
.action(async (shell) => {
|
|
2937
|
+
const { extractCommandTree, renderCompletion } = await import('./completions.js');
|
|
2938
|
+
console.log(renderCompletion(shell, extractCommandTree(program)));
|
|
2939
|
+
});
|
|
2940
|
+
// Error handling (configureOutput + exitOverride set above, before the command
|
|
2941
|
+
// tree, so subcommands inherit them).
|
|
1927
2942
|
try {
|
|
1928
2943
|
await program.parseAsync(process.argv);
|
|
1929
2944
|
}
|
|
1930
2945
|
catch (err) {
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2946
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
2947
|
+
// Commander throws these for --help and --version after writing output; exit 0.
|
|
2948
|
+
if (code === 'commander.helpDisplayed' || code === 'commander.version') {
|
|
2949
|
+
process.exit(0);
|
|
2950
|
+
}
|
|
2951
|
+
// Commander parse errors (unknownCommand, missingMandatoryOptionValue,
|
|
2952
|
+
// excessArguments, ...) carry a `commander.*` code. configureOutput above
|
|
2953
|
+
// already wrote their plain-text usage message in table mode, so we must not
|
|
2954
|
+
// print it again here; in JSON mode that output was suppressed, so we emit
|
|
2955
|
+
// the structured envelope below instead.
|
|
2956
|
+
const isCommanderParseError = typeof code === 'string' && code.startsWith('commander.');
|
|
2957
|
+
if (json()) {
|
|
2958
|
+
// Structured mode fails structured: scripted callers branch on .error
|
|
2959
|
+
// instead of string-scraping stderr.
|
|
2960
|
+
console.error(JSON.stringify(errorEnvelope(err), null, 2));
|
|
2961
|
+
}
|
|
2962
|
+
else if (!isCommanderParseError) {
|
|
2963
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1940
2964
|
}
|
|
1941
|
-
console.error(err instanceof Error ? err.message : err);
|
|
1942
2965
|
process.exit(1);
|
|
1943
2966
|
}
|
|
1944
2967
|
//# sourceMappingURL=cli.js.map
|