@aderis-erp/cli 1.1.4 → 1.1.5

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 (85) hide show
  1. package/dist/commands/access-groups.js +42 -8
  2. package/dist/commands/account-transaction-allocations.js +38 -4
  3. package/dist/commands/account-transactions.d.ts +2 -0
  4. package/dist/commands/account-transactions.js +68 -32
  5. package/dist/commands/accounts.js +46 -12
  6. package/dist/commands/agent-platform.js +37 -3
  7. package/dist/commands/agent-runs.js +49 -15
  8. package/dist/commands/analytics.js +39 -5
  9. package/dist/commands/api-clients.js +47 -13
  10. package/dist/commands/api-keys.js +40 -6
  11. package/dist/commands/async-jobs.js +45 -11
  12. package/dist/commands/audit-log.js +38 -4
  13. package/dist/commands/bank-accounts.js +54 -20
  14. package/dist/commands/bank-integrations.js +56 -22
  15. package/dist/commands/business-partners.js +77 -43
  16. package/dist/commands/chat.js +43 -9
  17. package/dist/commands/credit-card-accounts.js +57 -23
  18. package/dist/commands/currencies.js +37 -3
  19. package/dist/commands/custody-accounts.js +41 -7
  20. package/dist/commands/dashboards.js +52 -18
  21. package/dist/commands/delivery-notes.js +69 -35
  22. package/dist/commands/document-checksums.d.ts +1 -0
  23. package/dist/commands/document-checksums.js +38 -3
  24. package/dist/commands/dunning-configs.js +41 -7
  25. package/dist/commands/dunning-reminders.js +44 -10
  26. package/dist/commands/dunning-runs.js +37 -3
  27. package/dist/commands/email-transactions.js +39 -5
  28. package/dist/commands/employees.js +77 -43
  29. package/dist/commands/entity-configurations.js +39 -5
  30. package/dist/commands/event-mapping-rules.js +37 -3
  31. package/dist/commands/event-processor-mappings.js +41 -7
  32. package/dist/commands/files.js +48 -14
  33. package/dist/commands/financial-instrument-transactions.d.ts +56 -0
  34. package/dist/commands/financial-instrument-transactions.js +1369 -0
  35. package/dist/commands/financial-instrument-types.js +41 -7
  36. package/dist/commands/financial-instruments.js +41 -7
  37. package/dist/commands/health.js +41 -7
  38. package/dist/commands/imports.js +39 -5
  39. package/dist/commands/inbox.js +42 -8
  40. package/dist/commands/integrations.js +39 -5
  41. package/dist/commands/item-categories.js +41 -7
  42. package/dist/commands/items.js +42 -8
  43. package/dist/commands/journal-lines.js +37 -3
  44. package/dist/commands/journals.js +61 -27
  45. package/dist/commands/kpis.js +44 -10
  46. package/dist/commands/ledgers.js +83 -49
  47. package/dist/commands/my-account.js +54 -20
  48. package/dist/commands/notifications.js +37 -3
  49. package/dist/commands/office-locations.js +41 -7
  50. package/dist/commands/payment-methods.js +46 -12
  51. package/dist/commands/payment-runs.js +54 -20
  52. package/dist/commands/payment-terms.js +42 -8
  53. package/dist/commands/payments.js +43 -9
  54. package/dist/commands/payroll-runs.js +71 -37
  55. package/dist/commands/permissions.js +37 -3
  56. package/dist/commands/price-lists.js +48 -14
  57. package/dist/commands/processor-mappings.js +40 -6
  58. package/dist/commands/processors.js +46 -12
  59. package/dist/commands/purchase-order-types.js +41 -7
  60. package/dist/commands/purchase-orders.js +60 -26
  61. package/dist/commands/reports.js +43 -8
  62. package/dist/commands/roles.js +40 -6
  63. package/dist/commands/salary-types.js +41 -7
  64. package/dist/commands/sales-invoices.js +85 -51
  65. package/dist/commands/sales-orders.js +69 -35
  66. package/dist/commands/saved-views.js +39 -5
  67. package/dist/commands/scheduled-processor-mappings.js +41 -7
  68. package/dist/commands/schedules.js +45 -11
  69. package/dist/commands/settings.js +38 -4
  70. package/dist/commands/tags.js +42 -8
  71. package/dist/commands/tax-codes.js +44 -10
  72. package/dist/commands/tax-matrix.js +41 -7
  73. package/dist/commands/templates.js +49 -15
  74. package/dist/commands/tenants.js +44 -10
  75. package/dist/commands/tools.js +38 -4
  76. package/dist/commands/users.js +58 -24
  77. package/dist/commands/vat-declarations.js +61 -27
  78. package/dist/commands/vendor-invoices.js +81 -47
  79. package/dist/commands/webhooks.js +43 -9
  80. package/dist/commands/work-summary.js +37 -3
  81. package/dist/lib/paths.d.ts +8 -0
  82. package/dist/lib/paths.js +16 -0
  83. package/dist/lib/production-guard.js +2 -0
  84. package/oclif.manifest.json +199 -2
  85. package/package.json +9 -3
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,13 +12,13 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "approve", "cancel"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/payments', summary: 'List payments', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/payments', summary: 'Create a standalone payment', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/payments/{paymentId}', summary: 'Get a payment by ID', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/payments/{paymentId}', summary: 'Update a payment', pathParams: ['paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/payments/{paymentId}', summary: 'Delete a payment', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'approve': { method: 'POST', path: '/v1/payments/{paymentId}/approve', summary: 'Approve a payment (OPEN → APPROVED)', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'cancel': { method: 'POST', path: '/v1/payments/{paymentId}/cancel', summary: 'Cancel a sent payment (SENT → ERROR)', pathParams: ['paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/payments', summary: 'List payments', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/payments', summary: 'Create a standalone payment', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/payments/{paymentId}', summary: 'Get a payment by ID', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/payments/{paymentId}', summary: 'Update a payment', pathParams: ['paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/payments/{paymentId}', summary: 'Delete a payment', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'approve': { method: 'POST', path: '/v1/payments/{paymentId}/approve', summary: 'Approve a payment (OPEN → APPROVED)', pathParams: ['paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'cancel': { method: 'POST', path: '/v1/payments/{paymentId}/cancel', summary: 'Cancel a sent payment (SENT → ERROR)', pathParams: ['paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
22
  };
22
23
  export default class Payments extends Command {
23
24
  static description = 'Manage payments';
@@ -39,7 +40,7 @@ export default class Payments extends Command {
39
40
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
40
41
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
41
42
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
42
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
43
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
43
44
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
44
45
  page: Flags.string({ description: 'Page number' }),
45
46
  limit: Flags.string({ description: 'Items per page' }),
@@ -170,6 +171,17 @@ export default class Payments extends Command {
170
171
  }
171
172
  }
172
173
  }
174
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
175
+ // have them: their body is not JSON, so --set cannot carry a value there.
176
+ for (const param of endpoint.specParams) {
177
+ const value = flags[param.flag];
178
+ if (value === undefined || value === '') {
179
+ if (param.required)
180
+ this.error(`Missing required flag: --${param.flag}`);
181
+ continue;
182
+ }
183
+ queryParams[param.name] = value;
184
+ }
173
185
  let data;
174
186
  if (endpoint.isMultipart) {
175
187
  if (!flags.file)
@@ -190,6 +202,25 @@ export default class Payments extends Command {
190
202
  }
191
203
  data = form;
192
204
  }
205
+ else if (endpoint.bodyMediaType) {
206
+ // The body is not JSON (today only application/x-ndjson): it is sent
207
+ // verbatim, under the content type the endpoint asks for.
208
+ if (flags.file) {
209
+ const fsModule = await import('node:fs');
210
+ try {
211
+ data = fsModule.readFileSync(flags.file, 'utf-8');
212
+ }
213
+ catch {
214
+ this.error(`Failed to read file: ${flags.file}`);
215
+ }
216
+ }
217
+ else if (flags.data) {
218
+ data = flags.data;
219
+ }
220
+ else {
221
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
222
+ }
223
+ }
193
224
  else if (endpoint.hasBody) {
194
225
  // For binary-response endpoints, --file is the output path, not a JSON
195
226
  // input file — skip the read-as-body branch.
@@ -285,7 +316,7 @@ export default class Payments extends Command {
285
316
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
286
317
  }
287
318
  else if (data) {
288
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
319
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
289
320
  }
290
321
  process.stderr.write('\n');
291
322
  }
@@ -299,6 +330,7 @@ export default class Payments extends Command {
299
330
  const binResponse = await apiRequest(endpoint.method, apiPath, {
300
331
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
301
332
  data,
333
+ contentType: endpoint.bodyMediaType || undefined,
302
334
  contextName: flags.context,
303
335
  binary: true,
304
336
  });
@@ -324,6 +356,7 @@ export default class Payments extends Command {
324
356
  const raw = await apiRequest(endpoint.method, apiPath, {
325
357
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
326
358
  data,
359
+ contentType: endpoint.bodyMediaType || undefined,
327
360
  contextName: flags.context,
328
361
  binary: true,
329
362
  });
@@ -344,6 +377,7 @@ export default class Payments extends Command {
344
377
  response = await apiRequest(endpoint.method, apiPath, {
345
378
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
346
379
  data,
380
+ contentType: endpoint.bodyMediaType || undefined,
347
381
  contextName: flags.context,
348
382
  acceptAny: true,
349
383
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,41 +12,41 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["notes", "files", "files-parentId"];
12
13
  const SUBCOMMANDS = [...["list-notes", "create-notes", "update-notes", "delete-notes", "notices-dismiss", "notices-restore", "list", "create", "get", "update", "delete", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-bookings", "update-bookings", "list-entries", "get-entries", "update-entries", "entries-exclude", "entries-include", "list-files-parentId", "create-files-parentId", "get-files-parentId", "update-files-parentId", "delete-files-parentId", "files-download-parentId", "approve", "cancel", "payments", "recalculate-payment-status", "payslips-send"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list-notes': { method: 'GET', path: '/v1/payroll-runs/notes', summary: 'List payroll notes (paginated), newest effectiveDate first', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create-notes': { method: 'POST', path: '/v1/payroll-runs/notes', summary: 'Create a payroll note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'update-notes': { method: 'PATCH', path: '/v1/payroll-runs/notes/{noteId}', summary: 'Update a payroll note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'delete-notes': { method: 'DELETE', path: '/v1/payroll-runs/notes/{noteId}', summary: 'Delete a payroll note', pathParams: ['noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'notices-dismiss': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/notices/{noticeId}/dismiss', summary: 'Dismiss one review notice on a DRAFT run', pathParams: ['payrollRunId', 'noticeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'notices-restore': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/notices/{noticeId}/restore', summary: 'Restore a dismissed review notice on a DRAFT run', pathParams: ['payrollRunId', 'noticeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'list': { method: 'GET', path: '/v1/payroll-runs', summary: 'List payroll runs (paginated), each with its computed totals', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'create': { method: 'POST', path: '/v1/payroll-runs', summary: 'Create a payroll run with its entries and DRAFT payments', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['entries'] },
22
- 'get': { method: 'GET', path: '/v1/payroll-runs/{id}', summary: 'Get a payroll run with its totals, previous-run comparison and payments', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'update': { method: 'PATCH', path: '/v1/payroll-runs/{id}', summary: 'Change the payout date, description or bank account of a DRAFT payroll run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'delete': { method: 'DELETE', path: '/v1/payroll-runs/{id}', summary: 'Delete a DRAFT payroll run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'list-files': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files', summary: 'List file attachments for a payroll run', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'create-files': { method: 'POST', path: '/v1/payroll-runs/{parentId}/files', summary: 'Upload a file attachment to a payroll run', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'get-files': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Get a file attachment for a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'update-files': { method: 'PATCH', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a payroll run', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'delete-files': { method: 'DELETE', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'files-download': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'list-bookings': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/bookings', summary: 'List a payroll run\'s bookings, one per open ledger, and the clearing check', pathParams: ['payrollRunId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
- 'update-bookings': { method: 'PATCH', path: '/v1/payroll-runs/{payrollRunId}/bookings/{bookingId}', summary: 'Replace the lines of one of a DRAFT run\'s bookings by hand', pathParams: ['payrollRunId', 'bookingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
33
- 'list-entries': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/entries', summary: 'List a payroll run\'s entries, each with its previous-run comparison', pathParams: ['payrollRunId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
- 'get-entries': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}', summary: 'Get one payroll run entry with its previous-run comparison', pathParams: ['payrollRunId', 'entryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
- 'update-entries': { method: 'PATCH', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}', summary: 'Change a DRAFT entry\'s notes or payoutDate', pathParams: ['payrollRunId', 'entryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
- 'entries-exclude': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}/exclude', summary: 'Exclude a DRAFT entry from the payout', pathParams: ['payrollRunId', 'entryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
- 'entries-include': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}/include', summary: 'Include a previously excluded DRAFT entry back into the payout', pathParams: ['payrollRunId', 'entryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
- 'list-files-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files', summary: 'List file attachments for a payroll run entry', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
39
- 'create-files-parentId': { method: 'POST', path: '/v1/payroll-run-entries/{parentId}/files', summary: 'Upload a file attachment to a payroll run entry', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
40
- 'get-files-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Get a file attachment for a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
- 'update-files-parentId': { method: 'PATCH', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
- 'delete-files-parentId': { method: 'DELETE', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
43
- 'files-download-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
44
- 'approve': { method: 'POST', path: '/v1/payroll-runs/{id}/approve', summary: 'Approve a DRAFT payroll run and open its payment run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
45
- 'cancel': { method: 'POST', path: '/v1/payroll-runs/{id}/cancel', summary: 'Cancel an APPROVED payroll run whose payments have not been executed', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
46
- 'payments': { method: 'GET', path: '/v1/payroll-runs/{id}/payments', summary: 'List every payment of a payroll run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
47
- 'recalculate-payment-status': { method: 'POST', path: '/v1/payroll-runs/{id}/recalculate-payment-status', summary: 'Recalculate a payroll run\'s payment status from its payments', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
48
- 'payslips-send': { method: 'POST', path: '/v1/payroll-runs/{id}/payslips/send', summary: 'Email employees their payslips for an APPROVED payroll run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['entryIds'] },
15
+ 'list-notes': { method: 'GET', path: '/v1/payroll-runs/notes', summary: 'List payroll notes (paginated), newest effectiveDate first', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create-notes': { method: 'POST', path: '/v1/payroll-runs/notes', summary: 'Create a payroll note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'update-notes': { method: 'PATCH', path: '/v1/payroll-runs/notes/{noteId}', summary: 'Update a payroll note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'delete-notes': { method: 'DELETE', path: '/v1/payroll-runs/notes/{noteId}', summary: 'Delete a payroll note', pathParams: ['noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'notices-dismiss': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/notices/{noticeId}/dismiss', summary: 'Dismiss one review notice on a DRAFT run', pathParams: ['payrollRunId', 'noticeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'notices-restore': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/notices/{noticeId}/restore', summary: 'Restore a dismissed review notice on a DRAFT run', pathParams: ['payrollRunId', 'noticeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'list': { method: 'GET', path: '/v1/payroll-runs', summary: 'List payroll runs (paginated), each with its computed totals', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'create': { method: 'POST', path: '/v1/payroll-runs', summary: 'Create a payroll run with its entries and DRAFT payments', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['entries'], bodyMediaType: '', specParams: [] },
23
+ 'get': { method: 'GET', path: '/v1/payroll-runs/{id}', summary: 'Get a payroll run with its totals, previous-run comparison and payments', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'update': { method: 'PATCH', path: '/v1/payroll-runs/{id}', summary: 'Change the payout date, description or bank account of a DRAFT payroll run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'delete': { method: 'DELETE', path: '/v1/payroll-runs/{id}', summary: 'Delete a DRAFT payroll run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'list-files': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files', summary: 'List file attachments for a payroll run', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'create-files': { method: 'POST', path: '/v1/payroll-runs/{parentId}/files', summary: 'Upload a file attachment to a payroll run', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'get-files': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Get a file attachment for a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'update-files': { method: 'PATCH', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a payroll run', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'delete-files': { method: 'DELETE', path: '/v1/payroll-runs/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'files-download': { method: 'GET', path: '/v1/payroll-runs/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a payroll run', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'list-bookings': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/bookings', summary: 'List a payroll run\'s bookings, one per open ledger, and the clearing check', pathParams: ['payrollRunId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
33
+ 'update-bookings': { method: 'PATCH', path: '/v1/payroll-runs/{payrollRunId}/bookings/{bookingId}', summary: 'Replace the lines of one of a DRAFT run\'s bookings by hand', pathParams: ['payrollRunId', 'bookingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
34
+ 'list-entries': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/entries', summary: 'List a payroll run\'s entries, each with its previous-run comparison', pathParams: ['payrollRunId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
35
+ 'get-entries': { method: 'GET', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}', summary: 'Get one payroll run entry with its previous-run comparison', pathParams: ['payrollRunId', 'entryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
+ 'update-entries': { method: 'PATCH', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}', summary: 'Change a DRAFT entry\'s notes or payoutDate', pathParams: ['payrollRunId', 'entryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
37
+ 'entries-exclude': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}/exclude', summary: 'Exclude a DRAFT entry from the payout', pathParams: ['payrollRunId', 'entryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
38
+ 'entries-include': { method: 'POST', path: '/v1/payroll-runs/{payrollRunId}/entries/{entryId}/include', summary: 'Include a previously excluded DRAFT entry back into the payout', pathParams: ['payrollRunId', 'entryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
+ 'list-files-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files', summary: 'List file attachments for a payroll run entry', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
40
+ 'create-files-parentId': { method: 'POST', path: '/v1/payroll-run-entries/{parentId}/files', summary: 'Upload a file attachment to a payroll run entry', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
41
+ 'get-files-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Get a file attachment for a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
42
+ 'update-files-parentId': { method: 'PATCH', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
43
+ 'delete-files-parentId': { method: 'DELETE', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
44
+ 'files-download-parentId': { method: 'GET', path: '/v1/payroll-run-entries/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a payroll run entry', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
45
+ 'approve': { method: 'POST', path: '/v1/payroll-runs/{id}/approve', summary: 'Approve a DRAFT payroll run and open its payment run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
46
+ 'cancel': { method: 'POST', path: '/v1/payroll-runs/{id}/cancel', summary: 'Cancel an APPROVED payroll run whose payments have not been executed', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
47
+ 'payments': { method: 'GET', path: '/v1/payroll-runs/{id}/payments', summary: 'List every payment of a payroll run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
48
+ 'recalculate-payment-status': { method: 'POST', path: '/v1/payroll-runs/{id}/recalculate-payment-status', summary: 'Recalculate a payroll run\'s payment status from its payments', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
49
+ 'payslips-send': { method: 'POST', path: '/v1/payroll-runs/{id}/payslips/send', summary: 'Email employees their payslips for an APPROVED payroll run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['entryIds'], bodyMediaType: '', specParams: [] },
49
50
  };
50
51
  export default class PayrollRuns extends Command {
51
52
  static description = 'Manage payroll runs';
@@ -67,7 +68,7 @@ export default class PayrollRuns extends Command {
67
68
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
68
69
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
69
70
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
70
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
71
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
71
72
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
72
73
  'notice-id': Flags.string({ description: 'Path param: noticeId' }),
73
74
  'file-id': Flags.string({ description: 'Path param: fileId' }),
@@ -202,6 +203,17 @@ export default class PayrollRuns extends Command {
202
203
  }
203
204
  }
204
205
  }
206
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
207
+ // have them: their body is not JSON, so --set cannot carry a value there.
208
+ for (const param of endpoint.specParams) {
209
+ const value = flags[param.flag];
210
+ if (value === undefined || value === '') {
211
+ if (param.required)
212
+ this.error(`Missing required flag: --${param.flag}`);
213
+ continue;
214
+ }
215
+ queryParams[param.name] = value;
216
+ }
205
217
  let data;
206
218
  if (endpoint.isMultipart) {
207
219
  if (!flags.file)
@@ -222,6 +234,25 @@ export default class PayrollRuns extends Command {
222
234
  }
223
235
  data = form;
224
236
  }
237
+ else if (endpoint.bodyMediaType) {
238
+ // The body is not JSON (today only application/x-ndjson): it is sent
239
+ // verbatim, under the content type the endpoint asks for.
240
+ if (flags.file) {
241
+ const fsModule = await import('node:fs');
242
+ try {
243
+ data = fsModule.readFileSync(flags.file, 'utf-8');
244
+ }
245
+ catch {
246
+ this.error(`Failed to read file: ${flags.file}`);
247
+ }
248
+ }
249
+ else if (flags.data) {
250
+ data = flags.data;
251
+ }
252
+ else {
253
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
254
+ }
255
+ }
225
256
  else if (endpoint.hasBody) {
226
257
  // For binary-response endpoints, --file is the output path, not a JSON
227
258
  // input file — skip the read-as-body branch.
@@ -317,7 +348,7 @@ export default class PayrollRuns extends Command {
317
348
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
318
349
  }
319
350
  else if (data) {
320
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
351
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
321
352
  }
322
353
  process.stderr.write('\n');
323
354
  }
@@ -331,6 +362,7 @@ export default class PayrollRuns extends Command {
331
362
  const binResponse = await apiRequest(endpoint.method, apiPath, {
332
363
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
333
364
  data,
365
+ contentType: endpoint.bodyMediaType || undefined,
334
366
  contextName: flags.context,
335
367
  binary: true,
336
368
  });
@@ -356,6 +388,7 @@ export default class PayrollRuns extends Command {
356
388
  const raw = await apiRequest(endpoint.method, apiPath, {
357
389
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
358
390
  data,
391
+ contentType: endpoint.bodyMediaType || undefined,
359
392
  contextName: flags.context,
360
393
  binary: true,
361
394
  });
@@ -376,6 +409,7 @@ export default class PayrollRuns extends Command {
376
409
  response = await apiRequest(endpoint.method, apiPath, {
377
410
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
378
411
  data,
412
+ contentType: endpoint.bodyMediaType || undefined,
379
413
  contextName: flags.context,
380
414
  acceptAny: true,
381
415
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,7 +12,7 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/permissions', summary: 'List permissions selectable for tenant-created roles', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/permissions', summary: 'List permissions selectable for tenant-created roles', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
15
16
  };
16
17
  export default class Permissions extends Command {
17
18
  static description = 'Manage permissions';
@@ -29,7 +30,7 @@ export default class Permissions extends Command {
29
30
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
30
31
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
31
32
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
32
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
33
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
33
34
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
34
35
  page: Flags.string({ description: 'Page number' }),
35
36
  limit: Flags.string({ description: 'Items per page' }),
@@ -160,6 +161,17 @@ export default class Permissions extends Command {
160
161
  }
161
162
  }
162
163
  }
164
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
165
+ // have them: their body is not JSON, so --set cannot carry a value there.
166
+ for (const param of endpoint.specParams) {
167
+ const value = flags[param.flag];
168
+ if (value === undefined || value === '') {
169
+ if (param.required)
170
+ this.error(`Missing required flag: --${param.flag}`);
171
+ continue;
172
+ }
173
+ queryParams[param.name] = value;
174
+ }
163
175
  let data;
164
176
  if (endpoint.isMultipart) {
165
177
  if (!flags.file)
@@ -180,6 +192,25 @@ export default class Permissions extends Command {
180
192
  }
181
193
  data = form;
182
194
  }
195
+ else if (endpoint.bodyMediaType) {
196
+ // The body is not JSON (today only application/x-ndjson): it is sent
197
+ // verbatim, under the content type the endpoint asks for.
198
+ if (flags.file) {
199
+ const fsModule = await import('node:fs');
200
+ try {
201
+ data = fsModule.readFileSync(flags.file, 'utf-8');
202
+ }
203
+ catch {
204
+ this.error(`Failed to read file: ${flags.file}`);
205
+ }
206
+ }
207
+ else if (flags.data) {
208
+ data = flags.data;
209
+ }
210
+ else {
211
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
212
+ }
213
+ }
183
214
  else if (endpoint.hasBody) {
184
215
  // For binary-response endpoints, --file is the output path, not a JSON
185
216
  // input file — skip the read-as-body branch.
@@ -275,7 +306,7 @@ export default class Permissions extends Command {
275
306
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
276
307
  }
277
308
  else if (data) {
278
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
309
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
279
310
  }
280
311
  process.stderr.write('\n');
281
312
  }
@@ -289,6 +320,7 @@ export default class Permissions extends Command {
289
320
  const binResponse = await apiRequest(endpoint.method, apiPath, {
290
321
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
291
322
  data,
323
+ contentType: endpoint.bodyMediaType || undefined,
292
324
  contextName: flags.context,
293
325
  binary: true,
294
326
  });
@@ -314,6 +346,7 @@ export default class Permissions extends Command {
314
346
  const raw = await apiRequest(endpoint.method, apiPath, {
315
347
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
316
348
  data,
349
+ contentType: endpoint.bodyMediaType || undefined,
317
350
  contextName: flags.context,
318
351
  binary: true,
319
352
  });
@@ -334,6 +367,7 @@ export default class Permissions extends Command {
334
367
  response = await apiRequest(endpoint.method, apiPath, {
335
368
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
336
369
  data,
370
+ contentType: endpoint.bodyMediaType || undefined,
337
371
  contextName: flags.context,
338
372
  acceptAny: true,
339
373
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,18 +12,18 @@ const SERVER_IMPORTS = ["import", "import-prices"];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-prices", "create-prices", "get-prices", "update-prices", "delete-prices", "import", "import-prices"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'import': { method: 'POST', path: '/v1/price-lists/import', summary: 'Import price lists', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'import-prices': { method: 'POST', path: '/v1/price-lists/{priceListId}/prices/import', summary: 'Import prices into a price list', pathParams: ['priceListId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'import': { method: 'POST', path: '/v1/price-lists/import', summary: 'Import price lists', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'import-prices': { method: 'POST', path: '/v1/price-lists/{priceListId}/prices/import', summary: 'Import prices into a price list', pathParams: ['priceListId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
27
  };
27
28
  export default class PriceLists extends Command {
28
29
  static description = 'Manage price lists';
@@ -44,7 +45,7 @@ export default class PriceLists extends Command {
44
45
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
45
46
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
46
47
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
47
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
48
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
48
49
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
49
50
  mode: Flags.string({ description: 'Import mode: upsert (default) creates, updates or skips each record; create refuses records that already exist; update refuses records that do not', options: ['upsert', 'create', 'update'] }),
50
51
  reference: Flags.string({ description: 'Caller-defined text stored on the import run, for tracking and filtering' }),
@@ -180,6 +181,17 @@ export default class PriceLists extends Command {
180
181
  }
181
182
  }
182
183
  }
184
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
185
+ // have them: their body is not JSON, so --set cannot carry a value there.
186
+ for (const param of endpoint.specParams) {
187
+ const value = flags[param.flag];
188
+ if (value === undefined || value === '') {
189
+ if (param.required)
190
+ this.error(`Missing required flag: --${param.flag}`);
191
+ continue;
192
+ }
193
+ queryParams[param.name] = value;
194
+ }
183
195
  let data;
184
196
  if (endpoint.isMultipart) {
185
197
  if (!flags.file)
@@ -200,6 +212,25 @@ export default class PriceLists extends Command {
200
212
  }
201
213
  data = form;
202
214
  }
215
+ else if (endpoint.bodyMediaType) {
216
+ // The body is not JSON (today only application/x-ndjson): it is sent
217
+ // verbatim, under the content type the endpoint asks for.
218
+ if (flags.file) {
219
+ const fsModule = await import('node:fs');
220
+ try {
221
+ data = fsModule.readFileSync(flags.file, 'utf-8');
222
+ }
223
+ catch {
224
+ this.error(`Failed to read file: ${flags.file}`);
225
+ }
226
+ }
227
+ else if (flags.data) {
228
+ data = flags.data;
229
+ }
230
+ else {
231
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
232
+ }
233
+ }
203
234
  else if (endpoint.hasBody) {
204
235
  // For binary-response endpoints, --file is the output path, not a JSON
205
236
  // input file — skip the read-as-body branch.
@@ -295,7 +326,7 @@ export default class PriceLists extends Command {
295
326
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
296
327
  }
297
328
  else if (data) {
298
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
329
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
299
330
  }
300
331
  process.stderr.write('\n');
301
332
  }
@@ -309,6 +340,7 @@ export default class PriceLists extends Command {
309
340
  const binResponse = await apiRequest(endpoint.method, apiPath, {
310
341
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
311
342
  data,
343
+ contentType: endpoint.bodyMediaType || undefined,
312
344
  contextName: flags.context,
313
345
  binary: true,
314
346
  });
@@ -334,6 +366,7 @@ export default class PriceLists extends Command {
334
366
  const raw = await apiRequest(endpoint.method, apiPath, {
335
367
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
336
368
  data,
369
+ contentType: endpoint.bodyMediaType || undefined,
337
370
  contextName: flags.context,
338
371
  binary: true,
339
372
  });
@@ -354,6 +387,7 @@ export default class PriceLists extends Command {
354
387
  response = await apiRequest(endpoint.method, apiPath, {
355
388
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
356
389
  data,
390
+ contentType: endpoint.bodyMediaType || undefined,
357
391
  contextName: flags.context,
358
392
  acceptAny: true,
359
393
  });