@aderis-erp/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +244 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/access-groups.d.ts +39 -0
  7. package/dist/commands/access-groups.js +1004 -0
  8. package/dist/commands/account-statement-runs.d.ts +40 -0
  9. package/dist/commands/account-statement-runs.js +1004 -0
  10. package/dist/commands/account-transactions.d.ts +41 -0
  11. package/dist/commands/account-transactions.js +1020 -0
  12. package/dist/commands/accounts.d.ts +40 -0
  13. package/dist/commands/accounts.js +1012 -0
  14. package/dist/commands/agent-runs.d.ts +39 -0
  15. package/dist/commands/agent-runs.js +1001 -0
  16. package/dist/commands/audit-log.d.ts +39 -0
  17. package/dist/commands/audit-log.js +1000 -0
  18. package/dist/commands/auth/login.d.ts +14 -0
  19. package/dist/commands/auth/login.js +116 -0
  20. package/dist/commands/auth/logout.d.ts +10 -0
  21. package/dist/commands/auth/logout.js +58 -0
  22. package/dist/commands/auth/status.d.ts +12 -0
  23. package/dist/commands/auth/status.js +101 -0
  24. package/dist/commands/auth/tenant.d.ts +18 -0
  25. package/dist/commands/auth/tenant.js +168 -0
  26. package/dist/commands/auth.d.ts +37 -0
  27. package/dist/commands/auth.js +871 -0
  28. package/dist/commands/bank-accounts.d.ts +41 -0
  29. package/dist/commands/bank-accounts.js +1020 -0
  30. package/dist/commands/bank-integrations.d.ts +39 -0
  31. package/dist/commands/bank-integrations.js +1011 -0
  32. package/dist/commands/bank-statements.d.ts +40 -0
  33. package/dist/commands/bank-statements.js +998 -0
  34. package/dist/commands/business-partners---contacts.d.ts +41 -0
  35. package/dist/commands/business-partners---contacts.js +1006 -0
  36. package/dist/commands/business-partners.d.ts +40 -0
  37. package/dist/commands/business-partners.js +1012 -0
  38. package/dist/commands/config/add.d.ts +13 -0
  39. package/dist/commands/config/add.js +120 -0
  40. package/dist/commands/config/current.d.ts +9 -0
  41. package/dist/commands/config/current.js +43 -0
  42. package/dist/commands/config/delete.d.ts +12 -0
  43. package/dist/commands/config/delete.js +67 -0
  44. package/dist/commands/config/list.d.ts +9 -0
  45. package/dist/commands/config/list.js +50 -0
  46. package/dist/commands/config/use.d.ts +9 -0
  47. package/dist/commands/config/use.js +40 -0
  48. package/dist/commands/credit-card-accounts.d.ts +41 -0
  49. package/dist/commands/credit-card-accounts.js +1018 -0
  50. package/dist/commands/documents.d.ts +39 -0
  51. package/dist/commands/documents.js +1005 -0
  52. package/dist/commands/dunning-configs.d.ts +39 -0
  53. package/dist/commands/dunning-configs.js +1006 -0
  54. package/dist/commands/dunning-reminders.d.ts +39 -0
  55. package/dist/commands/dunning-reminders.js +1007 -0
  56. package/dist/commands/dunning-runs.d.ts +39 -0
  57. package/dist/commands/dunning-runs.js +995 -0
  58. package/dist/commands/employees.d.ts +43 -0
  59. package/dist/commands/employees.js +1037 -0
  60. package/dist/commands/event-processor-mappings.d.ts +39 -0
  61. package/dist/commands/event-processor-mappings.js +1005 -0
  62. package/dist/commands/files-internal.d.ts +39 -0
  63. package/dist/commands/files-internal.js +1007 -0
  64. package/dist/commands/health.d.ts +39 -0
  65. package/dist/commands/health.js +1001 -0
  66. package/dist/commands/inbox.d.ts +39 -0
  67. package/dist/commands/inbox.js +997 -0
  68. package/dist/commands/integrations.d.ts +40 -0
  69. package/dist/commands/integrations.js +1001 -0
  70. package/dist/commands/item-categories.d.ts +39 -0
  71. package/dist/commands/item-categories.js +1006 -0
  72. package/dist/commands/items.d.ts +39 -0
  73. package/dist/commands/items.js +1006 -0
  74. package/dist/commands/journal-lines.d.ts +39 -0
  75. package/dist/commands/journal-lines.js +998 -0
  76. package/dist/commands/journals.d.ts +41 -0
  77. package/dist/commands/journals.js +1022 -0
  78. package/dist/commands/ledgers.d.ts +41 -0
  79. package/dist/commands/ledgers.js +1019 -0
  80. package/dist/commands/office-locations.d.ts +39 -0
  81. package/dist/commands/office-locations.js +1006 -0
  82. package/dist/commands/payment-method-ledger-mappings.d.ts +40 -0
  83. package/dist/commands/payment-method-ledger-mappings.js +1000 -0
  84. package/dist/commands/payment-methods.d.ts +39 -0
  85. package/dist/commands/payment-methods.js +1006 -0
  86. package/dist/commands/payment-runs.d.ts +41 -0
  87. package/dist/commands/payment-runs.js +1021 -0
  88. package/dist/commands/payment-terms.d.ts +39 -0
  89. package/dist/commands/payment-terms.js +1007 -0
  90. package/dist/commands/price-lists.d.ts +40 -0
  91. package/dist/commands/price-lists.js +1012 -0
  92. package/dist/commands/processing-logs.d.ts +39 -0
  93. package/dist/commands/processing-logs.js +997 -0
  94. package/dist/commands/processor-mappings.d.ts +39 -0
  95. package/dist/commands/processor-mappings.js +1004 -0
  96. package/dist/commands/processors.d.ts +39 -0
  97. package/dist/commands/processors.js +1010 -0
  98. package/dist/commands/reconciliation.d.ts +39 -0
  99. package/dist/commands/reconciliation.js +1003 -0
  100. package/dist/commands/reports.d.ts +39 -0
  101. package/dist/commands/reports.js +996 -0
  102. package/dist/commands/salary-types.d.ts +39 -0
  103. package/dist/commands/salary-types.js +1006 -0
  104. package/dist/commands/sales-invoices.d.ts +42 -0
  105. package/dist/commands/sales-invoices.js +1033 -0
  106. package/dist/commands/sales-orders.d.ts +42 -0
  107. package/dist/commands/sales-orders.js +1030 -0
  108. package/dist/commands/switch.d.ts +6 -0
  109. package/dist/commands/switch.js +41 -0
  110. package/dist/commands/tags.d.ts +39 -0
  111. package/dist/commands/tags.js +1006 -0
  112. package/dist/commands/tax-codes.d.ts +39 -0
  113. package/dist/commands/tax-codes.js +1009 -0
  114. package/dist/commands/tax-matrix.d.ts +39 -0
  115. package/dist/commands/tax-matrix.js +1006 -0
  116. package/dist/commands/tenants.d.ts +39 -0
  117. package/dist/commands/tenants.js +1006 -0
  118. package/dist/commands/text-templates.d.ts +39 -0
  119. package/dist/commands/text-templates.js +1006 -0
  120. package/dist/commands/tools.d.ts +39 -0
  121. package/dist/commands/tools.js +999 -0
  122. package/dist/commands/vendor-invoices.d.ts +43 -0
  123. package/dist/commands/vendor-invoices.js +1037 -0
  124. package/dist/commands/webhooks.d.ts +39 -0
  125. package/dist/commands/webhooks.js +1008 -0
  126. package/dist/commands/workflows.d.ts +39 -0
  127. package/dist/commands/workflows.js +1000 -0
  128. package/dist/hooks/command-not-found.d.ts +3 -0
  129. package/dist/hooks/command-not-found.js +57 -0
  130. package/dist/index.d.ts +1 -0
  131. package/dist/index.js +1 -0
  132. package/dist/lib/api-client.d.ts +40 -0
  133. package/dist/lib/api-client.js +120 -0
  134. package/dist/lib/auth.d.ts +20 -0
  135. package/dist/lib/auth.js +431 -0
  136. package/dist/lib/base-api-command.d.ts +19 -0
  137. package/dist/lib/base-api-command.js +173 -0
  138. package/dist/lib/config.d.ts +40 -0
  139. package/dist/lib/config.js +113 -0
  140. package/dist/lib/openapi.d.ts +82 -0
  141. package/dist/lib/openapi.js +100 -0
  142. package/oclif.manifest.json +9040 -0
  143. package/package.json +91 -0
