@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,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/currencies', summary: 'GET /v1/currencies', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/currencies', summary: 'GET /v1/currencies', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
15
16
  };
16
17
  export default class Currencies extends Command {
17
18
  static description = 'Manage currencies';
@@ -29,7 +30,7 @@ export default class Currencies 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 Currencies 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 Currencies 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 Currencies 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 Currencies 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 Currencies 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 Currencies 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,11 +12,11 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/custody-accounts', summary: 'List custody accounts', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/custody-accounts', summary: 'Create a custody account', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/custody-accounts/{id}', summary: 'Read a custody account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/custody-accounts/{id}', summary: 'Update a custody account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/custody-accounts/{id}', summary: 'Delete a custody account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/custody-accounts', summary: 'List custody accounts', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/custody-accounts', summary: 'Create a custody account', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/custody-accounts/{id}', summary: 'Read a custody account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/custody-accounts/{id}', summary: 'Update a custody account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/custody-accounts/{id}', summary: 'Delete a custody account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
20
  };
20
21
  export default class CustodyAccounts extends Command {
21
22
  static description = 'Manage custody accounts';
@@ -37,7 +38,7 @@ export default class CustodyAccounts extends Command {
37
38
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
38
39
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
39
40
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
40
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
41
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
41
42
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
42
43
  page: Flags.string({ description: 'Page number' }),
43
44
  limit: Flags.string({ description: 'Items per page' }),
@@ -168,6 +169,17 @@ export default class CustodyAccounts extends Command {
168
169
  }
169
170
  }
170
171
  }
172
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
173
+ // have them: their body is not JSON, so --set cannot carry a value there.
174
+ for (const param of endpoint.specParams) {
175
+ const value = flags[param.flag];
176
+ if (value === undefined || value === '') {
177
+ if (param.required)
178
+ this.error(`Missing required flag: --${param.flag}`);
179
+ continue;
180
+ }
181
+ queryParams[param.name] = value;
182
+ }
171
183
  let data;
172
184
  if (endpoint.isMultipart) {
173
185
  if (!flags.file)
@@ -188,6 +200,25 @@ export default class CustodyAccounts extends Command {
188
200
  }
189
201
  data = form;
190
202
  }
203
+ else if (endpoint.bodyMediaType) {
204
+ // The body is not JSON (today only application/x-ndjson): it is sent
205
+ // verbatim, under the content type the endpoint asks for.
206
+ if (flags.file) {
207
+ const fsModule = await import('node:fs');
208
+ try {
209
+ data = fsModule.readFileSync(flags.file, 'utf-8');
210
+ }
211
+ catch {
212
+ this.error(`Failed to read file: ${flags.file}`);
213
+ }
214
+ }
215
+ else if (flags.data) {
216
+ data = flags.data;
217
+ }
218
+ else {
219
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
220
+ }
221
+ }
191
222
  else if (endpoint.hasBody) {
192
223
  // For binary-response endpoints, --file is the output path, not a JSON
193
224
  // input file — skip the read-as-body branch.
@@ -283,7 +314,7 @@ export default class CustodyAccounts extends Command {
283
314
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
284
315
  }
285
316
  else if (data) {
286
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
317
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
287
318
  }
288
319
  process.stderr.write('\n');
289
320
  }
