@aderis-erp/cli 1.0.10 → 1.0.12

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 (67) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/access-groups.js +178 -86
  3. package/dist/commands/account-transaction-allocations.js +178 -86
  4. package/dist/commands/account-transactions.js +181 -87
  5. package/dist/commands/accounts.js +178 -86
  6. package/dist/commands/agent-runs.d.ts +1 -0
  7. package/dist/commands/agent-runs.js +183 -87
  8. package/dist/commands/audit-log.js +178 -86
  9. package/dist/commands/auth/login.js +4 -3
  10. package/dist/commands/bank-accounts.js +178 -86
  11. package/dist/commands/bank-integrations.js +178 -86
  12. package/dist/commands/business-partners.js +178 -86
  13. package/dist/commands/chat.d.ts +40 -0
  14. package/dist/commands/chat.js +1173 -0
  15. package/dist/commands/config/add.js +4 -3
  16. package/dist/commands/credit-card-accounts.js +178 -86
  17. package/dist/commands/documents.js +178 -86
  18. package/dist/commands/dunning-configs.js +180 -88
  19. package/dist/commands/dunning-reminders.js +178 -86
  20. package/dist/commands/dunning-runs.js +178 -86
  21. package/dist/commands/email-transactions.d.ts +40 -0
  22. package/dist/commands/email-transactions.js +1173 -0
  23. package/dist/commands/employees.js +180 -88
  24. package/dist/commands/entity-configurations.d.ts +40 -0
  25. package/dist/commands/entity-configurations.js +1169 -0
  26. package/dist/commands/event-processor-mappings.js +178 -86
  27. package/dist/commands/files-internal.js +178 -86
  28. package/dist/commands/health.js +178 -86
  29. package/dist/commands/inbox.js +181 -89
  30. package/dist/commands/integrations.js +178 -86
  31. package/dist/commands/item-categories.js +178 -86
  32. package/dist/commands/items.js +178 -86
  33. package/dist/commands/journal-lines.js +178 -86
  34. package/dist/commands/journals.js +178 -86
  35. package/dist/commands/ledgers.js +178 -86
  36. package/dist/commands/office-locations.js +178 -86
  37. package/dist/commands/payment-methods.js +178 -86
  38. package/dist/commands/payment-runs.js +178 -86
  39. package/dist/commands/payment-terms.js +178 -86
  40. package/dist/commands/price-lists.js +178 -86
  41. package/dist/commands/processor-mappings.js +178 -86
  42. package/dist/commands/processors.js +181 -87
  43. package/dist/commands/purchase-order-types.d.ts +40 -0
  44. package/dist/commands/purchase-order-types.js +1178 -0
  45. package/dist/commands/purchase-orders.d.ts +42 -0
  46. package/dist/commands/purchase-orders.js +1192 -0
  47. package/dist/commands/reports.js +178 -86
  48. package/dist/commands/salary-types.js +178 -86
  49. package/dist/commands/sales-invoices.js +187 -93
  50. package/dist/commands/sales-orders.js +186 -93
  51. package/dist/commands/saved-views.js +178 -86
  52. package/dist/commands/tags.js +178 -86
  53. package/dist/commands/tax-codes.js +180 -88
  54. package/dist/commands/tax-matrix.js +178 -86
  55. package/dist/commands/tenants.js +180 -88
  56. package/dist/commands/text-templates.js +178 -86
  57. package/dist/commands/tools.js +178 -86
  58. package/dist/commands/vat-declarations.d.ts +1 -0
  59. package/dist/commands/vat-declarations.js +188 -89
  60. package/dist/commands/vendor-invoices.js +181 -87
  61. package/dist/commands/webhooks.js +178 -86
  62. package/dist/commands/workflows.js +178 -86
  63. package/dist/lib/auth.js +2 -2
  64. package/dist/lib/config.d.ts +1 -1
  65. package/dist/lib/config.js +5 -0
  66. package/oclif.manifest.json +1174 -328
  67. package/package.json +2 -1
