jaz-clio 4.34.6 → 4.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-api
3
- version: 4.34.6
3
+ version: 4.35.0
4
4
  description: >-
5
5
  Use this skill whenever you call, debug, or review code that touches the Jaz
6
6
  REST API. Covers field names, response shapes, 117 production gotchas, error
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-cli
3
- version: 4.34.6
3
+ version: 4.35.0
4
4
  description: >-
5
5
  Use this skill when running Clio CLI commands, building shell scripts with
6
6
  Clio, debugging auth issues, understanding --json output, paginating results,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 4.34.6
3
+ version: 4.35.0
4
4
  description: >-
5
5
  Use this skill when migrating accounting data into Jaz — importing from Xero,
6
6
  QuickBooks, Sage, MYOB, or Excel exports. Covers the full conversion pipeline:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-jobs
3
- version: 4.34.6
3
+ version: 4.35.0
4
4
  description: >-
5
5
  Use this skill for recurring accounting workflows — month/quarter/year-end
6
6
  close, bank reconciliation, GST/VAT filing, payment runs, credit control,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-recipes
3
- version: 4.34.6
3
+ version: 4.35.0
4
4
  description: >-
5
5
  Use this skill when modeling complex multi-step accounting transactions —
6
6
  anything that spans multiple periods, involves changing amounts, or requires
@@ -1,15 +1,23 @@
1
- import chalk from 'chalk';
2
1
  import { JazClient, JazApiError } from '../core/api/client.js';
3
2
  import { requireAuth, AuthError, resolvedProfileLabel, resolvedAuthSource, getProfile, listProfiles } from '../core/auth/index.js';
4
3
  import { isMachineFormat } from './output.js';
4
+ import { renderOrgBanner } from './ui/banner.js';
5
+ import { formatApiError, formatAuthError, formatGenericError } from './ui/error.js';
5
6
  /**
6
7
  * Shared action wrapper for all online CLI commands.
7
- * Handles auth resolution, client creation, org banner, and error formatting.
8
+ * Handles auth resolution, client creation, org banner, spinner, and error formatting.
8
9
  *
9
10
  * Exit codes: 1 = validation, 2 = API error, 3 = auth error.
10
11
  */