@@ -297,6 +328,7 @@ export default class CustodyAccounts extends Command {
297
328
  const binResponse = await apiRequest(endpoint.method, apiPath, {
298
329
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
330
  data,
331
+ contentType: endpoint.bodyMediaType || undefined,
300
332
  contextName: flags.context,
301
333
  binary: true,
302
334
  });
@@ -322,6 +354,7 @@ export default class CustodyAccounts extends Command {
322
354
  const raw = await apiRequest(endpoint.method, apiPath, {
323
355
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
324
356
  data,
357
+ contentType: endpoint.bodyMediaType || undefined,
325
358
  contextName: flags.context,
326
359
  binary: true,
327
360
  });
@@ -342,6 +375,7 @@ export default class CustodyAccounts extends Command {
342
375
  response = await apiRequest(endpoint.method, apiPath, {
343
376
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
344
377
  data,
378
+ contentType: endpoint.bodyMediaType || undefined,
345
379
  contextName: flags.context,
346
380
  acceptAny: true,
347
381
  });
@@ -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,22 +12,22 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["widgets"];
12
13
  const SUBCOMMANDS = [...["list-templates", "list-catalog", "home", "list", "create", "duplicate", "get", "update", "delete", "update-layout", "create-widgets", "update-widgets", "delete-widgets", "home-data", "data", "widgets-data"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list-templates': { method: 'GET', path: '/v1/dashboards/templates', summary: 'List the dashboard templates', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'list-catalog': { method: 'GET', path: '/v1/dashboards/catalog', summary: 'List widgets ready to add', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'home': { method: 'GET', path: '/v1/dashboards/home', summary: 'Open the caller\'s home dashboard', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'list': { method: 'GET', path: '/v1/dashboards', summary: 'List the dashboards the caller can open', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'create': { method: 'POST', path: '/v1/dashboards', summary: 'Create a dashboard', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'duplicate': { method: 'POST', path: '/v1/dashboards/{id}/duplicate', summary: 'Copy a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'get': { method: 'GET', path: '/v1/dashboards/{id}', summary: 'Open one dashboard', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'update': { method: 'PATCH', path: '/v1/dashboards/{id}', summary: 'Change a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'delete': { method: 'DELETE', path: '/v1/dashboards/{id}', summary: 'Delete a dashboard and its widgets', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'update-layout': { method: 'PATCH', path: '/v1/dashboards/{id}/layout', summary: 'Save the grid of a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'create-widgets': { method: 'POST', path: '/v1/dashboards/{id}/widgets', summary: 'Add a widget to a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'update-widgets': { method: 'PATCH', path: '/v1/dashboards/{id}/widgets/{widgetId}', summary: 'Replace the config of a widget', pathParams: ['id', 'widgetId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'delete-widgets': { method: 'DELETE', path: '/v1/dashboards/{id}/widgets/{widgetId}', summary: 'Remove a widget from a dashboard', pathParams: ['id', 'widgetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'home-data': { method: 'GET', path: '/v1/dashboards/home/data', summary: 'Evaluate every widget of the caller\'s home dashboard', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'data': { method: 'GET', path: '/v1/dashboards/{id}/data', summary: 'Evaluate every widget of a dashboard for a period', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'widgets-data': { method: 'GET', path: '/v1/dashboards/{id}/widgets/{widgetId}/data', summary: 'Evaluate one widget of a dashboard for a period', pathParams: ['id', 'widgetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list-templates': { method: 'GET', path: '/v1/dashboards/templates', summary: 'List the dashboard templates', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'list-catalog': { method: 'GET', path: '/v1/dashboards/catalog', summary: 'List widgets ready to add', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'home': { method: 'GET', path: '/v1/dashboards/home', summary: 'Open the caller\'s home dashboard', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'list': { method: 'GET', path: '/v1/dashboards', summary: 'List the dashboards the caller can open', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'create': { method: 'POST', path: '/v1/dashboards', summary: 'Create a dashboard', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'duplicate': { method: 'POST', path: '/v1/dashboards/{id}/duplicate', summary: 'Copy a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'get': { method: 'GET', path: '/v1/dashboards/{id}', summary: 'Open one dashboard', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'update': { method: 'PATCH', path: '/v1/dashboards/{id}', summary: 'Change a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'delete': { method: 'DELETE', path: '/v1/dashboards/{id}', summary: 'Delete a dashboard and its widgets', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'update-layout': { method: 'PATCH', path: '/v1/dashboards/{id}/layout', summary: 'Save the grid of a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'create-widgets': { method: 'POST', path: '/v1/dashboards/{id}/widgets', summary: 'Add a widget to a dashboard', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'update-widgets': { method: 'PATCH', path: '/v1/dashboards/{id}/widgets/{widgetId}', summary: 'Replace the config of a widget', pathParams: ['id', 'widgetId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'delete-widgets': { method: 'DELETE', path: '/v1/dashboards/{id}/widgets/{widgetId}', summary: 'Remove a widget from a dashboard', pathParams: ['id', 'widgetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'home-data': { method: 'GET', path: '/v1/dashboards/home/data', summary: 'Evaluate every widget of the caller\'s home dashboard', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'data': { method: 'GET', path: '/v1/dashboards/{id}/data', summary: 'Evaluate every widget of a dashboard for a period', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'widgets-data': { method: 'GET', path: '/v1/dashboards/{id}/widgets/{widgetId}/data', summary: 'Evaluate one widget of a dashboard for a period', pathParams: ['id', 'widgetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
31
  };
31
32
  export default class Dashboards extends Command {
32
33
  static description = 'Manage dashboards';
@@ -48,7 +49,7 @@ export default class Dashboards extends Command {
48
49
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
49
50
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
50
51
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
51
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
52
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
52
53
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
53
54
  'widget-id': Flags.string({ description: 'Path param: widgetId' }),
54
55
  page: Flags.string({ description: 'Page number' }),
@@ -180,6 +181,17 @@ export default class Dashboards 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 Dashboards 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 Dashboards 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 Dashboards 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 Dashboards 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 Dashboards 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
  });
@@ -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,39 +12,39 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["line-items", "notes", "files"];
12
13
  const SUBCOMMANDS = [...["list", "create", "create-from-orders", "create-from-invoice", "create-invoice", "get", "update", "delete", "get-pdf", "create-pdf", "submit", "approve", "cancel", "replace", "copy", "deliver", "ship", "send", "create-line-items", "update-line-items-reorder", "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_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/delivery-notes', summary: 'Get all delivery notes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/delivery-notes', summary: 'Create a new delivery note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'lineItems'] },
16
- 'create-from-orders': { method: 'POST', path: '/v1/delivery-notes/create-from-orders', summary: 'Create a DRAFT delivery note from several sales orders', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderIds'] },
17
- 'create-from-invoice': { method: 'POST', path: '/v1/delivery-notes/create-from-invoice', summary: 'Create a DRAFT delivery note from a sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'create-invoice': { method: 'POST', path: '/v1/delivery-notes/create-invoice', summary: 'Create ONE draft sales invoice from several delivery notes (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['deliveryNoteIds'] },
19
- 'get': { method: 'GET', path: '/v1/delivery-notes/{id}', summary: 'Get a delivery note by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'update': { method: 'PATCH', path: '/v1/delivery-notes/{id}', summary: 'Update a delivery note', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'removeFileIds'] },
21
- 'delete': { method: 'DELETE', path: '/v1/delivery-notes/{id}', summary: 'Delete a delivery note', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'get-pdf': { method: 'GET', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'create-pdf': { method: 'POST', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'submit': { method: 'POST', path: '/v1/delivery-notes/{id}/submit', summary: 'Submit a delivery note for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'approve': { method: 'POST', path: '/v1/delivery-notes/{id}/approve', summary: 'Approve a delivery note (DRAFT or PENDING_APPROVAL → APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'cancel': { method: 'POST', path: '/v1/delivery-notes/{id}/cancel', summary: 'Cancel a delivery note (DRAFT / PENDING_APPROVAL / APPROVED / SHIPPED / DELIVERED → CANCELLED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'replace': { method: 'POST', path: '/v1/delivery-notes/{id}/replace', summary: 'Replace a delivery note (APPROVED / SHIPPED / DELIVERED / CANCELLED → REPLACED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'copy': { method: 'POST', path: '/v1/delivery-notes/{id}/copy', summary: 'Create a copy of a delivery note as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'deliver': { method: 'POST', path: '/v1/delivery-notes/{id}/deliver', summary: 'Mark a delivery note as delivered (SHIPPED → DELIVERED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'ship': { method: 'POST', path: '/v1/delivery-notes/{id}/ship', summary: 'Ship a delivery note (APPROVED → SHIPPED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems'] },
31
- 'send': { method: 'POST', path: '/v1/delivery-notes/{id}/send', summary: 'Send a delivery note to its recipient by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'] },
32
- 'create-line-items': { method: 'POST', path: '/v1/delivery-notes/{noteId}/line-items', summary: 'Add a line item to a delivery note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
33
- 'update-line-items-reorder': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'] },
34
- 'update-line-items': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Update a line item on a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
35
- 'delete-line-items': { method: 'DELETE', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Remove a line item from a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
- 'list-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Get all notes for a delivery note', pathParams: ['deliveryNoteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
- 'create-notes': { method: 'POST', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Create a note for a delivery note', pathParams: ['deliveryNoteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
- 'get-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Get a specific note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
39
- 'update-notes': { method: 'PATCH', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Update a note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
40
- 'delete-notes': { method: 'DELETE', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Delete a note from a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
- 'list-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files', summary: 'List file attachments for a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
- 'create-files': { method: 'POST', path: '/v1/delivery-notes/{parentId}/files', summary: 'Upload a file attachment to a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
43
- 'get-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Get a file attachment for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
44
- 'update-files': { method: 'PATCH', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
45
- 'delete-files': { method: 'DELETE', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
46
- 'files-download': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/delivery-notes', summary: 'Get all delivery notes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/delivery-notes', summary: 'Create a new delivery note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'lineItems'], bodyMediaType: '', specParams: [] },
17
+ 'create-from-orders': { method: 'POST', path: '/v1/delivery-notes/create-from-orders', summary: 'Create a DRAFT delivery note from several sales orders', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderIds'], bodyMediaType: '', specParams: [] },
18
+ 'create-from-invoice': { method: 'POST', path: '/v1/delivery-notes/create-from-invoice', summary: 'Create a DRAFT delivery note from a sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'create-invoice': { method: 'POST', path: '/v1/delivery-notes/create-invoice', summary: 'Create ONE draft sales invoice from several delivery notes (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['deliveryNoteIds'], bodyMediaType: '', specParams: [] },
20
+ 'get': { method: 'GET', path: '/v1/delivery-notes/{id}', summary: 'Get a delivery note by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'update': { method: 'PATCH', path: '/v1/delivery-notes/{id}', summary: 'Update a delivery note', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'removeFileIds'], bodyMediaType: '', specParams: [] },
22
+ 'delete': { method: 'DELETE', path: '/v1/delivery-notes/{id}', summary: 'Delete a delivery note', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'get-pdf': { method: 'GET', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'create-pdf': { method: 'POST', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'submit': { method: 'POST', path: '/v1/delivery-notes/{id}/submit', summary: 'Submit a delivery note for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'approve': { method: 'POST', path: '/v1/delivery-notes/{id}/approve', summary: 'Approve a delivery note (DRAFT or PENDING_APPROVAL → APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'cancel': { method: 'POST', path: '/v1/delivery-notes/{id}/cancel', summary: 'Cancel a delivery note (DRAFT / PENDING_APPROVAL / APPROVED / SHIPPED / DELIVERED → CANCELLED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'replace': { method: 'POST', path: '/v1/delivery-notes/{id}/replace', summary: 'Replace a delivery note (APPROVED / SHIPPED / DELIVERED / CANCELLED → REPLACED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'copy': { method: 'POST', path: '/v1/delivery-notes/{id}/copy', summary: 'Create a copy of a delivery note as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'deliver': { method: 'POST', path: '/v1/delivery-notes/{id}/deliver', summary: 'Mark a delivery note as delivered (SHIPPED → DELIVERED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'ship': { method: 'POST', path: '/v1/delivery-notes/{id}/ship', summary: 'Ship a delivery note (APPROVED → SHIPPED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems'], bodyMediaType: '', specParams: [] },
32
+ 'send': { method: 'POST', path: '/v1/delivery-notes/{id}/send', summary: 'Send a delivery note to its recipient by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'], bodyMediaType: '', specParams: [] },
33
+ 'create-line-items': { method: 'POST', path: '/v1/delivery-notes/{noteId}/line-items', summary: 'Add a line item to a delivery note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
34
+ 'update-line-items-reorder': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'], bodyMediaType: '', specParams: [] },
35
+ 'update-line-items': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Update a line item on a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
36
+ 'delete-line-items': { method: 'DELETE', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Remove a line item from a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
37
+ 'list-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Get all notes for a delivery note', pathParams: ['deliveryNoteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
38
+ 'create-notes': { method: 'POST', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Create a note for a delivery note', pathParams: ['deliveryNoteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
+ 'get-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Get a specific note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
40
+ 'update-notes': { method: 'PATCH', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Update a note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
41
+ 'delete-notes': { method: 'DELETE', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Delete a note from a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
42
+ 'list-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files', summary: 'List file attachments for a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
43
+ 'create-files': { method: 'POST', path: '/v1/delivery-notes/{parentId}/files', summary: 'Upload a file attachment to a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
44
+ 'get-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Get a file attachment for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
45
+ 'update-files': { method: 'PATCH', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
46
+ 'delete-files': { method: 'DELETE', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
47
+ 'files-download': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
47
48
  };
48
49
  export default class DeliveryNotes extends Command {
49
50
  static description = 'Manage delivery notes';
@@ -65,7 +66,7 @@ export default class DeliveryNotes extends Command {
65
66
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
66
67
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
67
68
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
68
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
69
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
69
70
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
70
71
  'line-item-id': Flags.string({ description: 'Path param: lineItemId' }),
71
72
  'note-id': Flags.string({ description: 'Path param: noteId' }),
@@ -199,6 +200,17 @@ export default class DeliveryNotes extends Command {
199
200
  }
200
201
  }
201
202
  }
203
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
204
+ // have them: their body is not JSON, so --set cannot carry a value there.
205
+ for (const param of endpoint.specParams) {
206
+ const value = flags[param.flag];
207
+ if (value === undefined || value === '') {
208
+ if (param.required)
209
+ this.error(`Missing required flag: --${param.flag}`);
210
+ continue;
211
+ }
212
+ queryParams[param.name] = value;
213
+ }
202
214
  let data;
203
215
  if (endpoint.isMultipart) {
204
216
  if (!flags.file)
@@ -219,6 +231,25 @@ export default class DeliveryNotes extends Command {
219
231
  }
220
232
  data = form;
221
233
  }
234
+ else if (endpoint.bodyMediaType) {
235
+ // The body is not JSON (today only application/x-ndjson): it is sent
236
+ // verbatim, under the content type the endpoint asks for.
237
+ if (flags.file) {
238
+ const fsModule = await import('node:fs');
239
+ try {
240
+ data = fsModule.readFileSync(flags.file, 'utf-8');
241
+ }
242
+ catch {
243
+ this.error(`Failed to read file: ${flags.file}`);
244
+ }
245
+ }
246
+ else if (flags.data) {
247
+ data = flags.data;
248
+ }
249
+ else {
250
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
251
+ }
252
+ }
222
253
  else if (endpoint.hasBody) {
223
254
  // For binary-response endpoints, --file is the output path, not a JSON
224
255
  // input file — skip the read-as-body branch.
@@ -314,7 +345,7 @@ export default class DeliveryNotes extends Command {
314
345
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
315
346
  }
316
347
  else if (data) {
317
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
348
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
318
349
  }
319
350
  process.stderr.write('\n');
320
351
  }
@@ -328,6 +359,7 @@ export default class DeliveryNotes extends Command {
328
359
  const binResponse = await apiRequest(endpoint.method, apiPath, {
329
360
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
330
361
  data,
362
+ contentType: endpoint.bodyMediaType || undefined,
331
363
  contextName: flags.context,
332
364
  binary: true,
333
365
  });
@@ -353,6 +385,7 @@ export default class DeliveryNotes extends Command {
353
385
  const raw = await apiRequest(endpoint.method, apiPath, {
354
386
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
355
387
  data,
388
+ contentType: endpoint.bodyMediaType || undefined,
356
389
  contextName: flags.context,
357
390
  binary: true,
358
391
  });
@@ -373,6 +406,7 @@ export default class DeliveryNotes extends Command {
373
406
  response = await apiRequest(endpoint.method, apiPath, {
374
407
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
375
408
  data,
409
+ contentType: endpoint.bodyMediaType || undefined,
376
410
  contextName: flags.context,
377
411
  acceptAny: true,
378
412
  });
@@ -14,6 +14,7 @@ export default class DocumentChecksums extends Command {
14
14
  data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
15
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ percentage: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
18
  page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
19
  limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
20
  include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;