@@ -6,24 +6,25 @@ const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["line-items", "notes", "files"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "pdf", "submit", "approve", "accept", "terminate", "cancel", "replace", "create-invoice", "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_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "pdf", "submit", "approve", "accept", "terminate", "cancel", "replace", "create-invoice", "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_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
11
  'list': { method: 'GET', path: '/v1/sales-orders', summary: 'Get all sales orders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/sales-orders', summary: 'Create a new sales order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'lineItems'] },
12
+ 'create': { method: 'POST', path: '/v1/sales-orders', summary: 'Create a new sales order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'discounts', 'lineItems'] },
13
13
  'get': { method: 'GET', path: '/v1/sales-orders/{id}', summary: 'Get a sales order by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/sales-orders/{id}', summary: 'Update a sales order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'lineItems', 'removeFileIds'] },
14
+ 'update': { method: 'PATCH', path: '/v1/sales-orders/{id}', summary: 'Update a sales order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'discounts', 'lineItems', 'removeFileIds'] },
15
15
  'delete': { method: 'DELETE', path: '/v1/sales-orders/{id}', summary: 'Delete a sales order', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  'pdf': { method: 'GET', path: '/v1/sales-orders/{id}/pdf', summary: 'Render a sales order PDF using the configured PDF processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'submit': { method: 'POST', path: '/v1/sales-orders/{id}/submit', summary: 'Submit order for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'approve': { method: 'POST', path: '/v1/sales-orders/{id}/approve', summary: 'Approve order (PENDING_APPROVAL → APPROVED). Generates order number.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
+ 'submit': { method: 'POST', path: '/v1/sales-orders/{id}/submit', summary: 'Submit order for approval (DRAFT or CANCELLED → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
+ 'approve': { method: 'POST', path: '/v1/sales-orders/{id}/approve', summary: 'Approve order (DRAFT, PENDING_APPROVAL or CANCELLED → APPROVED). Generates order number if missing.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
19
  'accept': { method: 'POST', path: '/v1/sales-orders/{id}/accept', summary: 'Accept order (APPROVED → ACCEPTED). Adjusts billing start date.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
20
  'terminate': { method: 'POST', path: '/v1/sales-orders/{id}/terminate', summary: 'Terminate order (ACCEPTED → TERMINATED). Requires billingEndDate.', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
21
  'cancel': { method: 'POST', path: '/v1/sales-orders/{id}/cancel', summary: 'Cancel order (any pre-active status → CANCELLED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
22
  'replace': { method: 'POST', path: '/v1/sales-orders/{id}/replace', summary: 'Replace order with corrected version (APPROVED → creates new DRAFT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
23
  'create-invoice': { method: 'POST', path: '/v1/sales-orders/{id}/create-invoice', summary: 'Create invoice from sales order (ACCEPTED → creates DRAFT invoice)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'create-line-items': { method: 'POST', path: '/v1/sales-orders/{orderId}/line-items', summary: 'Add a line item to a sales order', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
+ 'send': { method: 'POST', path: '/v1/sales-orders/{id}/send', summary: 'Send a sales order to the customer by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['emailCc', 'emailBcc', 'files'] },
25
+ 'create-line-items': { method: 'POST', path: '/v1/sales-orders/{orderId}/line-items', summary: 'Add a line item to a sales order', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['discounts'] },
25
26
  'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-orders/{orderId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['items'] },
26
- 'update-line-items': { method: 'PATCH', path: '/v1/sales-orders/{orderId}/line-items/{lineItemId}', summary: 'Update a line item on a sales order', pathParams: ['orderId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
+ 'update-line-items': { method: 'PATCH', path: '/v1/sales-orders/{orderId}/line-items/{lineItemId}', summary: 'Update a line item on a sales order', pathParams: ['orderId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['discounts'] },
27
28
  'delete-line-items': { method: 'DELETE', path: '/v1/sales-orders/{orderId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales order', pathParams: ['orderId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
28
29
  'list-notes': { method: 'GET', path: '/v1/sales-orders/{salesOrderId}/notes', summary: 'Get all notes for a sales order', pathParams: ['salesOrderId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
30
  'create-notes': { method: 'POST', path: '/v1/sales-orders/{salesOrderId}/notes', summary: 'Create a note for a sales order', pathParams: ['salesOrderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
@@ -457,76 +458,126 @@ export default class SalesOrders extends Command {
457
458
  return '';
458
459
  };
459
460
  const results = [];
460
- for (const item of items) {
461
+ // Stream output row-by-row so progress is visible during long imports.
462
+ // Pre-compute the identifier column width from the input items so the
463
+ // table stays aligned without buffering all rows.
464
+ // When stdout is piped (IDE terminals, tee, CI), Node switches to async-buffered
465
+ // mode and rows accumulate in the pipe buffer until it fills. Force blocking
466
+ // mode so each \n-terminated write flushes immediately.
467
+ const stdoutHandle = process.stdout._handle;
468
+ if (stdoutHandle?.setBlocking)
469
+ stdoutHandle.setBlocking(true);
470
+ const outputFormat = flags.output || 'table';
471
+ const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
472
+ const useJson = outputFormat === 'json';
473
+ const statusWidth = 8;
474
+ const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
475
+ if (useCsv) {
476
+ this.log('status,identifier,id,error');
477
+ }
478
+ else if (!useJson) {
479
+ this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
480
+ this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
481
+ }
482
+ const emitRow = (r) => {
483
+ if (useJson)
484
+ return;
485
+ if (useCsv) {
486
+ const err = (r.error || '').replace(/"/g, '""');
487
+ this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
488
+ }
489
+ else {
490
+ this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
491
+ }
492
+ };
493
+ // Server-managed fields the API rejects in create/update bodies. Stripped
494
+ // before every request so re-importing a previously-exported payload Just Works.
495
+ const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
496
+ const stripServerFields = (item) => {
497
+ const out = {};
498
+ for (const [k, v] of Object.entries(item)) {
499
+ if (!STRIP_FIELDS.has(k))
500
+ out[k] = v;
501
+ }
502
+ return out;
503
+ };
504
+ const processItem = async (item) => {
461
505
  const identifier = getItemIdentifier(item);
506
+ const body = stripServerFields(item);
507
+ // If the payload carries an id (e.g. re-importing an export), PATCH directly.
508
+ // Fall through to POST only on 404 (so a missing record still gets created).
509
+ const itemId = typeof item.id === 'string' ? item.id : undefined;
510
+ if (itemId) {
511
+ try {
512
+ const updatePath = updateEndpoint.path.replace('{id}', itemId);
513
+ await apiRequest('PATCH', updatePath, {
514
+ data: body,
515
+ contextName: flags.context,
516
+ });
517
+ return { status: 'updated', id: itemId, identifier };
518
+ }
519
+ catch (error) {
520
+ if (!(error instanceof ApiError && error.status === 404)) {
521
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
522
+ return { status: 'error', id: itemId, identifier, error: msg };
523
+ }
524
+ // 404 → record gone, try create below
525
+ }
526
+ }
462
527
  try {
463
- // Try to create
464
528
  const response = await apiRequest('POST', createEndpoint.path, {
465
- data: item,
529
+ data: body,
466
530
  contextName: flags.context,
467
531
  });
468
532
  const id = response.data?.id;
469
- results.push({ status: 'created', id: id ? String(id) : undefined, identifier });
533
+ return { status: 'created', id: id ? String(id) : undefined, identifier };
470
534
  }
471
535
  catch (error) {
472
536
  if (error instanceof ApiError && error.status === 409) {
473
- // Conflict - try to update existing
474
537
  const errorData = error.data;
475
538
  const existingId = errorData?.existingId;
476
539
  if (existingId) {
477
540
  try {
478
541
  const updatePath = updateEndpoint.path.replace('{id}', existingId);
479
542
  await apiRequest('PATCH', updatePath, {
480
- data: item,
543
+ data: body,
481
544
  contextName: flags.context,
482
545
  });
483
- results.push({ status: 'updated', id: existingId, identifier });
546
+ return { status: 'updated', id: existingId, identifier };
484
547
  }
485
548
  catch (updateError) {
486
549
  const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
487
- results.push({ status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` });
550
+ return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
488
551
  }
489
552
  }
490
- else {
491
- results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
492
- }
493
- }
494
- else {
495
- const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
496
- results.push({ status: 'error', identifier, error: msg });
553
+ return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
497
554
  }
555
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
556
+ return { status: 'error', identifier, error: msg };
498
557
  }
499
- }
558
+ };
559
+ // Worker pool: keep N requests in flight at all times. Rows are emitted as
560
+ // each request resolves (so output order may differ from input order). The
561
+ // summary at the end remains accurate regardless.
562
+ const concurrency = 4;
563
+ const queue = [...items];
564
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
565
+ for (;;) {
566
+ const item = queue.shift();
567
+ if (!item)
568
+ return;
569
+ const r = await processItem(item);
570
+ results.push(r);
571
+ emitRow(r);
572
+ }
573
+ }));
500
574
  // Summary counts
501
575
  const created = results.filter(r => r.status === 'created').length;
502
576
  const updated = results.filter(r => r.status === 'updated').length;
503
577
  const errors = results.filter(r => r.status === 'error');
504
- // Output results based on format (default: table)
505
- const outputFormat = flags.output || 'table';
506
- if (outputFormat === 'json') {
578
+ if (useJson) {
507
579
  this.log(JSON.stringify(results, null, 2));
508
580
  }
509
- else if (outputFormat === 'csv' || flags.csv) {
510
- // CSV output
511
- this.log('status,identifier,id,error');
512
- for (const r of results) {
513
- const err = (r.error || '').replace(/"/g, '""');
514
- this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
515
- }
516
- }
517
- else {
518
- // Table output (default)
519
- if (results.length > 0) {
520
- const statusWidth = 8;
521
- const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
522
- this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
523
- this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
524
- for (const r of results) {
525
- const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
526
- this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
527
- }
528
- }
529
- }
530
581
  // Always show summary at the end
531
582
  this.log('');
532
583
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
@@ -806,80 +857,122 @@ export default class SalesOrders extends Command {
806
857
  return '';
807
858
  };
808
859
  const results = [];
809
- for (const item of items) {
860
+ const stdoutHandle = process.stdout._handle;
861
+ if (stdoutHandle?.setBlocking)
862
+ stdoutHandle.setBlocking(true);
863
+ const outputFormat = flags.output || 'table';
864
+ const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
865
+ const useJson = outputFormat === 'json';
866
+ const statusWidth = 8;
867
+ const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
868
+ if (useCsv) {
869
+ this.log('status,identifier,id,error');
870
+ }
871
+ else if (!useJson) {
872
+ this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
873
+ this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
874
+ }
875
+ const emitRow = (r) => {
876
+ if (useJson)
877
+ return;
878
+ if (useCsv) {
879
+ const err = (r.error || '').replace(/"/g, '""');
880
+ this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
881
+ }
882
+ else {
883
+ this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
884
+ }
885
+ };
886
+ const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
887
+ const stripServerFields = (item) => {
888
+ const out = {};
889
+ for (const [k, v] of Object.entries(item)) {
890
+ if (!STRIP_FIELDS.has(k))
891
+ out[k] = v;
892
+ }
893
+ return out;
894
+ };
895
+ const buildUpdatePath = (existingId) => {
896
+ const firstUpdateParam = updateEndpoint.pathParams[0];
897
+ const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
898
+ let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
899
+ if (nestedIdParam) {
900
+ updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
901
+ }
902
+ return updatePath;
903
+ };
904
+ const processItem = async (item) => {
810
905
  const identifier = getItemIdentifier(item);
906
+ const body = stripServerFields(item);
907
+ const itemId = typeof item.id === 'string' ? item.id : undefined;
908
+ if (itemId) {
909
+ try {
910
+ await apiRequest('PATCH', buildUpdatePath(itemId), {
911
+ data: body,
912
+ contextName: flags.context,
913
+ });
914
+ return { status: 'updated', id: itemId, identifier };
915
+ }
916
+ catch (error) {
917
+ if (!(error instanceof ApiError && error.status === 404)) {
918
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
919
+ return { status: 'error', id: itemId, identifier, error: msg };
920
+ }
921
+ // 404 → fall through and try create
922
+ }
923
+ }
811
924
  try {
812
- // Try to create
813
925
  const response = await apiRequest('POST', basePath, {
814
- data: item,
926
+ data: body,
815
927
  contextName: flags.context,
816
928
  });
817
929
  const id = response.data?.id;
818
- results.push({ status: 'created', id: id ? String(id) : undefined, identifier });
930
+ return { status: 'created', id: id ? String(id) : undefined, identifier };
819
931
  }
820
932
  catch (error) {
821
933
  if (error instanceof ApiError && error.status === 409) {
822
- // Conflict - try to update existing
823
934
  const errorData = error.data;
824
935
  const existingId = errorData?.existingId || item.id;
825
936
  if (existingId) {
826
937
  try {
827
- // Find the nested ID param name (e.g., 'priceId' for prices)
828
- const firstUpdateParam = updateEndpoint.pathParams[0];
829
- const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
830
- let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
831
- if (nestedIdParam) {
832
- updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
833
- }
834
- await apiRequest('PATCH', updatePath, {
835
- data: item,
938
+ await apiRequest('PATCH', buildUpdatePath(existingId), {
939
+ data: body,
836
940
  contextName: flags.context,
837
941
  });
838
- results.push({ status: 'updated', id: existingId, identifier });
942
+ return { status: 'updated', id: existingId, identifier };
839
943
  }
840
944
  catch (updateError) {
841
945
  const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
842
- results.push({ status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` });
946
+ return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
843
947
  }
844
948
  }
845
- else {
846
- results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
847
- }
848
- }
849
- else {
850
- const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
851
- results.push({ status: 'error', identifier, error: msg });
949
+ return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
852
950
  }
951
+ const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
952
+ return { status: 'error', identifier, error: msg };
853
953
  }
854
- }
954
+ };
955
+ // Worker pool: 4 requests in flight at a time. Rows are emitted as each
956
+ // request resolves so output order may differ from input order.
957
+ const concurrency = 4;
958
+ const queue = [...items];
959
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
960
+ for (;;) {
961
+ const item = queue.shift();
962
+ if (!item)
963
+ return;
964
+ const r = await processItem(item);
965
+ results.push(r);
966
+ emitRow(r);
967
+ }
968
+ }));
855
969
  // Summary counts
856
970
  const created = results.filter(r => r.status === 'created').length;
857
971
  const updated = results.filter(r => r.status === 'updated').length;
858
972
  const errors = results.filter(r => r.status === 'error');
859
- // Output results based on format (default: table)
860
- const outputFormat = flags.output || 'table';
861
- if (outputFormat === 'json') {
973
+ if (useJson) {
862
974
  this.log(JSON.stringify(results, null, 2));
863
975
  }
864
- else if (outputFormat === 'csv' || flags.csv) {
865
- this.log('status,identifier,id,error');
866
- for (const r of results) {
867
- const err = (r.error || '').replace(/"/g, '""');
868
- this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
869
- }
870
- }
871
- else {
872
- if (results.length > 0) {
873
- const statusWidth = 8;
874
- const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
875
- this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
876
- this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
877
- for (const r of results) {
878
- const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
879
- this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
880
- }
881
- }
882
- }
883
976
  this.log('');
884
977
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
885
978
  }