@@ -0,0 +1,1033 @@
1
+ import { Command, Args, Flags } from '@oclif/core';
2
+ import { apiRequest, ApiError } from '../lib/api-client.js';
3
+ import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
4
+ // Add 'import' action if both create and update exist
5
+ const HAS_IMPORT = true;
6
+ const HAS_EXPORT = true;
7
+ const HAS_CAMT053 = false;
8
+ const NESTED_IMPORTS = ["line-items", "notes", "files"];
9
+ const SUBCOMMANDS = [...["list", "create", "create-migrated-invoice", "pdf", "get", "update", "delete", "approve", "validate", "request-approval", "send", "send-resend", "cancel", "copy", "replace", "create-line-items", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
+ const ENDPOINTS = {
11
+ 'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false },
12
+ 'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false },
13
+ 'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false },
14
+ 'pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: 'Generate a PDF preview of a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false },
15
+ 'get': { method: 'GET', path: '/v1/sales-invoices/{id}', summary: 'Get a sales invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
16
+ 'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false },
17
+ 'delete': { method: 'DELETE', path: '/v1/sales-invoices/{id}', summary: 'Delete a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false },
18
+ 'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false },
19
+ 'validate': { method: 'POST', path: '/v1/sales-invoices/{id}/validate', summary: 'Validate a sales invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false },
20
+ 'request-approval': { method: 'POST', path: '/v1/sales-invoices/{id}/request-approval', summary: 'Request approval for sales invoice (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false },
21
+ 'send': { method: 'POST', path: '/v1/sales-invoices/{id}/send', summary: 'Mark invoice as sent to customer (APPROVED → SENT)', pathParams: ['id'], hasBody: false, isMultipart: false },
22
+ 'send-resend': { method: 'POST', path: '/v1/sales-invoices/{id}/resend', summary: 'Resend a sales invoice to the original delivery recipient or an alternative one', pathParams: ['id'], hasBody: true, isMultipart: false },
23
+ 'cancel': { method: 'POST', path: '/v1/sales-invoices/{id}/cancel', summary: 'Cancel a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false },
24
+ 'copy': { method: 'POST', path: '/v1/sales-invoices/{id}/copy', summary: 'Create a copy of a cancelled invoice', pathParams: ['id'], hasBody: false, isMultipart: false },
25
+ 'replace': { method: 'POST', path: '/v1/sales-invoices/{id}/replace', summary: 'Replace invoice with corrected version (creates new CORRECTED_INVOICE)', pathParams: ['id'], hasBody: false, isMultipart: false },
26
+ 'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false },
27
+ 'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false },
28
+ 'delete-line-items': { method: 'DELETE', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false },
29
+ 'list-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Get all notes for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false },
30
+ 'create-notes': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Create a note for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false },
31
+ 'get-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Get a specific note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false },
32
+ 'update-notes': { method: 'PATCH', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Update a note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: true, isMultipart: false },
33
+ 'delete-notes': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Delete a note from a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false },
34
+ 'list-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files', summary: 'List file attachments for a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: false },
35
+ 'create-files': { method: 'POST', path: '/v1/sales-invoices/{parentId}/files', summary: 'Upload a file attachment to a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: true },
36
+ 'get-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Get a file attachment for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
37
+ 'update-files': { method: 'PATCH', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false },
38
+ 'delete-files': { method: 'DELETE', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
39
+ 'files-download': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
40
+ };
41
+ export default class SalesInvoices extends Command {
42
+ static description = 'Manage sales invoices';
43
+ static examples = [
44
+ '<%= config.bin %> sales-invoices list',
45
+ '<%= config.bin %> sales-invoices get <id>',
46
+ '<%= config.bin %> sales-invoices create --data \'{...}\'',
47
+ '<%= config.bin %> sales-invoices update <id> --set field=value',
48
+ '<%= config.bin %> sales-invoices import --file data.json',
49
+ '<%= config.bin %> sales-invoices export --csv > sales-invoices.csv',
50
+ ];
51
+ static args = {
52
+ action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
53
+ id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
54
+ };
55
+ static flags = {
56
+ context: Flags.string({ char: 'c', description: 'Context to use' }),
57
+ output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
58
+ table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
59
+ csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
60
+ data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
61
+ file: Flags.string({ char: 'f', description: 'Read request body from file' }),
62
+ set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
63
+ 'line-item-id': Flags.string({ description: 'Path param: lineItemId' }),
64
+ 'note-id': Flags.string({ description: 'Path param: noteId' }),
65
+ 'file-id': Flags.string({ description: 'Path param: fileId' }),
66
+ page: Flags.string({ description: 'Page number' }),
67
+ limit: Flags.string({ description: 'Items per page' }),
68
+ include: Flags.string({ description: 'Relations to include' }),
69
+ search: Flags.string({ description: 'Search term' }),
70
+ 'sort-by': Flags.string({ description: 'Sort field' }),
71
+ filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
72
+ query: Flags.string({ description: 'Additional query params as JSON' }),
73
+ fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
74
+ sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
75
+ 'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
76
+ };
77
+ async run() {
78
+ const { args, flags } = await this.parse(SalesInvoices);
79
+ // Handle import action specially
80
+ if (args.action === 'import') {
81
+ if (!HAS_IMPORT)
82
+ this.error('Import not available for this resource');
83
+ await this.runImport(flags);
84
+ return;
85
+ }
86
+ // Handle export action specially
87
+ if (args.action === 'export') {
88
+ if (!HAS_EXPORT)
89
+ this.error('Export not available for this resource');
90
+ await this.runExport(flags);
91
+ return;
92
+ }
93
+ // Handle CAMT.053 import specially - sends raw XML
94
+ if (args.action === 'import-camt053') {
95
+ if (!HAS_CAMT053)
96
+ this.error('CAMT.053 import not available for this resource');
97
+ await this.runCamt053Import(flags);
98
+ return;
99
+ }
100
+ // Handle nested resource imports (e.g., import-prices)
101
+ if (args.action.startsWith('import-')) {
102
+ const suffix = args.action.replace('import-', '');
103
+ if (NESTED_IMPORTS.includes(suffix)) {
104
+ await this.runNestedImport(suffix, args.id, flags);
105
+ return;
106
+ }
107
+ }
108
+ const endpoint = ENDPOINTS[args.action];
109
+ if (!endpoint)
110
+ this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
111
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
112
+ if (!context)
113
+ this.error('No context configured. Run: aderis auth login --new');
114
+ if (!context.tokens)
115
+ this.error('Not logged in. Run: aderis auth login');
116
+ let apiPath = endpoint.path;
117
+ for (let i = 0; i < endpoint.pathParams.length; i++) {
118
+ const param = endpoint.pathParams[i];
119
+ let value;
120
+ if (i === 0) {
121
+ // First path param always comes from positional argument
122
+ value = args.id;
123
+ if (!value)
124
+ this.error('Missing required argument: id. Usage: aderis sales-invoices <action> <id>');
125
+ }
126
+ else {
127
+ const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
128
+ value = flags[flagName];
129
+ if (!value)
130
+ this.error(`Missing required parameter: --${flagName}`);
131
+ }
132
+ apiPath = apiPath.replace(`{${param}}`, String(value));
133
+ }
134
+ const queryParams = {};
135
+ if (flags.page)
136
+ queryParams.page = flags.page;
137
+ if (flags.limit)
138
+ queryParams.limit = flags.limit;
139
+ if (flags.include)
140
+ queryParams.include = flags.include;
141
+ if (flags.search)
142
+ queryParams.search = flags.search;
143
+ if (flags['sort-by'])
144
+ queryParams.sortBy = flags['sort-by'];
145
+ if (flags.query) {
146
+ try {
147
+ Object.assign(queryParams, JSON.parse(flags.query));
148
+ }
149
+ catch {
150
+ this.error('Invalid JSON in --query');
151
+ }
152
+ }
153
+ // Parse --filter flags (e.g., --filter status=ACTIVE becomes filter.status=$eq:ACTIVE)
154
+ if (flags.filter && flags.filter.length > 0) {
155
+ for (const f of flags.filter) {
156
+ const eqIndex = f.indexOf('=');
157
+ if (eqIndex === -1)
158
+ this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
159
+ const key = f.slice(0, eqIndex);
160
+ let value = f.slice(eqIndex + 1);
161
+ // Check for operator prefix (with or without $)
162
+ const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
163
+ if (opMatch) {
164
+ value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
165
+ }
166
+ else {
167
+ value = `\$eq:${value}`;
168
+ }
169
+ const filterKey = `filter.${key}`;
170
+ if (filterKey in queryParams) {
171
+ // Multiple filters on same field: collect as array
172
+ const existing = queryParams[filterKey];
173
+ queryParams[filterKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
174
+ }
175
+ else {
176
+ queryParams[filterKey] = value;
177
+ }
178
+ }
179
+ }
180
+ let data;
181
+ if (endpoint.isMultipart) {
182
+ if (!flags.file)
183
+ this.error('Missing required --file flag for file upload');
184
+ const fsModule = await import('node:fs');
185
+ const pathModule = await import('node:path');
186
+ const FormDataLib = (await import('form-data')).default;
187
+ const form = new FormDataLib();
188
+ form.append('file', fsModule.createReadStream(flags.file), pathModule.basename(flags.file));
189
+ // Append --set fields as extra form fields (e.g. --set type=CREDIT_CARD_STATEMENT)
190
+ if (flags.set && flags.set.length > 0) {
191
+ for (const pair of flags.set) {
192
+ const eqIndex = pair.indexOf('=');
193
+ if (eqIndex === -1)
194
+ this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
195
+ form.append(pair.slice(0, eqIndex), pair.slice(eqIndex + 1));
196
+ }
197
+ }
198
+ data = form;
199
+ }
200
+ else if (endpoint.hasBody) {
201
+ if (flags.file) {
202
+ const fsModule = await import('node:fs');
203
+ try {
204
+ data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
205
+ }
206
+ catch {
207
+ this.error(`Failed to read file: ${flags.file}`);
208
+ }
209
+ }
210
+ else if (flags.data) {
211
+ try {
212
+ data = JSON.parse(flags.data);
213
+ }
214
+ catch {
215
+ this.error('Invalid JSON in --data');
216
+ }
217
+ }
218
+ // Apply --set flags (can be combined with --data or --file)
219
+ if (flags.set && flags.set.length > 0) {
220
+ if (!data)
221
+ data = {};
222
+ const body = data;
223
+ for (const pair of flags.set) {
224
+ const eqIndex = pair.indexOf('=');
225
+ if (eqIndex === -1)
226
+ this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
227
+ const key = pair.slice(0, eqIndex);
228
+ let value = pair.slice(eqIndex + 1);
229
+ // Try to parse as JSON for numbers, booleans, arrays, objects
230
+ try {
231
+ value = JSON.parse(value);
232
+ }
233
+ catch { /* keep as string */ }
234
+ // Support nested keys like "address.city"
235
+ const keys = key.split('.');
236
+ if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
237
+ this.error(`Invalid --set key: ${key}`);
238
+ }
239
+ let obj = body;
240
+ for (let i = 0; i < keys.length - 1; i++) {
241
+ if (!(keys[i] in obj) || typeof obj[keys[i]] !== 'object') {
242
+ obj[keys[i]] = {};
243
+ }
244
+ obj = obj[keys[i]];
245
+ }
246
+ obj[keys[keys.length - 1]] = value;
247
+ }
248
+ }
249
+ }
250
+ else if (flags.set && flags.set.length > 0) {
251
+ // For GET endpoints, --set adds query params
252
+ for (const pair of flags.set) {
253
+ const eqIndex = pair.indexOf('=');
254
+ if (eqIndex === -1)
255
+ this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
256
+ const key = pair.slice(0, eqIndex);
257
+ let value = pair.slice(eqIndex + 1);
258
+ try {
259
+ value = JSON.parse(value);
260
+ }
261
+ catch { /* keep as string */ }
262
+ queryParams[key] = value;
263
+ }
264
+ }
265
+ // Show HTTP request details if --show-http-request flag is set
266
+ if (flags['show-http-request']) {
267
+ const baseUrls = getBaseUrls(context.environment);
268
+ const queryString = Object.keys(queryParams).length > 0
269
+ ? '?' + Object.entries(queryParams).flatMap(([k, v]) => {
270
+ const vals = Array.isArray(v) ? v : [v];
271
+ return vals.map(val => `${encodeURIComponent(k)}=${encodeURIComponent(String(val))}`);
272
+ }).join('&')
273
+ : '';
274
+ process.stderr.write(`\n${endpoint.method.toUpperCase()} ${baseUrls.api}${apiPath}${queryString}\n`);
275
+ if (endpoint.isMultipart) {
276
+ process.stderr.write(`Body: [multipart/form-data file upload]\n`);
277
+ }
278
+ else if (data) {
279
+ process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
280
+ }
281
+ process.stderr.write('\n');
282
+ }
283
+ try {
284
+ const response = await apiRequest(endpoint.method, apiPath, {
285
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
286
+ data,
287
+ contextName: flags.context,
288
+ acceptAny: true,
289
+ });
290
+ // Check content-type for non-JSON responses (XML, text, etc.)
291
+ const contentType = (response.headers['content-type'] || '').toLowerCase();
292
+ if (contentType.includes('xml') || contentType.includes('text/')) {
293
+ // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
294
+ this.log(String(response.data));
295
+ return;
296
+ }
297
+ // Parse JSON if response came as string (due to acceptAny)
298
+ let output = response.data;
299
+ if (typeof output === 'string') {
300
+ try {
301
+ output = JSON.parse(output);
302
+ }
303
+ catch { /* keep as string */ }
304
+ }
305
+ // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
306
+ if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
307
+ output = output.data;
308
+ }
309
+ // For table/csv on non-array responses, auto-extract the primary array
310
+ // e.g., { payments: [...], total: 3 } → extract payments array
311
+ if ((flags.table || flags.output === 'table' || flags.csv || flags.output === 'csv')
312
+ && output && typeof output === 'object' && !Array.isArray(output)) {
313
+ const obj = output;
314
+ const arrayKeys = Object.keys(obj).filter(k => Array.isArray(obj[k]));
315
+ if (arrayKeys.length === 1) {
316
+ output = obj[arrayKeys[0]];
317
+ }
318
+ }
319
+ // Filter fields if --fields flag is provided
320
+ if (flags.fields) {
321
+ const fieldList = flags.fields.split(',').map(f => f.trim());
322
+ output = this.filterFields(output, fieldList);
323
+ }
324
+ // Sanitize output if requested
325
+ if (flags.sanitize) {
326
+ output = this.sanitizeOutput(output);
327
+ }
328
+ const fieldList = flags.fields?.split(',').map(f => f.trim());
329
+ if (flags.csv || flags.output === 'csv')
330
+ this.printCsv(output, fieldList);
331
+ else if (flags.table || flags.output === 'table')
332
+ this.printTable(output, fieldList);
333
+ else
334
+ this.log(JSON.stringify(output, null, 2));
335
+ }
336
+ catch (error) {
337
+ if (error instanceof ApiError) {
338
+ if (error.status === 404)
339
+ this.error(error.message);
340
+ this.error(`API Error (${error.status}): ${error.message}`);
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+ async runImport(flags) {
346
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
347
+ if (!context)
348
+ this.error('No context configured. Run: aderis auth login --new');
349
+ if (!context.tokens)
350
+ this.error('Not logged in. Run: aderis auth login');
351
+ // Get data from file, --data, or --set flags
352
+ let items = [];
353
+ if (flags.file) {
354
+ const fsModule = await import('node:fs');
355
+ try {
356
+ const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
357
+ items = Array.isArray(content) ? content : [content];
358
+ }
359
+ catch {
360
+ this.error(`Failed to read file: ${flags.file}`);
361
+ }
362
+ }
363
+ else if (flags.data) {
364
+ try {
365
+ const content = JSON.parse(flags.data);
366
+ items = Array.isArray(content) ? content : [content];
367
+ }
368
+ catch {
369
+ this.error('Invalid JSON in --data');
370
+ }
371
+ }
372
+ else if (flags.set && flags.set.length > 0) {
373
+ const item = {};
374
+ for (const pair of flags.set) {
375
+ const eqIndex = pair.indexOf('=');
376
+ if (eqIndex === -1)
377
+ this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
378
+ const key = pair.slice(0, eqIndex);
379
+ let value = pair.slice(eqIndex + 1);
380
+ try {
381
+ value = JSON.parse(value);
382
+ }
383
+ catch { /* keep as string */ }
384
+ const keys = key.split('.');
385
+ if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
386
+ this.error(`Invalid --set key: ${key}`);
387
+ }
388
+ let obj = item;
389
+ for (let i = 0; i < keys.length - 1; i++) {
390
+ if (!(keys[i] in obj))
391
+ obj[keys[i]] = {};
392
+ obj = obj[keys[i]];
393
+ }
394
+ obj[keys[keys.length - 1]] = value;
395
+ }
396
+ items = [item];
397
+ }
398
+ else {
399
+ this.error('Import requires --file, --data, or --set flags');
400
+ }
401
+ const createEndpoint = ENDPOINTS['create'];
402
+ const updateEndpoint = ENDPOINTS['update'];
403
+ if (!createEndpoint || !updateEndpoint) {
404
+ this.error('Import requires both create and update endpoints');
405
+ }
406
+ // Helper to extract identifier from item (id, code, or xxxCode)
407
+ const getItemIdentifier = (item) => {
408
+ if (item.id)
409
+ return String(item.id);
410
+ if (item.code)
411
+ return String(item.code);
412
+ // Look for xxxCode fields (e.g., ledgerCode, accountCode)
413
+ for (const key of Object.keys(item)) {
414
+ if (key.endsWith('Code') && item[key])
415
+ return String(item[key]);
416
+ }
417
+ // Look for name as fallback
418
+ if (item.name)
419
+ return String(item.name).slice(0, 30);
420
+ return '';
421
+ };
422
+ const results = [];
423
+ for (const item of items) {
424
+ const identifier = getItemIdentifier(item);
425
+ try {
426
+ // Try to create
427
+ const response = await apiRequest('POST', createEndpoint.path, {
428
+ data: item,
429
+ contextName: flags.context,
430
+ });
431
+ const id = response.data?.id;
432
+ results.push({ status: 'created', id: id ? String(id) : undefined, identifier });
433
+ }
434
+ catch (error) {
435
+ if (error instanceof ApiError && error.status === 409) {
436
+ // Conflict - try to update existing
437
+ const errorData = error.data;
438
+ const existingId = errorData?.existingId;
439
+ if (existingId) {
440
+ try {
441
+ const updatePath = updateEndpoint.path.replace('{id}', existingId);
442
+ await apiRequest('PATCH', updatePath, {
443
+ data: item,
444
+ contextName: flags.context,
445
+ });
446
+ results.push({ status: 'updated', id: existingId, identifier });
447
+ }
448
+ catch (updateError) {
449
+ const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
450
+ results.push({ status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` });
451
+ }
452
+ }
453
+ else {
454
+ results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
455
+ }
456
+ }
457
+ else {
458
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
459
+ results.push({ status: 'error', identifier, error: msg });
460
+ }
461
+ }
462
+ }
463
+ // Summary counts
464
+ const created = results.filter(r => r.status === 'created').length;
465
+ const updated = results.filter(r => r.status === 'updated').length;
466
+ const errors = results.filter(r => r.status === 'error');
467
+ // Output results based on format (default: table)
468
+ const outputFormat = flags.output || 'table';
469
+ if (outputFormat === 'json') {
470
+ this.log(JSON.stringify(results, null, 2));
471
+ }
472
+ else if (outputFormat === 'csv' || flags.csv) {
473
+ // CSV output
474
+ this.log('status,identifier,id,error');
475
+ for (const r of results) {
476
+ const err = (r.error || '').replace(/"/g, '""');
477
+ this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
478
+ }
479
+ }
480
+ else {
481
+ // Table output (default)
482
+ if (results.length > 0) {
483
+ const statusWidth = 8;
484
+ const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
485
+ this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
486
+ this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
487
+ for (const r of results) {
488
+ const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
489
+ this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
490
+ }
491
+ }
492
+ }
493
+ // Always show summary at the end
494
+ this.log('');
495
+ this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
496
+ }
497
+ async runExport(flags) {
498
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
499
+ if (!context)
500
+ this.error('No context configured. Run: aderis auth login --new');
501
+ if (!context.tokens)
502
+ this.error('Not logged in. Run: aderis auth login');
503
+ const listEndpoint = ENDPOINTS['list'];
504
+ if (!listEndpoint)
505
+ this.error('Export requires a list endpoint');
506
+ const allItems = [];
507
+ let page = 1;
508
+ const limit = 100;
509
+ let hasMore = true;
510
+ // Build base query params
511
+ const baseParams = {};
512
+ if (flags.search)
513
+ baseParams.search = flags.search;
514
+ if (flags['sort-by'])
515
+ baseParams.sortBy = flags['sort-by'];
516
+ if (flags.include)
517
+ baseParams.include = flags.include;
518
+ if (flags.query) {
519
+ try {
520
+ Object.assign(baseParams, JSON.parse(flags.query));
521
+ }
522
+ catch {
523
+ this.error('Invalid JSON in --query');
524
+ }
525
+ }
526
+ // Parse --filter flags (auto-prepend $eq: if no operator specified)
527
+ if (flags.filter && flags.filter.length > 0) {
528
+ for (const f of flags.filter) {
529
+ const eqIndex = f.indexOf('=');
530
+ if (eqIndex === -1)
531
+ this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
532
+ const key = f.slice(0, eqIndex);
533
+ let value = f.slice(eqIndex + 1);
534
+ const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
535
+ if (opMatch) {
536
+ value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
537
+ }
538
+ else {
539
+ value = `\$eq:${value}`;
540
+ }
541
+ const filterKey = `filter.${key}`;
542
+ if (filterKey in baseParams) {
543
+ const existing = baseParams[filterKey];
544
+ baseParams[filterKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
545
+ }
546
+ else {
547
+ baseParams[filterKey] = value;
548
+ }
549
+ }
550
+ }
551
+ // Fetch all pages
552
+ process.stderr.write('Exporting...');
553
+ while (hasMore) {
554
+ const params = { ...baseParams, page, limit };
555
+ const response = await apiRequest('GET', listEndpoint.path, {
556
+ params,
557
+ contextName: flags.context,
558
+ });
559
+ let items = [];
560
+ const data = response.data;
561
+ // Handle paginated response format
562
+ if (data && typeof data === 'object' && 'data' in data && Array.isArray(data.data)) {
563
+ items = data.data;
564
+ }
565
+ else if (Array.isArray(response.data)) {
566
+ items = response.data;
567
+ }
568
+ allItems.push(...items);
569
+ process.stderr.write(` ${allItems.length}`);
570
+ // Check if there are more pages
571
+ if (items.length < limit) {
572
+ hasMore = false;
573
+ }
574
+ else {
575
+ page++;
576
+ }
577
+ }
578
+ process.stderr.write(' done\n');
579
+ // Filter fields if specified
580
+ let output = allItems;
581
+ if (flags.fields) {
582
+ const fieldList = flags.fields.split(',').map(f => f.trim());
583
+ output = this.filterFields(allItems, fieldList);
584
+ }
585
+ // Sanitize output if requested
586
+ if (flags.sanitize) {
587
+ output = this.sanitizeOutput(output);
588
+ }
589
+ // Output in requested format
590
+ const fieldList = flags.fields ? flags.fields.split(',').map(f => f.trim()) : undefined;
591
+ if (flags.csv || flags.output === 'csv') {
592
+ this.printCsv(output, fieldList);
593
+ }
594
+ else {
595
+ this.log(JSON.stringify(output, null, 2));
596
+ }
597
+ }
598
+ async runCamt053Import(flags) {
599
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
600
+ if (!context)
601
+ this.error('No context configured. Run: aderis auth login --new');
602
+ if (!context.tokens)
603
+ this.error('Not logged in. Run: aderis auth login');
604
+ if (!flags.file) {
605
+ this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
606
+ }
607
+ const fsModule = await import('node:fs');
608
+ let xmlContent;
609
+ try {
610
+ xmlContent = fsModule.readFileSync(flags.file, 'utf-8');
611
+ }
612
+ catch {
613
+ this.error(`Failed to read file: ${flags.file}`);
614
+ }
615
+ const endpoint = ENDPOINTS['import-camt053'];
616
+ try {
617
+ const response = await apiRequest(endpoint.method, endpoint.path, {
618
+ data: xmlContent,
619
+ contextName: flags.context,
620
+ contentType: 'application/xml',
621
+ });
622
+ // Extract transactions array and counts from response
623
+ let transactions = [];
624
+ let imported = 0;
625
+ let skipped = 0;
626
+ if (response.data && typeof response.data === 'object') {
627
+ const data = response.data;
628
+ if ('data' in data && Array.isArray(data.data)) {
629
+ transactions = data.data;
630
+ }
631
+ else if ('transactions' in data && Array.isArray(data.transactions)) {
632
+ transactions = data.transactions;
633
+ }
634
+ else if (Array.isArray(response.data)) {
635
+ transactions = response.data;
636
+ }
637
+ if (typeof data.imported === 'number')
638
+ imported = data.imported;
639
+ if (typeof data.skipped === 'number')
640
+ skipped = data.skipped;
641
+ }
642
+ else if (Array.isArray(response.data)) {
643
+ transactions = response.data;
644
+ }
645
+ if (imported === 0 && skipped === 0 && transactions.length > 0) {
646
+ for (const tx of transactions) {
647
+ const t = tx;
648
+ if (t.status === 'skipped' || t.skipped === true) {
649
+ skipped++;
650
+ }
651
+ else {
652
+ imported++;
653
+ }
654
+ }
655
+ if (imported === 0 && skipped === 0) {
656
+ imported = transactions.length;
657
+ }
658
+ }
659
+ let output = transactions;
660
+ if (flags.sanitize) {
661
+ output = this.sanitizeOutput(transactions);
662
+ }
663
+ const fieldList = flags.fields ? flags.fields.split(',').map(f => f.trim()) : undefined;
664
+ if (flags.csv || flags.output === 'csv') {
665
+ this.printCsv(output, fieldList);
666
+ }
667
+ else if (flags.table || flags.output === 'table') {
668
+ this.printTable(output, fieldList);
669
+ }
670
+ else {
671
+ this.log(JSON.stringify(output, null, 2));
672
+ }
673
+ this.log('');
674
+ this.log(`Summary: ${imported} imported, ${skipped} skipped`);
675
+ }
676
+ catch (error) {
677
+ if (error instanceof ApiError) {
678
+ if (error.status === 404)
679
+ this.error(error.message);
680
+ this.error(`API Error (${error.status}): ${error.message}`);
681
+ }
682
+ throw error;
683
+ }
684
+ }
685
+ async runNestedImport(suffix, parentId, flags) {
686
+ if (!parentId) {
687
+ this.error(`Missing required argument: id. Usage: aderis sales-invoices import-${suffix} <parent-id> --file data.json`);
688
+ }
689
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
690
+ if (!context)
691
+ this.error('No context configured. Run: aderis auth login --new');
692
+ if (!context.tokens)
693
+ this.error('Not logged in. Run: aderis auth login');
694
+ // Get data from file, --data, or --set flags
695
+ let items = [];
696
+ if (flags.file) {
697
+ const fsModule = await import('node:fs');
698
+ try {
699
+ const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
700
+ items = Array.isArray(content) ? content : [content];
701
+ }
702
+ catch {
703
+ this.error(`Failed to read file: ${flags.file}`);
704
+ }
705
+ }
706
+ else if (flags.data) {
707
+ try {
708
+ const content = JSON.parse(flags.data);
709
+ items = Array.isArray(content) ? content : [content];
710
+ }
711
+ catch {
712
+ this.error('Invalid JSON in --data');
713
+ }
714
+ }
715
+ else if (flags.set && flags.set.length > 0) {
716
+ const item = {};
717
+ for (const pair of flags.set) {
718
+ const eqIndex = pair.indexOf('=');
719
+ if (eqIndex === -1)
720
+ this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
721
+ const key = pair.slice(0, eqIndex);
722
+ let value = pair.slice(eqIndex + 1);
723
+ try {
724
+ value = JSON.parse(value);
725
+ }
726
+ catch { /* keep as string */ }
727
+ const keys = key.split('.');
728
+ if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
729
+ this.error(`Invalid --set key: ${key}`);
730
+ }
731
+ let obj = item;
732
+ for (let i = 0; i < keys.length - 1; i++) {
733
+ if (!(keys[i] in obj))
734
+ obj[keys[i]] = {};
735
+ obj = obj[keys[i]];
736
+ }
737
+ obj[keys[keys.length - 1]] = value;
738
+ }
739
+ items = [item];
740
+ }
741
+ else {
742
+ this.error(`Import requires --file, --data, or --set flags`);
743
+ }
744
+ const createEndpoint = ENDPOINTS[`create-${suffix}`];
745
+ const updateEndpoint = ENDPOINTS[`update-${suffix}`];
746
+ if (!createEndpoint || !updateEndpoint) {
747
+ this.error(`Import requires both create-${suffix} and update-${suffix} endpoints`);
748
+ }
749
+ // Build the base path with parent ID (first path param)
750
+ const firstParam = createEndpoint.pathParams[0];
751
+ const basePath = createEndpoint.path.replace(`{${firstParam}}`, parentId);
752
+ // Helper to extract identifier from item
753
+ const getItemIdentifier = (item) => {
754
+ if (item.id)
755
+ return String(item.id);
756
+ if (item.itemId)
757
+ return String(item.itemId);
758
+ if (item.code)
759
+ return String(item.code);
760
+ for (const key of Object.keys(item)) {
761
+ if (key.endsWith('Id') && item[key])
762
+ return String(item[key]);
763
+ if (key.endsWith('Code') && item[key])
764
+ return String(item[key]);
765
+ }
766
+ return '';
767
+ };
768
+ const results = [];
769
+ for (const item of items) {
770
+ const identifier = getItemIdentifier(item);
771
+ try {
772
+ // Try to create
773
+ const response = await apiRequest('POST', basePath, {
774
+ data: item,
775
+ contextName: flags.context,
776
+ });
777
+ const id = response.data?.id;
778
+ results.push({ status: 'created', id: id ? String(id) : undefined, identifier });
779
+ }
780
+ catch (error) {
781
+ if (error instanceof ApiError && error.status === 409) {
782
+ // Conflict - try to update existing
783
+ const errorData = error.data;
784
+ const existingId = errorData?.existingId || item.id;
785
+ if (existingId) {
786
+ try {
787
+ // Find the nested ID param name (e.g., 'priceId' for prices)
788
+ const firstUpdateParam = updateEndpoint.pathParams[0];
789
+ const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
790
+ let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
791
+ if (nestedIdParam) {
792
+ updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
793
+ }
794
+ await apiRequest('PATCH', updatePath, {
795
+ data: item,
796
+ contextName: flags.context,
797
+ });
798
+ results.push({ status: 'updated', id: existingId, identifier });
799
+ }
800
+ catch (updateError) {
801
+ const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
802
+ results.push({ status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` });
803
+ }
804
+ }
805
+ else {
806
+ results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
807
+ }
808
+ }
809
+ else {
810
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
811
+ results.push({ status: 'error', identifier, error: msg });
812
+ }
813
+ }
814
+ }
815
+ // Summary counts
816
+ const created = results.filter(r => r.status === 'created').length;
817
+ const updated = results.filter(r => r.status === 'updated').length;
818
+ const errors = results.filter(r => r.status === 'error');
819
+ // Output results based on format (default: table)
820
+ const outputFormat = flags.output || 'table';
821
+ if (outputFormat === 'json') {
822
+ this.log(JSON.stringify(results, null, 2));
823
+ }
824
+ else if (outputFormat === 'csv' || flags.csv) {
825
+ this.log('status,identifier,id,error');
826
+ for (const r of results) {
827
+ const err = (r.error || '').replace(/"/g, '""');
828
+ this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
829
+ }
830
+ }
831
+ else {
832
+ if (results.length > 0) {
833
+ const statusWidth = 8;
834
+ const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
835
+ this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
836
+ this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
837
+ for (const r of results) {
838
+ const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
839
+ this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
840
+ }
841
+ }
842
+ }
843
+ this.log('');
844
+ this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
845
+ }
846
+ filterFields(data, fields) {
847
+ const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
848
+ const pick = (obj, keys) => {
849
+ const result = {};
850
+ for (const key of keys) {
851
+ if (key.split('.').some(isUnsafeKey))
852
+ continue;
853
+ if (key.includes('.')) {
854
+ // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
855
+ const [first, ...rest] = key.split('.');
856
+ const val = obj[first];
857
+ if (val && typeof val === 'object') {
858
+ const restKey = rest.join('.');
859
+ if (Array.isArray(val)) {
860
+ // Map over array elements (e.g., allocations[].salesInvoiceId)
861
+ const existing = Array.isArray(result[first]) ? result[first] : [];
862
+ result[first] = val.map((item, i) => ({
863
+ ...(existing[i] || {}),
864
+ ...pick(item, [restKey]),
865
+ }));
866
+ }
867
+ else {
868
+ const nested = pick(val, [restKey]);
869
+ result[first] = { ...(result[first] || {}), ...nested };
870
+ }
871
+ }
872
+ }
873
+ else if (key in obj) {
874
+ result[key] = obj[key];
875
+ }
876
+ }
877
+ return result;
878
+ };
879
+ if (Array.isArray(data)) {
880
+ return data.map(item => pick(item, fields));
881
+ }
882
+ return pick(data, fields);
883
+ }
884
+ sanitizeOutput(data) {
885
+ const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
886
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
887
+ const sanitize = (obj, parentUuids = new Set()) => {
888
+ if (obj === null || obj === undefined)
889
+ return undefined;
890
+ if (Array.isArray(obj)) {
891
+ const result = obj.map(item => sanitize(item, new Set(parentUuids))).filter(item => item !== undefined);
892
+ return result.length > 0 ? result : undefined;
893
+ }
894
+ if (typeof obj === 'object') {
895
+ const input = obj;
896
+ // Use a Map to avoid any prototype-pollution risk on the result object
897
+ const collected = new Map();
898
+ // Collect UUIDs from this level to pass to children
899
+ const currentUuids = new Set(parentUuids);
900
+ for (const [, value] of Object.entries(input)) {
901
+ if (typeof value === 'string' && uuidRegex.test(value)) {
902
+ currentUuids.add(value);
903
+ }
904
+ }
905
+ for (const [key, value] of Object.entries(input)) {
906
+ // Skip prototype-pollution keys
907
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
908
+ continue;
909
+ // Skip audit fields
910
+ if (auditFields.includes(key))
911
+ continue;
912
+ // Skip null/undefined values
913
+ if (value === null || value === undefined)
914
+ continue;
915
+ // Skip UUID values that exist in parent
916
+ if (typeof value === 'string' && uuidRegex.test(value) && parentUuids.has(value))
917
+ continue;
918
+ // Recursively sanitize nested objects/arrays
919
+ const sanitized = sanitize(value, currentUuids);
920
+ if (sanitized !== undefined) {
921
+ collected.set(key, sanitized);
922
+ }
923
+ }
924
+ if (collected.size === 0)
925
+ return undefined;
926
+ return Object.fromEntries(collected);
927
+ }
928
+ return obj;
929
+ };
930
+ return sanitize(data) ?? (Array.isArray(data) ? [] : {});
931
+ }
932
+ getColumns(data, fields, flatten = false) {
933
+ const items = Array.isArray(data) ? data : [data];
934
+ if (items.length === 0)
935
+ return { cols: [], items: [] };
936
+ const first = items[0];
937
+ const hiddenFields = ['tenantId', '_type'];
938
+ if (fields) {
939
+ return { cols: fields, items: items };
940
+ }
941
+ // Recursively collect all column paths, flattening objects (but not arrays)
942
+ const collectPaths = (obj, prefix = '') => {
943
+ const paths = [];
944
+ for (const key of Object.keys(obj)) {
945
+ const fullKey = prefix ? `${prefix}.${key}` : key;
946
+ if (hiddenFields.includes(key))
947
+ continue;
948
+ const val = obj[key];
949
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
950
+ // Flatten nested objects (but not arrays)
951
+ if (flatten) {
952
+ paths.push(...collectPaths(val, fullKey));
953
+ }
954
+ // Skip objects when not flattening
955
+ }
956
+ else if (!Array.isArray(val)) {
957
+ // Include primitives (and null)
958
+ paths.push(fullKey);
959
+ }
960
+ // Skip arrays entirely
961
+ }
962
+ return paths;
963
+ };
964
+ // Scan multiple items to discover all columns (handles fields absent/null in first item)
965
+ const cols = collectPaths(first);
966
+ const colSet = new Set(cols);
967
+ const scanCount = Math.min(items.length, 50);
968
+ for (let i = 1; i < scanCount; i++) {
969
+ for (const p of collectPaths(items[i])) {
970
+ if (!colSet.has(p)) {
971
+ cols.push(p);
972
+ colSet.add(p);
973
+ }
974
+ }
975
+ }
976
+ return { cols: flatten ? cols : cols.slice(0, 8), items: items };
977
+ }
978
+ getValue(row, col) {
979
+ let val = row[col];
980
+ if (col.includes('.')) {
981
+ const parts = col.split('.');
982
+ val = parts.reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), row);
983
+ }
984
+ return String(val ?? '');
985
+ }
986
+ printTable(data, fields) {
987
+ const { cols, items } = this.getColumns(data, fields);
988
+ if (items.length === 0) {
989
+ this.log('No results');
990
+ return;
991
+ }
992
+ // Calculate column widths
993
+ const widths = cols.map(c => c.length);
994
+ for (const item of items) {
995
+ cols.forEach((col, i) => {
996
+ const val = this.getValue(item, col).slice(0, 40);
997
+ widths[i] = Math.max(widths[i], val.length);
998
+ });
999
+ }
1000
+ // Print header
1001
+ const header = cols.map((c, i) => c.padEnd(widths[i])).join(' ');
1002
+ this.log(header);
1003
+ this.log(widths.map(w => '-'.repeat(w)).join(' '));
1004
+ // Print rows
1005
+ for (const item of items) {
1006
+ const row = cols.map((col, i) => {
1007
+ const val = this.getValue(item, col).slice(0, 40);
1008
+ return val.padEnd(widths[i]);
1009
+ }).join(' ');
1010
+ this.log(row);
1011
+ }
1012
+ }
1013
+ printCsv(data, fields) {
1014
+ const { cols, items } = this.getColumns(data, fields, true); // flatten=true for CSV
1015
+ if (items.length === 0) {
1016
+ this.log('No results');
1017
+ return;
1018
+ }
1019
+ const escape = (val) => {
1020
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
1021
+ return '"' + val.replace(/"/g, '""') + '"';
1022
+ }
1023
+ return val;
1024
+ };
1025
+ // Print header
1026
+ this.log(cols.map(escape).join(','));
1027
+ // Print rows
1028
+ for (const item of items) {
1029
+ const row = cols.map(col => escape(this.getValue(item, col)));
1030
+ this.log(row.join(','));
1031
+ }
1032
+ }
1033
+ }