11
12
  export function apiAction(fn) {
12
13
  return async (opts) => {
14
+ let spinner;
15
+ // Hoist machine flag — computed once, safe to use in catch even if resolveFormat would throw
16
+ let machine = false;
17
+ try {
18
+ machine = isMachineFormat(opts);
19
+ }
20
+ catch { /* --json + --format conflict — treat as human mode for error display */ }
13
21
  try {
14
22
  // Conflict check: --api-key and --org are mutually exclusive
15
23
  if (opts.apiKey && opts.org) {
@@ -17,9 +25,8 @@ export function apiAction(fn) {
17
25
  }
18
26
  const auth = requireAuth(opts.apiKey);
19
27
  const client = new JazClient(auth);
20
- // Org banner — show which org we're hitting (suppressed in machine formats)
21
- // Visual guard: yellow warning when unpinned + multi-org, dim banner otherwise
22
- if (!isMachineFormat(opts)) {
28
+ // Org banner — stderr, suppressed in machine formats
29
+ if (!machine) {
23
30
  const label = resolvedProfileLabel();
24
31
  if (label) {
25
32
  const entry = getProfile(label);
@@ -31,43 +38,36 @@ export function apiAction(fn) {
31
38
  orgCount = Object.keys(listProfiles() ?? {}).length;
32
39
  }
33
40
  catch { /* best-effort */ }
34
- if (!isPinned && orgCount > 1) {
35
- // UNPINNED multi-org: prominent yellow warning
36
- process.stderr.write(chalk.yellow(` \u26A0 ${label} \u00B7 ${entry.orgName} (${entry.currency})`) +
37
- chalk.dim(` \u2014 not pinned to this terminal\n`) +
38
- chalk.dim(` Pin: export JAZ_ORG=${label} or --org ${label}\n`));
39
- }
40
- else {
41
- // Pinned or single-org: normal dim banner
42
- process.stderr.write(chalk.dim(` \u25B8 ${label} \u00B7 ${entry.orgName} (${entry.currency})\n`));
43
- }
41
+ renderOrgBanner({ label, orgName: entry.orgName, currency: entry.currency }, isPinned, orgCount > 1);
44
42
  }
45
43
  }
46
44
  }
47
45
  await fn(client, opts, auth);
48
46
  }
49
47
  catch (err) {
50
- const machine = isMachineFormat(opts);
51
48
  if (err instanceof AuthError) {
52
49
  if (machine)
53
- console.log(JSON.stringify({ error: { code: 'AUTH_ERROR', message: err.message } }));
50
+ console.error(JSON.stringify({ error: { code: 'AUTH_ERROR', message: err.message } }));
54
51
  else
55
- console.error(chalk.red(`Error: ${err.message}`));
52
+ console.error(formatAuthError(err.message));
56
53
  process.exit(3);
57
54
  }
58
55
  if (err instanceof JazApiError) {
59
56
  if (machine)
60
- console.log(JSON.stringify({ error: { code: 'API_ERROR', status: err.status, message: err.message } }));
57
+ console.error(JSON.stringify({ error: { code: 'API_ERROR', status: err.status, message: err.message } }));
61
58
  else
62
- console.error(chalk.red(`API Error (${err.status}): ${err.message}`));
59
+ console.error(formatApiError(err.status, err.message, err.endpoint));
63
60
  process.exit(2);
64
61
  }
65
62
  const message = err.message;
66
63
  if (machine)
67
- console.log(JSON.stringify({ error: { code: 'UNKNOWN_ERROR', message } }));
64
+ console.error(JSON.stringify({ error: { code: 'UNKNOWN_ERROR', message } }));
68
65
  else
69
- console.error(chalk.red(`Error: ${message}`));
66
+ console.error(formatGenericError(message));
70
67
  process.exit(2);
71
68
  }
69
+ finally {
70
+ spinner?.stop();
71
+ }
72
72
  };
73
73
  }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import prompts from 'prompts';
2
+ import * as p from '@clack/prompts';
3
3
  import { clearStoredCredentials, requireAuth, getProfile, setProfile, removeProfile, getActiveLabel, setActiveLabel, listProfiles, findLabelByApiKey, resolvedAuthSource, } from '../core/auth/index.js';
4
4
  import { JazClient } from '../core/api/client.js';
5
5
  import { getOrganization } from '../core/api/organization.js';
@@ -118,19 +118,21 @@ export function registerAuthCommand(program) {
118
118
  console.error(chalk.red('Error: --json and --export require a label argument.'));
119
119
  process.exit(1);
120
120
  }
121
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
122
+ console.error(chalk.red('Error: interactive picker requires a TTY. Provide a label argument.'));
123
+ process.exit(1);
124
+ }
121
125
  const active = getActiveLabel();
