@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,9 +12,9 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["get", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'get': { method: 'GET', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Get integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'update': { method: 'PUT', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Create or update integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'delete': { method: 'DELETE', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Delete integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'get': { method: 'GET', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Get integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'update': { method: 'PUT', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Create or update integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'delete': { method: 'DELETE', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Delete integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
18
  };
18
19
  export default class Integrations extends Command {
19
20
  static description = 'Manage integrations';
@@ -31,7 +32,7 @@ export default class Integrations extends Command {
31
32
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
32
33
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
33
34
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
34
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
35
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
35
36
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
36
37
  'config-key': Flags.string({ description: 'Path param: configKey' }),
37
38
  page: Flags.string({ description: 'Page number' }),
@@ -163,6 +164,17 @@ export default class Integrations extends Command {
163
164
  }
164
165
  }
165
166
  }
167
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
168
+ // have them: their body is not JSON, so --set cannot carry a value there.
169
+ for (const param of endpoint.specParams) {
170
+ const value = flags[param.flag];
171
+ if (value === undefined || value === '') {
172
+ if (param.required)
173
+ this.error(`Missing required flag: --${param.flag}`);
174
+ continue;
175
+ }
176
+ queryParams[param.name] = value;
177
+ }
166
178
  let data;
167
179
  if (endpoint.isMultipart) {
168
180
  if (!flags.file)
@@ -183,6 +195,25 @@ export default class Integrations extends Command {
183
195
  }
184
196
  data = form;
185
197
  }
198
+ else if (endpoint.bodyMediaType) {
199
+ // The body is not JSON (today only application/x-ndjson): it is sent
200
+ // verbatim, under the content type the endpoint asks for.
201
+ if (flags.file) {
202
+ const fsModule = await import('node:fs');
203
+ try {
204
+ data = fsModule.readFileSync(flags.file, 'utf-8');
205
+ }
206
+ catch {
207
+ this.error(`Failed to read file: ${flags.file}`);
208
+ }
209
+ }
210
+ else if (flags.data) {
211
+ data = flags.data;
212
+ }
213
+ else {
214
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
215
+ }
216
+ }
186
217
  else if (endpoint.hasBody) {
187
218
  // For binary-response endpoints, --file is the output path, not a JSON
188
219
  // input file — skip the read-as-body branch.
@@ -278,7 +309,7 @@ export default class Integrations extends Command {
278
309
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
279
310
  }
280
311
  else if (data) {
281
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
312
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
282
313
  }
283
314
  process.stderr.write('\n');
284
315
  }
@@ -292,6 +323,7 @@ export default class Integrations extends Command {
292
323
  const binResponse = await apiRequest(endpoint.method, apiPath, {
293
324
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
294
325
  data,
326
+ contentType: endpoint.bodyMediaType || undefined,
295
327
  contextName: flags.context,
296
328
  binary: true,
297
329
  });
@@ -317,6 +349,7 @@ export default class Integrations extends Command {
317
349
  const raw = await apiRequest(endpoint.method, apiPath, {
318
350
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
319
351
  data,
352
+ contentType: endpoint.bodyMediaType || undefined,
320
353
  contextName: flags.context,
321
354
  binary: true,
322
355
  });
@@ -337,6 +370,7 @@ export default class Integrations extends Command {
337
370
  response = await apiRequest(endpoint.method, apiPath, {
338
371
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
339
372
  data,
373
+ contentType: endpoint.bodyMediaType || undefined,
340
374
  contextName: flags.context,
341
375
  acceptAny: true,
342
376
  });
@@ -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/item-categories', summary: 'Get all item categories with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/item-categories', summary: 'Create a new item category', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/item-categories/{id}', summary: 'Get an item category by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/item-categories/{id}', summary: 'Update an item category', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/item-categories/{id}', summary: 'Delete an item category', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/item-categories', summary: 'Get all item categories with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/item-categories', summary: 'Create a new item category', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/item-categories/{id}', summary: 'Get an item category by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/item-categories/{id}', summary: 'Update an item category', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/item-categories/{id}', summary: 'Delete an item category', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
20
  };
20
21
  export default class ItemCategories extends Command {
21
22
  static description = 'Manage item categories';
@@ -37,7 +38,7 @@ export default class ItemCategories 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 ItemCategories 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 ItemCategories 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 ItemCategories 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 ItemCategories 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 ItemCategories 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 ItemCategories 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,12 +12,12 @@ const SERVER_IMPORTS = ["import"];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "import"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/items', summary: 'Get all items with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/items', summary: 'Create a new item', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['replacedByIds', 'tagIds', 'tagCodes'] },
16
- 'get': { method: 'GET', path: '/v1/items/{id}', summary: 'Get an item by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/items/{id}', summary: 'Update an item', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['replacedByIds', 'tagIds', 'tagCodes'] },
18
- 'delete': { method: 'DELETE', path: '/v1/items/{id}', summary: 'Delete an item', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'import': { method: 'POST', path: '/v1/items/import', summary: 'Import items', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/items', summary: 'Get all items with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/items', summary: 'Create a new item', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['replacedByIds', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/items/{id}', summary: 'Get an item by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/items/{id}', summary: 'Update an item', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['replacedByIds', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/items/{id}', summary: 'Delete an item', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'import': { method: 'POST', path: '/v1/items/import', summary: 'Import items', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
21
  };
21
22
  export default class Items extends Command {
22
23
  static description = 'Manage items';
@@ -38,7 +39,7 @@ export default class Items extends Command {
38
39
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
39
40
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
40
41
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
41
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
42
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
42
43
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
43
44
  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'] }),
44
45
  reference: Flags.string({ description: 'Caller-defined text stored on the import run, for tracking and filtering' }),
@@ -173,6 +174,17 @@ export default class Items extends Command {
173
174
  }
174
175
  }
175
176
  }
177
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
178
+ // have them: their body is not JSON, so --set cannot carry a value there.
179
+ for (const param of endpoint.specParams) {
180
+ const value = flags[param.flag];
181
+ if (value === undefined || value === '') {
182
+ if (param.required)
183
+ this.error(`Missing required flag: --${param.flag}`);
184
+ continue;
185
+ }
186
+ queryParams[param.name] = value;
187
+ }
176
188
  let data;
177
189
  if (endpoint.isMultipart) {
178
190
  if (!flags.file)
@@ -193,6 +205,25 @@ export default class Items extends Command {
193
205
  }
194
206
  data = form;
195
207
  }
208
+ else if (endpoint.bodyMediaType) {
209
+ // The body is not JSON (today only application/x-ndjson): it is sent
210
+ // verbatim, under the content type the endpoint asks for.
211
+ if (flags.file) {
212
+ const fsModule = await import('node:fs');
213
+ try {
214
+ data = fsModule.readFileSync(flags.file, 'utf-8');
215
+ }
216
+ catch {
217
+ this.error(`Failed to read file: ${flags.file}`);
218
+ }
219
+ }
220
+ else if (flags.data) {
221
+ data = flags.data;
222
+ }
223
+ else {
224
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
225
+ }
226
+ }
196
227
  else if (endpoint.hasBody) {
197
228
  // For binary-response endpoints, --file is the output path, not a JSON
198
229
  // input file — skip the read-as-body branch.
@@ -288,7 +319,7 @@ export default class Items extends Command {
288
319
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
289
320
  }
290
321
  else if (data) {
291
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
322
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
292
323
  }
293
324
  process.stderr.write('\n');
294
325
  }
@@ -302,6 +333,7 @@ export default class Items extends Command {
302
333
  const binResponse = await apiRequest(endpoint.method, apiPath, {
303
334
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
304
335
  data,
336
+ contentType: endpoint.bodyMediaType || undefined,
305
337
  contextName: flags.context,
306
338
  binary: true,
307
339
  });
@@ -327,6 +359,7 @@ export default class Items extends Command {
327
359
  const raw = await apiRequest(endpoint.method, apiPath, {
328
360
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
329
361
  data,
362
+ contentType: endpoint.bodyMediaType || undefined,
330
363
  contextName: flags.context,
331
364
  binary: true,
332
365
  });
@@ -347,6 +380,7 @@ export default class Items extends Command {
347
380
  response = await apiRequest(endpoint.method, apiPath, {
348
381
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
349
382
  data,
383
+ contentType: endpoint.bodyMediaType || undefined,
350
384
  contextName: flags.context,
351
385
  acceptAny: true,
352
386
  });
@@ -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/journal-lines', summary: 'List all journal lines (global view across all journals)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/journal-lines', summary: 'List all journal lines (global view across all journals)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
15
16
  };
16
17
  export default class JournalLines extends Command {
17
18
  static description = 'Manage journal lines';
@@ -29,7 +30,7 @@ export default class JournalLines 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 JournalLines 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 JournalLines 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 JournalLines 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 JournalLines 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 JournalLines 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 JournalLines 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,31 +12,31 @@ const SERVER_IMPORTS = ["import"];
11
12
  const NESTED_UPSERTS = ["notes", "files"];
12
13
  const SUBCOMMANDS = [...["list", "create", "stats", "get", "update", "delete", "post", "approve", "cancel", "reverse", "update-lines", "get-deferrals", "create-deferrals", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "import"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/journals', summary: 'List all journals', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/journals', summary: 'Create a new journal (with embedded lines)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines', 'files', 'notes'] },
16
- 'stats': { method: 'GET', path: '/v1/journals/stats', summary: 'Journal counts and debit totals per date range', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'get': { method: 'GET', path: '/v1/journals/{id}', summary: 'Get a journal by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'update': { method: 'PATCH', path: '/v1/journals/{id}', summary: 'Update a journal (with embedded lines upsert)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
19
- 'delete': { method: 'DELETE', path: '/v1/journals/{id}', summary: 'Delete a journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'post': { method: 'POST', path: '/v1/journals/{id}/post', summary: 'Post journal (finalize)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'approve': { method: 'POST', path: '/v1/journals/{id}/approve', summary: 'Approve a journal (transitions DRAFT to APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'cancel': { method: 'POST', path: '/v1/journals/{id}/cancel', summary: 'Cancel a journal', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'reverse': { method: 'POST', path: '/v1/journals/{id}/reverse', summary: 'Reverse a posted journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'update-lines': { method: 'PATCH', path: '/v1/journals/{id}/lines/{lineId}', summary: 'Update a single journal line in place', pathParams: ['id', 'lineId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes'] },
25
- 'get-deferrals': { method: 'GET', path: '/v1/journals/{id}/deferrals', summary: 'List the deferral schedules of a journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'create-deferrals': { method: 'POST', path: '/v1/journals/{id}/deferrals', summary: 'Create deferral schedules for one line of a journal (manual trigger)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['periods'] },
27
- 'list-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes', summary: 'Get all notes for a journal', pathParams: ['journalId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'create-notes': { method: 'POST', path: '/v1/journals/{journalId}/notes', summary: 'Create a note for a journal', pathParams: ['journalId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'get-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Get a specific note for a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'update-notes': { method: 'PATCH', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Update a note for a journal', pathParams: ['journalId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'delete-notes': { method: 'DELETE', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Delete a note from a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
- 'list-files': { method: 'GET', path: '/v1/journals/{parentId}/files', summary: 'List file attachments for a journal', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
33
- 'create-files': { method: 'POST', path: '/v1/journals/{parentId}/files', summary: 'Upload a file attachment to a journal', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
- 'get-files': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Get a file attachment for a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
- 'update-files': { method: 'PATCH', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a journal', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
- 'delete-files': { method: 'DELETE', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
- 'files-download': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
38
- 'import': { method: 'POST', path: '/v1/journals/import', summary: 'Import journals', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/journals', summary: 'List all journals', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/journals', summary: 'Create a new journal (with embedded lines)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines', 'files', 'notes'], bodyMediaType: '', specParams: [] },
17
+ 'stats': { method: 'GET', path: '/v1/journals/stats', summary: 'Journal counts and debit totals per date range', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'get': { method: 'GET', path: '/v1/journals/{id}', summary: 'Get a journal by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'update': { method: 'PATCH', path: '/v1/journals/{id}', summary: 'Update a journal (with embedded lines upsert)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
20
+ 'delete': { method: 'DELETE', path: '/v1/journals/{id}', summary: 'Delete a journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'post': { method: 'POST', path: '/v1/journals/{id}/post', summary: 'Post journal (finalize)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'approve': { method: 'POST', path: '/v1/journals/{id}/approve', summary: 'Approve a journal (transitions DRAFT to APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'cancel': { method: 'POST', path: '/v1/journals/{id}/cancel', summary: 'Cancel a journal', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'reverse': { method: 'POST', path: '/v1/journals/{id}/reverse', summary: 'Reverse a posted journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'update-lines': { method: 'PATCH', path: '/v1/journals/{id}/lines/{lineId}', summary: 'Update a single journal line in place', pathParams: ['id', 'lineId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
26
+ 'get-deferrals': { method: 'GET', path: '/v1/journals/{id}/deferrals', summary: 'List the deferral schedules of a journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'create-deferrals': { method: 'POST', path: '/v1/journals/{id}/deferrals', summary: 'Create deferral schedules for one line of a journal (manual trigger)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['periods'], bodyMediaType: '', specParams: [] },
28
+ 'list-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes', summary: 'Get all notes for a journal', pathParams: ['journalId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'create-notes': { method: 'POST', path: '/v1/journals/{journalId}/notes', summary: 'Create a note for a journal', pathParams: ['journalId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'get-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Get a specific note for a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'update-notes': { method: 'PATCH', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Update a note for a journal', pathParams: ['journalId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'delete-notes': { method: 'DELETE', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Delete a note from a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
33
+ 'list-files': { method: 'GET', path: '/v1/journals/{parentId}/files', summary: 'List file attachments for a journal', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
34
+ 'create-files': { method: 'POST', path: '/v1/journals/{parentId}/files', summary: 'Upload a file attachment to a journal', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
35
+ 'get-files': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Get a file attachment for a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
+ 'update-files': { method: 'PATCH', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a journal', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
37
+ 'delete-files': { method: 'DELETE', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
38
+ 'files-download': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
+ 'import': { method: 'POST', path: '/v1/journals/import', summary: 'Import journals', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
40
  };
40
41
  export default class Journals extends Command {
41
42
  static description = 'Manage journals';
@@ -57,7 +58,7 @@ export default class Journals extends Command {
57
58
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
58
59
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
59
60
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
60
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
61
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
61
62
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
62
63
  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'] }),
63
64
  reference: Flags.string({ description: 'Caller-defined text stored on the import run, for tracking and filtering' }),
@@ -195,6 +196,17 @@ export default class Journals extends Command {
195
196
  }
196
197
  }
197
198
  }
199
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
200
+ // have them: their body is not JSON, so --set cannot carry a value there.
201
+ for (const param of endpoint.specParams) {
202
+ const value = flags[param.flag];
203
+ if (value === undefined || value === '') {
204
+ if (param.required)
205
+ this.error(`Missing required flag: --${param.flag}`);
206
+ continue;
207
+ }
208
+ queryParams[param.name] = value;
209
+ }
198
210
  let data;
199
211
  if (endpoint.isMultipart) {
200
212
  if (!flags.file)
@@ -215,6 +227,25 @@ export default class Journals extends Command {
215
227
  }
216
228
  data = form;
217
229
  }
230
+ else if (endpoint.bodyMediaType) {
231
+ // The body is not JSON (today only application/x-ndjson): it is sent
232
+ // verbatim, under the content type the endpoint asks for.
233
+ if (flags.file) {
234
+ const fsModule = await import('node:fs');
235
+ try {
236
+ data = fsModule.readFileSync(flags.file, 'utf-8');
237
+ }
238
+ catch {
239
+ this.error(`Failed to read file: ${flags.file}`);
240
+ }
241
+ }
242
+ else if (flags.data) {
243
+ data = flags.data;
244
+ }
245
+ else {
246
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
247
+ }
248
+ }
218
249
  else if (endpoint.hasBody) {
219
250
  // For binary-response endpoints, --file is the output path, not a JSON
220
251
  // input file — skip the read-as-body branch.
@@ -310,7 +341,7 @@ export default class Journals extends Command {
310
341
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
311
342
  }
312
343
  else if (data) {
313
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
344
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
314
345
  }
315
346
  process.stderr.write('\n');
316
347
  }
@@ -324,6 +355,7 @@ export default class Journals extends Command {
324
355
  const binResponse = await apiRequest(endpoint.method, apiPath, {
325
356
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
326
357
  data,
358
+ contentType: endpoint.bodyMediaType || undefined,
327
359
  contextName: flags.context,
328
360
  binary: true,
329
361
  });
@@ -349,6 +381,7 @@ export default class Journals extends Command {
349
381
  const raw = await apiRequest(endpoint.method, apiPath, {
350
382
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
351
383
  data,
384
+ contentType: endpoint.bodyMediaType || undefined,
352
385
  contextName: flags.context,
353
386
  binary: true,
354
387
  });
@@ -369,6 +402,7 @@ export default class Journals extends Command {
369
402
  response = await apiRequest(endpoint.method, apiPath, {
370
403
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
371
404
  data,
405
+ contentType: endpoint.bodyMediaType || undefined,
372
406
  contextName: flags.context,
373
407
  acceptAny: true,
374
408
  });