122
- const response = await prompts({
123
- type: 'select',
124
- name: 'label',
126
+ const selected = await p.select({
125
127
  message: 'Switch to:',
126
- choices: labels.map(l => ({
127
- title: `${l === active ? '\u2605 ' : ' '}${l} \u2014 ${orgs[l].orgName} (${orgs[l].currency})`,
128
+ options: labels.map(l => ({
129
+ label: `${l === active ? '\u2605 ' : ' '}${l} \u2014 ${orgs[l].orgName} (${orgs[l].currency})`,
128
130
  value: l,
129
131
  })),
130
132
  });
131
- if (!response.label)
133
+ if (p.isCancel(selected))
132
134
  return; // User cancelled
133
- target = response.label;
135
+ target = selected;
134
136
  }
135
137
  try {
136
138
  setActiveLabel(target);
@@ -1,35 +1,44 @@
1
- import chalk from 'chalk';
1
+ import { success, danger, warning, muted, accent } from './ui/theme.js';
2
2
  const STATUS_COLORS = {
3
- DRAFT: chalk.yellow,
4
- VOID: chalk.red,
5
- VOIDED: chalk.red,
6
- FAILED: chalk.red,
7
- DELETED: chalk.red,
3
+ DRAFT: warning,
4
+ VOID: danger,
5
+ VOIDED: danger,
6
+ FAILED: danger,
7
+ DELETED: danger,
8
8
  };
9
9
  export function formatStatus(status) {
10
10
  const s = status || 'UNKNOWN';
11
- return (STATUS_COLORS[s] ?? chalk.green)(s);
11
+ return (STATUS_COLORS[s] ?? success)(s);
12
12
  }
13
13
  // ── Shared column formatters (DRY across all command files) ─────
14
- /** Format a resource ID in cyan. */
14
+ /** Format a resource ID in accent color. */
15
15
  export function formatId(v) {
16
- return chalk.cyan(String(v));
16
+ return accent(String(v));
17
17
  }
18
18
  /** Format a reference field, showing '(no ref)' for empty values. */
19
19
  export function formatReference(v) {
20
- return String(v || '(no ref)');
20
+ return String(v || muted('(no ref)'));
21
21
  }
22
- /** Format a currency amount to 2 decimal places. */
22
+ /** Format a currency amount to 2 decimal places. Preserves decimal string precision. */
23
23
  export function formatCurrency(v) {
24
- return v != null ? `$${Number(v).toFixed(2)}` : '-';
24
+ if (v == null || v === '')
25
+ return muted('-');
26
+ const s = String(v);
27
+ // If already a valid decimal string, format without binary float conversion
28
+ if (/^-?\d+(\.\d+)?$/.test(s)) {
29
+ const [int, dec] = s.split('.');
30
+ return `$${int}.${(dec ?? '').padEnd(2, '0').slice(0, 2)}`;
31
+ }
32
+ const n = Number(v);
33
+ return Number.isNaN(n) ? muted('-') : `$${n.toFixed(2)}`;
25
34
  }
26
35
  /** Format a payment direction as colored IN/OUT. */
27
36
  export function formatDirection(v) {
28
- return String(v) === 'PAYIN' ? chalk.green('IN') : chalk.red('OUT');
37
+ return String(v) === 'PAYIN' ? success('IN') : danger('OUT');
29
38
  }
30
39
  /** Format an epoch-ms timestamp as YYYY-MM-DD. */
31
40
  export function formatEpochDate(v) {
32
41
  if (typeof v !== 'number' || v === 0 || Number.isNaN(v))
33
- return '-';
42
+ return muted('-');
34
43
  return new Date(v).toISOString().slice(0, 10);
35
44
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import prompts from 'prompts';
3
+ import * as p from '@clack/prompts';
4
4
  import { SKILL_DESCRIPTIONS } from '../types/index.js';
5
5
  import { installSkills, detectPlatform } from '../utils/template.js';
6
6
  import { logger } from '../utils/logger.js';
@@ -9,49 +9,47 @@ export async function initCommand(options) {
9
9
  let skillType = options.skill ?? 'all';
10
10
  // Prompt for skill selection if not specified
11
11
  if (!options.skill) {
12
- const response = await prompts({
13
- type: 'select',
14
- name: 'skill',
12
+ const skill = await p.select({
15
13
  message: 'Which skills do you want to install?',
16
- choices: [
14
+ options: [
17
15
  {
18
- title: `All (Recommended)`,
19
- description: 'API reference + CLI + data conversion + transaction recipes + accounting jobs',
16
+ label: 'All (Recommended)',
17
+ hint: 'API reference + CLI + data conversion + transaction recipes + accounting jobs',
20
18
  value: 'all',
21
19
  },
22
20
  {
23
- title: 'API only',
24
- description: SKILL_DESCRIPTIONS['jaz-api'],
21
+ label: 'API only',
22
+ hint: SKILL_DESCRIPTIONS['jaz-api'],
25
23
  value: 'jaz-api',
26
24
  },
27
25
  {
28
- title: 'CLI only',
29
- description: SKILL_DESCRIPTIONS['jaz-cli'],
26
+ label: 'CLI only',
27
+ hint: SKILL_DESCRIPTIONS['jaz-cli'],
30
28
  value: 'jaz-cli',
31
29
  },
32
30
  {
33
- title: 'Conversion only',
34
- description: SKILL_DESCRIPTIONS['jaz-conversion'],
31
+ label: 'Conversion only',
32
+ hint: SKILL_DESCRIPTIONS['jaz-conversion'],
35
33
  value: 'jaz-conversion',
36
34
  },
37
35
  {
38
- title: 'Transaction Recipes only',
39
- description: SKILL_DESCRIPTIONS['jaz-recipes'],
36
+ label: 'Transaction Recipes only',
37
+ hint: SKILL_DESCRIPTIONS['jaz-recipes'],
40
38
  value: 'jaz-recipes',
41
39
  },
42
40
  {
43
- title: 'Jobs only',
44
- description: SKILL_DESCRIPTIONS['jaz-jobs'],
41
+ label: 'Jobs only',
42
+ hint: SKILL_DESCRIPTIONS['jaz-jobs'],
45
43
  value: 'jaz-jobs',
46
44
  },
47
45
  ],
48
- initial: 0,
46
+ initialValue: 'all',
49
47
  });
50
- if (!response.skill) {
48
+ if (p.isCancel(skill)) {
51
49
  logger.warn('Installation cancelled');
52
50
  return;
53
51
  }
54
- skillType = response.skill;
52
+ skillType = skill;
55
53
  }
56
54
  const skillLabel = skillType === 'all'
57
55
  ? 'jaz-api + jaz-cli + jaz-conversion + jaz-recipes + jaz-jobs'
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import prompts from 'prompts';
2
+ import * as p from '@clack/prompts';
3
3
  import { readFileSync, writeFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
  import { generateMonthEndBlueprint } from '../core/jobs/month-end/blueprint.js';
@@ -93,16 +93,16 @@ async function resolveBankFormat(opts) {
93
93
  if (selected.length === 0) {
94
94
  // Interactive: prompt for format in TTY
95
95
  if (process.stdin.isTTY && !opts.json) {
96
- const { format } = await prompts({
97
- type: 'select',
98
- name: 'format',
96
+ const format = await p.select({
99
97
  message: 'Bank format',
100
- choices: [
101
- { title: 'DBS GIRO (IDEAL UFF — CSV)', value: 'dbs-giro' },
102
- { title: 'OCBC GIRO/FAST (fixed-width 1000 chars)', value: 'ocbc-giro' },
103
- { title: 'UOB GIRO (tilde-delimited UFF)', value: 'uob-giro' },
98
+ options: [
99
+ { label: 'DBS GIRO (IDEAL UFF — CSV)', value: 'dbs-giro' },
100
+ { label: 'OCBC GIRO/FAST (fixed-width 1000 chars)', value: 'ocbc-giro' },
101
+ { label: 'UOB GIRO (tilde-delimited UFF)', value: 'uob-giro' },
104
102
  ],
105
- }, { onCancel: () => process.exit(0) });
103
+ });
104
+ if (p.isCancel(format))
105
+ process.exit(0);
106
106
  return format;
107
107
  }
108
108
  throw new BankFileValidationError('No bank format specified. Pick one:\n\n' +
@@ -126,12 +126,12 @@ async function resolveBankFormat(opts) {
126
126
  * a pre-filled bank-file JSON input.
127
127
  */
128
128
  async function fetchOutstandingForBankFile(opts, format) {
129
- const { fetch: doFetch } = await prompts({
130
- type: 'confirm',
131
- name: 'fetch',
129
+ const doFetch = await p.confirm({
132
130
  message: 'No input file. Fetch outstanding bills from API?',
133
- initial: true,
134
- }, { onCancel: () => process.exit(0) });
131
+ initialValue: true,
132
+ });
133
+ if (p.isCancel(doFetch))
134
+ process.exit(0);
135
135
  if (!doFetch) {
136
136
  throw new BankFileValidationError('No input provided. Use --input <file> or pipe JSON via stdin.\n\n' +
137
137
  `Sample JSON for ${format}:\n` +
@@ -190,40 +190,44 @@ async function fetchOutstandingForBankFile(opts, format) {
190
190
  value: s.contactResourceId,
191
191
  selected: true,
192
192
  }));
193
- const { selected } = await prompts({
194
- type: 'multiselect',
195
- name: 'selected',
193
+ const selected = await p.multiselect({
196
194
  message: 'Select suppliers to pay',
197
- choices,
198
- hint: '- Space to toggle, Enter to confirm',
199
- }, { onCancel: () => process.exit(0) });
195
+ options: choices.map(c => ({
196
+ label: c.title,
197
+ value: c.value,
198
+ })),
199
+ initialValues: choices.map(c => c.value),
200
+ required: true,
201
+ });
202
+ if (p.isCancel(selected))
203
+ process.exit(0);
200
204
  if (!selected || selected.length === 0) {
201
205
  throw new BankFileValidationError('No suppliers selected.');
202
206
  }
203
207
  const selectedSet = new Set(selected);
204
208
  const selectedSuppliers = outstanding.suppliers.filter(s => selectedSet.has(s.contactResourceId));
205
209
  // Prompt for originator details
206
- const originatorAnswers = await prompts([
207
- {
208
- type: 'text',
209
- name: 'accountNumber',
210
- message: 'Originator account number',
211
- validate: (v) => v.trim().length > 0 || 'Required',
212
- },
213
- {
214
- type: 'text',
215
- name: 'accountName',
216
- message: 'Originator company name',
217
- validate: (v) => v.trim().length > 0 || 'Required',
218
- },
219
- {
220
- type: 'text',
221
- name: 'valueDate',
222
- message: 'Value date (YYYY-MM-DD)',
223
- initial: new Date().toISOString().slice(0, 10),
224
- validate: (v) => /^\d{4}-\d{2}-\d{2}$/.test(v) || 'Format: YYYY-MM-DD',
225
- },
226
- ], { onCancel: () => process.exit(0) });
210
+ const accountNumber = await p.text({
211
+ message: 'Originator account number',
212
+ validate: (v) => (!v || v.trim().length === 0) ? 'Required' : undefined,
213
+ });
214
+ if (p.isCancel(accountNumber))
215
+ process.exit(0);
216
+ const accountName = await p.text({
217
+ message: 'Originator company name',
218
+ validate: (v) => (!v || v.trim().length === 0) ? 'Required' : undefined,
219
+ });
220
+ if (p.isCancel(accountName))
221
+ process.exit(0);
222
+ const valueDate = await p.text({
223
+ message: 'Value date (YYYY-MM-DD)',
224
+ placeholder: new Date().toISOString().slice(0, 10),
225
+ defaultValue: new Date().toISOString().slice(0, 10),
226
+ validate: (v) => !/^\d{4}-\d{2}-\d{2}$/.test(v ?? '') ? 'Format: YYYY-MM-DD' : undefined,
227
+ });
228
+ if (p.isCancel(valueDate))
229
+ process.exit(0);
230
+ const originatorAnswers = { accountNumber, accountName, valueDate };
227
231
  // Build the bank-file input structure
228
232
  // NOTE: payee.accountNumber and payee.bankCode are NOT available from the API.
229
233
  // We use placeholders — user must fill them in or the generator will validate.
@@ -725,12 +729,10 @@ export function registerJobsCommand(program) {
725
729
  // Interactive mode — prompt user for each encrypted file
726
730
  console.error(chalk.yellow(`\n${needPassword.length} encrypted PDF(s) need a password:\n`));
727
731
  for (const f of needPassword) {
728
- const { password } = await prompts({
729
- type: 'text',
730
- name: 'password',
732
+ const password = await p.text({
731
733
  message: `PDF password for ${f.folder}/${f.filename}`,
732
734
  });
733
- if (!password) {
735
+ if (p.isCancel(password) || !password) {
734
736
  console.error(chalk.red('Aborted — no password provided.'));
735
737
  console.error(chalk.dim('Tip: embed password in filename to skip prompts: filename__pw__password.pdf'));
736
738
  process.exit(1);
@@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:f
3
3
  import { formatStatus } from './format-helpers.js';
4
4
  import { basename, extname, join, resolve } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
- import prompts from 'prompts';
6
+ import * as p from '@clack/prompts';
7
7
  import { createFromAttachment, searchMagicWorkflows, waitForWorkflows, } from '../core/api/magic.js';
8
8
  import { extractFilePassword, isPdfEncrypted, isQpdfAvailable, decryptPdf, cleanupDecryptedFile, } from '../core/jobs/document-collection/tools/ingest/decrypt.js';
9
9
  import { extractZipToDir, flattenSingleRoot } from '../core/jobs/document-collection/tools/ingest/cloud/zip.js';
@@ -298,17 +298,15 @@ async function resolveInputPdf(filePath, ext, opts) {
298
298
  }));
299
299
  process.exit(1);
300
300
  }
301
- const response = await prompts({
302
- type: 'text',
303
- name: 'password',
301
+ const pwInput = await p.text({
304
302
  message: `PDF password for ${rawName}`,
305
303
  });
306
- if (!response.password) {
304
+ if (p.isCancel(pwInput) || !pwInput) {
307
305
  console.error(chalk.red('Aborted — no password provided.'));
308
306
  console.error(chalk.dim('Tip: embed password in filename: name__pw__password.pdf'));
309
307
  process.exit(1);
310
308
  }
311
- password = response.password;
309
+ password = pwInput;
312
310
  }
313
311
  const decryptedPath = decryptPdf(filePath, password);
314
312
  return { effectivePath: decryptedPath, cleanName, decryptedPath };
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { stringify as yamlStringify } from 'yaml';
3
3
  import { formatTable } from './table-formatter.js';
4
4
  import { displaySlice, paginatedJson } from './pagination.js';
5
+ import { formatRecord } from './ui/record.js';
5
6
  /** Resolve the output format from CLI flags. --json is shorthand for --format json. */
6
7
  export function resolveFormat(opts) {
7
8
  if (opts.json && opts.format) {
@@ -114,7 +115,7 @@ export function outputRecord(record, opts) {
114
115
  console.log(yamlStringify(record).trimEnd());
115
116
  break;
116
117
  case 'table':
117
- console.log(JSON.stringify(record, null, 2));
118
+ console.log(formatRecord(record));
118
119
  break;
119
120
  }
120
121
  }
@@ -1,5 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { fetchAllPages } from '../core/api/pagination.js';
3
+ import { createProgress } from './ui/progress.js';
4
+ import { isMachineFormat } from './output.js';
3
5
  /** Default row cap for --all mode. Prevents runaway fetches and agent token waste. */
4
6
  const DEFAULT_MAX_ROWS = 10_000;
5
7
  /** Display cap for human (non-JSON) output. */
@@ -14,13 +16,6 @@ const DISPLAY_CAP = 500;
14
16
  * - Progress display on stderr (TTY-aware, suppressed for --json)
15
17
  * - Truncation metadata (truncated field) for agent consumption
16
18
  * - Single-page fetch when --all is not set
17
- *
18
- * Usage:
19
- * const { data, totalElements, truncated } = await paginatedFetch(
20
- * opts,
21
- * (p) => listInvoices(client, p),
22
- * { label: 'Fetching invoices' },
23
- * );
24
19
  */
25
20
  export async function paginatedFetch(opts, fetcher, options) {
26
21
  const defaultLimit = options.defaultLimit ?? 100;
@@ -31,18 +26,15 @@ export async function paginatedFetch(opts, fetcher, options) {
31
26
  // ── Auto-paginate mode ──
32
27
  if (opts.all) {
33
28
  const resolvedFormat = opts.format?.toLowerCase() ?? (opts.json ? 'json' : 'table');
34
- const showProgress = resolvedFormat === 'table' && process.stderr.isTTY;
29
+ const showProgress = resolvedFormat === 'table';
30
+ const progress = showProgress ? createProgress(options.label) : undefined;
35
31
  const result = await fetchAllPages((offset, limit) => fetcher({ limit, offset }), {
36
32
  pageSize: opts.limit ?? 200,
37
- onProgress: showProgress
38
- ? (fetched, total) => {
39
- process.stderr.write(`\r${chalk.dim(`${options.label}... ${fetched.toLocaleString()}/${total.toLocaleString()}`)}`);
40
- }
33
+ onProgress: progress
34
+ ? (fetched, total) => progress.update(fetched, total)
41
35
  : undefined,
42
36
  });
43
- if (showProgress) {
44
- process.stderr.write('\r\x1b[K'); // clear progress line
45
- }
37
+ progress?.clear();
46
38
  // Apply max-rows soft cap
47
39
  const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
48
40
  let { truncated } = result;
@@ -51,7 +43,7 @@ export async function paginatedFetch(opts, fetcher, options) {
51
43
  data = data.slice(0, maxRows);
52
44
  truncated = true;
53
45
  }
54
- if (truncated && !opts.json) {
46
+ if (truncated && !isMachineFormat(opts)) {
55
47
  console.error(chalk.yellow(`Warning: dataset has ${result.totalElements.toLocaleString()} items — showing ${data.length.toLocaleString()} (max-rows: ${maxRows.toLocaleString()})`));
56
48
  }
57
49
  const pageSize = opts.limit ?? defaultLimit;