@doow/cli 0.1.6 → 0.1.7

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.
package/dist/cli.cjs CHANGED
@@ -184,6 +184,9 @@ async function deleteProfile(name) {
184
184
  if (config.activeProfile === name) {
185
185
  throw new Error(`Cannot delete the active profile "${name}". Switch to another profile first.`);
186
186
  }
187
+ if (!config.profiles[name]) {
188
+ throw new Error(`Profile "${name}" does not exist.`);
189
+ }
187
190
  delete config.profiles[name];
188
191
  await writeConfig(config);
189
192
  await clearProfileCredentials(name);
@@ -791,6 +794,33 @@ function isLegacyPendingPollResponse(status, body) {
791
794
  legacyBody.message === 'Bad Request Exception' &&
792
795
  legacyBody.response === 'Bad Request Exception');
793
796
  }
797
+ function isThrottledPollResponse(status, body) {
798
+ const legacyBody = body;
799
+ return (status === 429 ||
800
+ legacyBody.status === 429 ||
801
+ legacyBody.statusCode === 429 ||
802
+ legacyBody.message?.toLowerCase().includes('too many requests') === true);
803
+ }
804
+ function getRetryAfterSeconds(headers) {
805
+ const headerValue = headers.get('retry-after-auth') ?? headers.get('retry-after');
806
+ if (!headerValue)
807
+ return undefined;
808
+ const seconds = Number.parseInt(headerValue, 10);
809
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : undefined;
810
+ }
811
+ function formatPollFailure(status, body) {
812
+ if (typeof body.error === 'string' && body.error.length > 0) {
813
+ return body.error_description ? `${body.error} — ${body.error_description}` : body.error;
814
+ }
815
+ const legacyBody = body;
816
+ if (typeof legacyBody.message === 'string' && legacyBody.message.length > 0) {
817
+ return legacyBody.message;
818
+ }
819
+ if (typeof legacyBody.response === 'string' && legacyBody.response.length > 0) {
820
+ return legacyBody.response;
821
+ }
822
+ return `HTTP ${status}`;
823
+ }
794
824
  // ---------------------------------------------------------------------------
795
825
  // Core function
796
826
  // ---------------------------------------------------------------------------
@@ -893,7 +923,11 @@ async function executeDeviceFlow(options = {}) {
893
923
  if (isLegacyPendingPollResponse(tokenRes.status, errBody)) {
894
924
  continue;
895
925
  }
896
- throw new Error(`Token polling failed: ${errBody.error}${errBody.error_description ? ` — ${errBody.error_description}` : ''}`);
926
+ if (isThrottledPollResponse(tokenRes.status, errBody)) {
927
+ pollInterval = getRetryAfterSeconds(tokenRes.headers) ?? Math.max(pollInterval + 5, 30);
928
+ continue;
929
+ }
930
+ throw new Error(`Token polling failed: ${formatPollFailure(tokenRes.status, errBody)}`);
897
931
  }
898
932
  }
899
933
  }
@@ -1562,7 +1596,11 @@ function getValue(row, key) {
1562
1596
  function truncate(s, max) {
1563
1597
  if (s.length <= max)
1564
1598
  return s;
1565
- return s.slice(0, max - 1) + '…'; // …
1599
+ if (max <= 0)
1600
+ return '';
1601
+ if (max <= 3)
1602
+ return '.'.repeat(max);
1603
+ return s.slice(0, max - 3) + '...';
1566
1604
  }
1567
1605
  /**
1568
1606
  * Calculate the display width for a column, taking into account the header
@@ -1595,9 +1633,9 @@ function printTable(rows, columns) {
1595
1633
  })
1596
1634
  .join(' ');
1597
1635
  process.stderr.write(header + '\n');
1598
- // Separator ( chars)
1636
+ // Separator (ASCII-only hyphen chars)
1599
1637
  const totalWidth = widths.reduce((sum, w) => sum + w, 0) + (columns.length - 1) * 2;
1600
- const separator = ''.repeat(Math.min(totalWidth, termWidth));
1638
+ const separator = '-'.repeat(Math.min(totalWidth, termWidth));
1601
1639
  process.stderr.write(separator + '\n');
1602
1640
  // Data rows
1603
1641
  for (const row of rows) {
@@ -2858,9 +2896,15 @@ function registerAppsCommands(program) {
2858
2896
  .command('add-manager <appId> <memberId>')
2859
2897
  .description('Add a manager to an application')
2860
2898
  .action(async (appId, memberId) => {
2861
- const format = resolveFormat(program.opts());
2899
+ const globalOpts = program.opts();
2900
+ const format = resolveFormat(globalOpts);
2862
2901
  try {
2863
- const data = await gqlFetch(ADD_APP_MANAGER, { input: { application_id: appId, member_id: memberId } }, clientOpts$4(program));
2902
+ const variables = { input: { application_id: appId, member_id: memberId } };
2903
+ if (isDryRun(globalOpts)) {
2904
+ printDryRun({ operation: 'mutation', name: 'UpdateAppManager', variables });
2905
+ return;
2906
+ }
2907
+ const data = await gqlFetch(ADD_APP_MANAGER, variables, clientOpts$4(program));
2864
2908
  const result = data.updateAppManager
2865
2909
  ?? data.addAppManager
2866
2910
  ?? { success: false, message: 'No response', appManagerId: null };
@@ -4208,13 +4252,23 @@ function registerLicensesCommands(program) {
4208
4252
  .command('assign <licenseId> <memberId>')
4209
4253
  .description('Assign a license to a user')
4210
4254
  .action(async (licenseId, memberId) => {
4211
- const format = resolveFormat(program.opts());
4255
+ const globalOpts = program.opts();
4256
+ const format = resolveFormat(globalOpts);
4212
4257
  try {
4213
- const data = await gqlFetch(ASSIGN_LICENSE, {
4258
+ const variables = {
4214
4259
  input: { license_id: licenseId, user_id: memberId },
4215
4260
  licenseId,
4216
4261
  memberId,
4217
- }, clientOpts$2(program));
4262
+ };
4263
+ if (isDryRun(globalOpts)) {
4264
+ printDryRun({
4265
+ operation: 'mutation',
4266
+ name: 'AddUserToLicense',
4267
+ variables: { input: variables.input },
4268
+ });
4269
+ return;
4270
+ }
4271
+ const data = await gqlFetch(ASSIGN_LICENSE, variables, clientOpts$2(program));
4218
4272
  const legacy = data;
4219
4273
  const license = normalizeLicense(legacy.addUserToLicense?.data ?? legacy.assignLicense);
4220
4274
  if (format === 'json') {
@@ -4918,7 +4972,7 @@ async function cardsListHandler(opts, override) {
4918
4972
  { header: 'APPS', key: 'applications', width: 24 },
4919
4973
  ]);
4920
4974
  if (payload.cursor) {
4921
- process.stderr.write(`\n more results available (cursor: ${payload.cursor})\n`);
4975
+ process.stderr.write(`\n ... more results available (cursor: ${payload.cursor})\n`);
4922
4976
  }
4923
4977
  }
4924
4978
  async function cardGetHandler(id, opts, override) {
@@ -4991,7 +5045,7 @@ async function cardTransactionsHandler(id, opts, override) {
4991
5045
  { header: 'DATE', key: 'date', width: 24 },
4992
5046
  ]);
4993
5047
  if (payload.cursor !== undefined && payload.cursor !== null) {
4994
- process.stderr.write(`\n more results available (cursor: ${payload.cursor})\n`);
5048
+ process.stderr.write(`\n ... more results available (cursor: ${payload.cursor})\n`);
4995
5049
  }
4996
5050
  }
4997
5051
  async function cardsCreateHandler(opts, override) {
@@ -7030,10 +7084,10 @@ function printMoreResultsHint$2(nextPageAvailable, cursor) {
7030
7084
  if (!nextPageAvailable)
7031
7085
  return;
7032
7086
  if (cursor) {
7033
- process.stderr.write(`\n more results available (cursor: ${cursor})\n`);
7087
+ process.stderr.write(`\n ... more results available (cursor: ${cursor})\n`);
7034
7088
  return;
7035
7089
  }
7036
- process.stderr.write('\n more results available\n');
7090
+ process.stderr.write('\n ... more results available\n');
7037
7091
  }
7038
7092
  function getConnectResultSummary(result) {
7039
7093
  const name = result.integration.integration?.name ?? '(unknown)';
@@ -7326,6 +7380,55 @@ function registerIntegrationsCommands(program) {
7326
7380
  });
7327
7381
  }
7328
7382
 
7383
+ /**
7384
+ * ASCII-only bar chart renderer for human-readable terminal summaries.
7385
+ *
7386
+ * All output goes to stderr so stdout stays clean for JSON / piping.
7387
+ */
7388
+ function truncateAscii(s, max) {
7389
+ if (s.length <= max)
7390
+ return s;
7391
+ if (max <= 0)
7392
+ return '';
7393
+ if (max <= 3)
7394
+ return '.'.repeat(max);
7395
+ return s.slice(0, max - 3) + '...';
7396
+ }
7397
+ function resolveBarWidth(rows, options, labelWidth) {
7398
+ if (options.barWidth !== undefined)
7399
+ return Math.max(8, options.barWidth);
7400
+ const termWidth = process.stderr.columns ?? 120;
7401
+ const suffixWidth = rows.reduce((max, row) => {
7402
+ const valueText = options.valueFormatter
7403
+ ? options.valueFormatter(row.value, row)
7404
+ : String(row.value);
7405
+ const detailText = row.detail ? ` ${row.detail}` : '';
7406
+ return Math.max(max, (valueText + detailText).length);
7407
+ }, 0);
7408
+ return Math.max(8, Math.min(40, termWidth - labelWidth - suffixWidth - 5));
7409
+ }
7410
+ function printBarChart(rows, options = {}) {
7411
+ if (rows.length === 0)
7412
+ return;
7413
+ const widestLabel = rows.reduce((max, row) => Math.max(max, row.label.length), 0);
7414
+ const labelWidth = options.labelWidth ??
7415
+ Math.min(options.maxLabelWidth ?? 24, widestLabel);
7416
+ const barWidth = resolveBarWidth(rows, options, labelWidth);
7417
+ const maxValue = rows.reduce((max, row) => Math.max(max, row.value), 0);
7418
+ for (const row of rows) {
7419
+ const label = truncateAscii(row.label, labelWidth).padEnd(labelWidth);
7420
+ const valueText = options.valueFormatter
7421
+ ? options.valueFormatter(row.value, row)
7422
+ : String(row.value);
7423
+ const detailText = row.detail ? ` ${row.detail}` : '';
7424
+ const barLength = row.value <= 0 || maxValue <= 0
7425
+ ? 0
7426
+ : Math.max(1, Math.round((row.value / maxValue) * barWidth));
7427
+ const bar = '#'.repeat(barLength).padEnd(barWidth, ' ');
7428
+ process.stderr.write(`${label} | ${bar} ${valueText}${detailText}\n`);
7429
+ }
7430
+ }
7431
+
7329
7432
  /**
7330
7433
  * gql/operations/insights.ts
7331
7434
  *
@@ -7438,7 +7541,7 @@ function isRead(notification) {
7438
7541
  return notification.status !== 'UNREAD' || notification.read_at !== null;
7439
7542
  }
7440
7543
  async function insightsListHandler(opts) {
7441
- const format = resolveFormat(opts);
7544
+ const format = opts.chart && !opts.json ? 'table' : resolveFormat(opts);
7442
7545
  const data = await gqlFetch(GET_NOTIFICATION_STATS, undefined, buildClientOpts(opts));
7443
7546
  const stats = data.getAppNotificationStats;
7444
7547
  if (format === 'json') {
@@ -7453,6 +7556,16 @@ async function insightsListHandler(opts) {
7453
7556
  ['Actionable', String(stats.actionable)],
7454
7557
  ['Persistent', String(stats.persistent)],
7455
7558
  ]);
7559
+ if (opts.chart) {
7560
+ process.stderr.write('\n--- Notification Mix (ASCII Chart) ---\n');
7561
+ printBarChart([
7562
+ { label: 'Unread', value: stats.unread },
7563
+ { label: 'Read', value: stats.read },
7564
+ { label: 'Dismissed', value: stats.dismissed },
7565
+ { label: 'Actionable', value: stats.actionable },
7566
+ { label: 'Persistent', value: stats.persistent },
7567
+ ]);
7568
+ }
7456
7569
  }
7457
7570
  async function needsAttentionHandler(opts) {
7458
7571
  const format = resolveFormat(opts);
@@ -7500,7 +7613,7 @@ async function notificationsListHandler(opts) {
7500
7613
  { header: 'CREATED', key: 'created_at', width: 20 },
7501
7614
  ]);
7502
7615
  if (page < total_pages) {
7503
- process.stderr.write(`\n more results available (page ${page} of ${total_pages})\n`);
7616
+ process.stderr.write(`\n ... more results available (page ${page} of ${total_pages})\n`);
7504
7617
  }
7505
7618
  }
7506
7619
  async function notificationsReadHandler(id, opts) {
@@ -7569,10 +7682,11 @@ function registerInsightsCommands(program) {
7569
7682
  .command('stats')
7570
7683
  .alias('list')
7571
7684
  .description('Show notification summary stats')
7572
- .action(async () => {
7685
+ .option('--chart', 'Render ASCII charts in human output')
7686
+ .action(async (opts) => {
7573
7687
  const globalOpts = program.opts();
7574
7688
  try {
7575
- await insightsListHandler(globalOpts);
7689
+ await insightsListHandler({ ...globalOpts, ...opts });
7576
7690
  }
7577
7691
  catch (err) {
7578
7692
  const msg = err instanceof Error ? err.message : String(err);
@@ -8179,7 +8293,7 @@ async function uploadFile(options) {
8179
8293
  const fileBuffer = fs$1.readFileSync(options.filePath);
8180
8294
  const fileName = node_path.basename(options.filePath);
8181
8295
  const formData = new FormData();
8182
- formData.append('file', new Blob([fileBuffer], { type: 'application/octet-stream' }), fileName);
8296
+ formData.append('files', new Blob([fileBuffer], { type: 'application/octet-stream' }), fileName);
8183
8297
  if (options.chatId !== undefined) {
8184
8298
  formData.append('chat_id', options.chatId);
8185
8299
  }
@@ -8824,8 +8938,18 @@ function formatAmount(value) {
8824
8938
  function formatPercent(value) {
8825
8939
  return `${value.toFixed(1)}%`;
8826
8940
  }
8941
+ function shouldRenderChart(opts, format) {
8942
+ return opts.chart === true && format !== 'json';
8943
+ }
8944
+ function formatDelta(changeAmount, changePercentage, valueFormatter = (value) => String(value)) {
8945
+ if (changeAmount === undefined || changePercentage === undefined)
8946
+ return undefined;
8947
+ const amountPrefix = changeAmount > 0 ? '+' : '';
8948
+ const percentPrefix = changePercentage > 0 ? '+' : '';
8949
+ return `${amountPrefix}${valueFormatter(changeAmount)} (${percentPrefix}${changePercentage.toFixed(1)}%)`;
8950
+ }
8827
8951
  async function dashboardOverviewHandler(opts) {
8828
- const format = resolveFormat(opts);
8952
+ const format = opts.chart && !opts.json ? 'table' : resolveFormat(opts);
8829
8953
  const clientOpts = buildClientOptions(opts);
8830
8954
  const period = parseDashboardPeriod(opts.period);
8831
8955
  const [spendData, topAppsData, renewalsData] = await Promise.all([
@@ -8849,6 +8973,16 @@ async function dashboardOverviewHandler(opts) {
8849
8973
  ['Period', overview.period],
8850
8974
  ['Total spend', formatAmount(overview.spend.totalSpend)],
8851
8975
  ]);
8976
+ if (shouldRenderChart(opts, format) && overview.top_apps.length > 0) {
8977
+ process.stderr.write('\n--- Top Applications (ASCII Chart) ---\n');
8978
+ printBarChart(overview.top_apps.map((app) => ({
8979
+ label: app.name,
8980
+ value: app.totalCost,
8981
+ detail: `${app.userCount} users`,
8982
+ })), {
8983
+ valueFormatter: (value) => formatAmount(value),
8984
+ });
8985
+ }
8852
8986
  if (overview.top_apps.length > 0) {
8853
8987
  process.stderr.write('\n--- Top Applications ---\n');
8854
8988
  printTable(overview.top_apps, [
@@ -8866,6 +9000,16 @@ async function dashboardOverviewHandler(opts) {
8866
9000
  },
8867
9001
  ]);
8868
9002
  }
9003
+ if (shouldRenderChart(opts, format) && overview.upcoming_renewals.length > 0) {
9004
+ process.stderr.write('\n--- Upcoming Renewals (ASCII Chart) ---\n');
9005
+ printBarChart(overview.upcoming_renewals.map((renewal) => ({
9006
+ label: renewal.appName,
9007
+ value: renewal.amount,
9008
+ detail: `${renewal.daysUntilRenewal}d ${renewal.renewalDate}`,
9009
+ })), {
9010
+ valueFormatter: (value) => formatAmount(value),
9011
+ });
9012
+ }
8869
9013
  if (overview.upcoming_renewals.length > 0) {
8870
9014
  process.stderr.write('\n--- Upcoming Renewals (30 days) ---\n');
8871
9015
  printTable(overview.upcoming_renewals, [
@@ -8886,7 +9030,7 @@ async function dashboardOverviewHandler(opts) {
8886
9030
  }
8887
9031
  }
8888
9032
  async function dashboardSpendHandler(opts) {
8889
- const format = resolveFormat(opts);
9033
+ const format = opts.chart && !opts.json ? 'table' : resolveFormat(opts);
8890
9034
  const clientOpts = buildClientOptions(opts);
8891
9035
  const period = parseDashboardPeriod(opts.period);
8892
9036
  const [overviewData, categoryData] = await Promise.all([
@@ -8908,6 +9052,16 @@ async function dashboardSpendHandler(opts) {
8908
9052
  ['Total spend', formatAmount(spend.overview.totalSpend)],
8909
9053
  ['Category total', formatAmount(spend.category_total)],
8910
9054
  ]);
9055
+ if (shouldRenderChart(opts, format) && spend.by_category.length > 0) {
9056
+ process.stderr.write('\n--- Spend by Category (ASCII Chart) ---\n');
9057
+ printBarChart(spend.by_category.map((category) => ({
9058
+ label: category.name,
9059
+ value: category.amount,
9060
+ detail: formatPercent(category.percentage),
9061
+ })), {
9062
+ valueFormatter: (value) => formatAmount(value),
9063
+ });
9064
+ }
8911
9065
  if (spend.by_category.length > 0) {
8912
9066
  process.stderr.write('\n--- Spend by Category ---\n');
8913
9067
  printTable(spend.by_category, [
@@ -8926,7 +9080,7 @@ async function dashboardSpendHandler(opts) {
8926
9080
  }
8927
9081
  }
8928
9082
  async function dashboardRenewalsHandler(opts) {
8929
- const format = resolveFormat(opts);
9083
+ const format = opts.chart && !opts.json ? 'table' : resolveFormat(opts);
8930
9084
  const clientOpts = buildClientOptions(opts);
8931
9085
  const days = opts.days ? Number.parseInt(opts.days, 10) : undefined;
8932
9086
  const data = await gqlFetch(GET_UPCOMING_RENEWALS, days !== undefined ? { days } : undefined, clientOpts);
@@ -8939,6 +9093,17 @@ async function dashboardRenewalsHandler(opts) {
8939
9093
  process.stderr.write('No upcoming renewals found.\n');
8940
9094
  return;
8941
9095
  }
9096
+ if (shouldRenderChart(opts, format)) {
9097
+ process.stderr.write('\n--- Upcoming Renewals (ASCII Chart) ---\n');
9098
+ printBarChart(renewals.map((renewal) => ({
9099
+ label: renewal.appName,
9100
+ value: renewal.amount,
9101
+ detail: `${renewal.daysUntilRenewal}d ${renewal.renewalDate}`,
9102
+ })), {
9103
+ valueFormatter: (value) => formatAmount(value),
9104
+ });
9105
+ process.stderr.write('\n');
9106
+ }
8942
9107
  printTable(renewals, [
8943
9108
  { header: 'APP', key: 'appName', width: 24 },
8944
9109
  {
@@ -8957,7 +9122,7 @@ async function dashboardRenewalsHandler(opts) {
8957
9122
  ]);
8958
9123
  }
8959
9124
  async function dashboardMetricsHandler(opts) {
8960
- const format = resolveFormat(opts);
9125
+ const format = opts.chart && !opts.json ? 'table' : resolveFormat(opts);
8961
9126
  const clientOpts = buildClientOptions(opts);
8962
9127
  const period = parseDashboardPeriod(opts.period);
8963
9128
  const data = await gqlFetch(GET_DASHBOARD_METRICS, { input: buildDashboardMetricsInput(opts.period) }, clientOpts);
@@ -8970,29 +9135,91 @@ async function dashboardMetricsHandler(opts) {
8970
9135
  const applicationMetrics = metrics.applicationMetrics;
8971
9136
  const userMetrics = metrics.userMetrics;
8972
9137
  const subscriptionMetrics = metrics.subscriptionMetrics;
8973
- printKeyValue([
8974
- ['Period', period.label],
8975
- ['Filter', metrics.filter],
8976
- ['Paid apps', String(allMetrics?.paidApplications.current ?? 0)],
8977
- ['Free apps', String(allMetrics?.freeApplications.current ?? 0)],
8978
- ['Active apps', String(applicationMetrics?.activeApplications.current ?? 0)],
8979
- ['Inactive apps', String(applicationMetrics?.inactiveApplications.current ?? 0)],
8980
- ['Users', String(allMetrics?.totalUsers.current ?? userMetrics?.activeUsers.current ?? 0)],
8981
- ['Active subscriptions', String(subscriptionMetrics?.activeSubscriptions.currentCount ?? 0)],
8982
- ['Active licenses', String(subscriptionMetrics?.activeLicenses.currentCount ?? 0)],
8983
- [
8984
- 'Total spend',
8985
- formatAmount(allMetrics?.totalSubscriptionSpend.current ??
8986
- subscriptionMetrics?.totalSubscriptionValue.currentValue ??
8987
- 0),
8988
- ],
8989
- ]);
9138
+ const rows = [['Period', period.label], ['Filter', metrics.filter]];
9139
+ const paidApps = allMetrics?.paidApplications.current;
9140
+ if (paidApps !== undefined)
9141
+ rows.push(['Paid apps', String(paidApps)]);
9142
+ const freeApps = allMetrics?.freeApplications.current;
9143
+ if (freeApps !== undefined)
9144
+ rows.push(['Free apps', String(freeApps)]);
9145
+ const activeApps = applicationMetrics?.activeApplications.current;
9146
+ if (activeApps !== undefined)
9147
+ rows.push(['Active apps', String(activeApps)]);
9148
+ const inactiveApps = applicationMetrics?.inactiveApplications.current;
9149
+ if (inactiveApps !== undefined)
9150
+ rows.push(['Inactive apps', String(inactiveApps)]);
9151
+ const users = allMetrics?.totalUsers.current ?? userMetrics?.activeUsers.current;
9152
+ if (users !== undefined)
9153
+ rows.push(['Users', String(users)]);
9154
+ const activeSubscriptions = subscriptionMetrics?.activeSubscriptions.currentCount;
9155
+ if (activeSubscriptions !== undefined) {
9156
+ rows.push(['Active subscriptions', String(activeSubscriptions)]);
9157
+ }
9158
+ const activeLicenses = subscriptionMetrics?.activeLicenses.currentCount;
9159
+ if (activeLicenses !== undefined) {
9160
+ rows.push(['Active licenses', String(activeLicenses)]);
9161
+ }
9162
+ const totalSpend = allMetrics?.totalSubscriptionSpend.current ??
9163
+ subscriptionMetrics?.totalSubscriptionValue.currentValue;
9164
+ if (totalSpend !== undefined)
9165
+ rows.push(['Total spend', formatAmount(totalSpend)]);
9166
+ printKeyValue(rows);
9167
+ if (shouldRenderChart(opts, format)) {
9168
+ const metricRows = [];
9169
+ if (paidApps !== undefined) {
9170
+ metricRows.push({
9171
+ label: 'Paid apps',
9172
+ value: paidApps,
9173
+ detail: formatDelta(allMetrics?.paidApplications.changeAmount, allMetrics?.paidApplications.changePercentage),
9174
+ });
9175
+ }
9176
+ if (freeApps !== undefined) {
9177
+ metricRows.push({
9178
+ label: 'Free apps',
9179
+ value: freeApps,
9180
+ detail: formatDelta(allMetrics?.freeApplications.changeAmount, allMetrics?.freeApplications.changePercentage),
9181
+ });
9182
+ }
9183
+ if (activeApps !== undefined) {
9184
+ metricRows.push({
9185
+ label: 'Active apps',
9186
+ value: activeApps,
9187
+ detail: formatDelta(applicationMetrics?.activeApplications.changeAmount, applicationMetrics?.activeApplications.changePercentage),
9188
+ });
9189
+ }
9190
+ if (users !== undefined) {
9191
+ metricRows.push({
9192
+ label: 'Users',
9193
+ value: users,
9194
+ detail: formatDelta(allMetrics?.totalUsers.changeAmount ?? userMetrics?.activeUsers.changeAmount, allMetrics?.totalUsers.changePercentage ?? userMetrics?.activeUsers.changePercentage),
9195
+ });
9196
+ }
9197
+ if (activeSubscriptions !== undefined) {
9198
+ metricRows.push({
9199
+ label: 'Active subscriptions',
9200
+ value: activeSubscriptions,
9201
+ detail: formatDelta(subscriptionMetrics?.activeSubscriptions.changeAmount, subscriptionMetrics?.activeSubscriptions.changePercentage),
9202
+ });
9203
+ }
9204
+ if (activeLicenses !== undefined) {
9205
+ metricRows.push({
9206
+ label: 'Active licenses',
9207
+ value: activeLicenses,
9208
+ detail: formatDelta(subscriptionMetrics?.activeLicenses.changeAmount, subscriptionMetrics?.activeLicenses.changePercentage),
9209
+ });
9210
+ }
9211
+ if (metricRows.length > 0) {
9212
+ process.stderr.write('\n--- Metrics (ASCII Chart) ---\n');
9213
+ printBarChart(metricRows);
9214
+ }
9215
+ }
8990
9216
  }
8991
9217
  function registerDashboardCommands(program) {
8992
9218
  const dashboard = program
8993
9219
  .command('dashboard')
8994
9220
  .description('View dashboard overview and key metrics')
8995
9221
  .option('--period <period>', 'Time period (e.g. 7d, 30d, 90d, 12m, all)')
9222
+ .option('--chart', 'Render ASCII charts in human output')
8996
9223
  .action(async (opts) => {
8997
9224
  const globalOpts = program.opts();
8998
9225
  try {
@@ -9013,6 +9240,7 @@ function registerDashboardCommands(program) {
9013
9240
  .command('overview')
9014
9241
  .description('View dashboard overview')
9015
9242
  .option('--period <period>', 'Time period (e.g. 7d, 30d, 90d, 12m, all)')
9243
+ .option('--chart', 'Render ASCII charts in human output')
9016
9244
  .action(async (opts) => {
9017
9245
  const globalOpts = program.opts();
9018
9246
  try {
@@ -9033,6 +9261,7 @@ function registerDashboardCommands(program) {
9033
9261
  .command('spend')
9034
9262
  .description('View spend breakdown')
9035
9263
  .option('--period <period>', 'Time period (e.g. 7d, 30d, 90d, 12m, all)')
9264
+ .option('--chart', 'Render ASCII charts in human output')
9036
9265
  .action(async (opts) => {
9037
9266
  const globalOpts = program.opts();
9038
9267
  try {
@@ -9053,6 +9282,7 @@ function registerDashboardCommands(program) {
9053
9282
  .command('renewals')
9054
9283
  .description('View upcoming renewals')
9055
9284
  .option('--days <n>', 'Number of days ahead to look (default: 30)')
9285
+ .option('--chart', 'Render ASCII charts in human output')
9056
9286
  .action(async (opts) => {
9057
9287
  const globalOpts = program.opts();
9058
9288
  try {
@@ -9073,6 +9303,7 @@ function registerDashboardCommands(program) {
9073
9303
  .command('metrics')
9074
9304
  .description('View key dashboard metrics')
9075
9305
  .option('--period <period>', 'Time period (e.g. 7d, 30d, 90d, 12m, all)')
9306
+ .option('--chart', 'Render ASCII charts in human output')
9076
9307
  .action(async (opts) => {
9077
9308
  const globalOpts = program.opts();
9078
9309
  try {
@@ -9496,10 +9727,10 @@ function printMoreResultsHint$1(pagination) {
9496
9727
  if (!pagination.has_more)
9497
9728
  return;
9498
9729
  if (pagination.cursor) {
9499
- process.stderr.write(`\n more results available (cursor: ${pagination.cursor})\n`);
9730
+ process.stderr.write(`\n ... more results available (cursor: ${pagination.cursor})\n`);
9500
9731
  return;
9501
9732
  }
9502
- process.stderr.write('\n more results available\n');
9733
+ process.stderr.write('\n ... more results available\n');
9503
9734
  }
9504
9735
  async function expensesListHandler(opts) {
9505
9736
  const format = resolveFormat(opts);
@@ -9960,10 +10191,10 @@ function printMoreResultsHint(pagination) {
9960
10191
  if (!pagination.has_more)
9961
10192
  return;
9962
10193
  if (pagination.cursor) {
9963
- process.stderr.write(`\n more results available (cursor: ${pagination.cursor})\n`);
10194
+ process.stderr.write(`\n ... more results available (cursor: ${pagination.cursor})\n`);
9964
10195
  return;
9965
10196
  }
9966
- process.stderr.write('\n more results available\n');
10197
+ process.stderr.write('\n ... more results available\n');
9967
10198
  }
9968
10199
  async function overagesListHandler(opts) {
9969
10200
  const format = resolveFormat(opts);
@@ -10467,6 +10698,56 @@ function decodeReportBuffer$1(data, reportFormat) {
10467
10698
  ? Buffer.from(data, 'base64')
10468
10699
  : Buffer.from(data, 'utf-8');
10469
10700
  }
10701
+ const DEFAULT_EXPENSE_REPORT_COLUMNS = [
10702
+ 'Total',
10703
+ 'Month',
10704
+ 'Year',
10705
+ 'Date',
10706
+ 'App ID',
10707
+ 'Transaction ID',
10708
+ 'Source',
10709
+ 'Payment Channel',
10710
+ 'Transaction Status',
10711
+ 'Vendor',
10712
+ 'App Name',
10713
+ 'Description',
10714
+ ];
10715
+ const EXPENSE_REPORT_COLUMN_LOOKUP = new Map(DEFAULT_EXPENSE_REPORT_COLUMNS.map((column) => [normalizeExpenseReportColumnToken(column), column]));
10716
+ function normalizeExpenseReportColumnToken(value) {
10717
+ return value.trim().toLowerCase().replace(/[\s_-]+/g, '');
10718
+ }
10719
+ function dedupe(values) {
10720
+ return Array.from(new Set(values));
10721
+ }
10722
+ function resolveExpenseReportColumns(raw) {
10723
+ if (Array.isArray(raw)) {
10724
+ return raw.length > 0 ? dedupe(raw) : [...DEFAULT_EXPENSE_REPORT_COLUMNS];
10725
+ }
10726
+ if (!raw || raw.trim().length === 0) {
10727
+ return [...DEFAULT_EXPENSE_REPORT_COLUMNS];
10728
+ }
10729
+ const requested = raw
10730
+ .split(',')
10731
+ .map((value) => value.trim())
10732
+ .filter(Boolean);
10733
+ if (requested.length === 0) {
10734
+ return [...DEFAULT_EXPENSE_REPORT_COLUMNS];
10735
+ }
10736
+ const resolved = [];
10737
+ const unknown = [];
10738
+ for (const token of requested) {
10739
+ const normalized = EXPENSE_REPORT_COLUMN_LOOKUP.get(normalizeExpenseReportColumnToken(token));
10740
+ if (!normalized) {
10741
+ unknown.push(token);
10742
+ continue;
10743
+ }
10744
+ resolved.push(normalized);
10745
+ }
10746
+ if (unknown.length > 0) {
10747
+ throw new Error(`Unknown expense report columns: ${unknown.join(', ')}. Allowed columns: ${DEFAULT_EXPENSE_REPORT_COLUMNS.join(', ')}`);
10748
+ }
10749
+ return dedupe(resolved);
10750
+ }
10470
10751
  function buildExpenseReportInput(filters) {
10471
10752
  const input = {};
10472
10753
  if (filters.from)
@@ -10487,6 +10768,7 @@ function buildExpenseReportInput(filters) {
10487
10768
  input['amount_min'] = filters.amountMin;
10488
10769
  if (filters.amountMax !== undefined)
10489
10770
  input['amount_max'] = filters.amountMax;
10771
+ input['columns'] = resolveExpenseReportColumns(filters.columns);
10490
10772
  return input;
10491
10773
  }
10492
10774
  async function expenseReportHandler(opts) {
@@ -10609,6 +10891,7 @@ function registerReportsCommands(program) {
10609
10891
  .option('--source <source>', 'Filter by source')
10610
10892
  .option('--amount-min <number>', 'Minimum amount filter', parseFloat)
10611
10893
  .option('--amount-max <number>', 'Maximum amount filter', parseFloat)
10894
+ .option('--columns <items>', 'Comma-separated columns (default: Total,Month,Year,Date,App ID,Transaction ID,Source,Payment Channel,Transaction Status,Vendor,App Name,Description)')
10612
10895
  .option('--output <path>', 'Write report to file (required for PDF when not piped)')
10613
10896
  .option('--email <addresses>', 'Comma-separated email addresses — send report by email instead of downloading')
10614
10897
  .action(async (opts) => {
@@ -11705,38 +11988,63 @@ const GLOBAL_FLAGS = [
11705
11988
  '--api-url',
11706
11989
  '--debug',
11707
11990
  ];
11708
- const SUBCOMMAND_TREE = {
11709
- login: ['--device', '--api-key', '--token-stdin'],
11710
- logout: [],
11711
- whoami: [],
11712
- profiles: ['list', 'switch', 'delete'],
11713
- apps: ['list', 'get', 'create', 'update', 'deactivate', 'add-manager', 'compare', 'metrics', 'insights', 'alternatives'],
11714
- contracts: ['list', 'get', 'create', 'update', 'delete', 'metrics', 'renewals'],
11715
- licenses: ['list', 'get', 'create', 'update', 'delete', 'assign', 'spend-pool', 'vintage', 'metrics', 'usage'],
11716
- cards: ['list', 'get', 'create', 'fund', 'withdraw', 'topup', 'freeze', 'unfreeze', 'assign', 'map-app', 'transactions'],
11717
- team: ['list', 'get', 'invite', 'update', 'deactivate', 'reactivate', 'departments'],
11718
- integrations: ['list', 'get', 'connect', 'disconnect', 'logs'],
11719
- insights: ['list', 'needs-attention', 'notifications'],
11720
- dashboard: ['overview', 'spend', 'renewals', 'metrics'],
11721
- search: [],
11722
- expenses: ['list', 'get', 'create', 'update'],
11723
- overages: ['list', 'get', 'create', 'update', 'delete'],
11724
- 'usage-tuples': ['link-binding'],
11725
- reports: ['expense', 'contracts', 'licenses', 'cards', 'departments', 'department-users'],
11726
- chat: ['send', 'sessions', 'upload'],
11727
- mcp: ['--list-tools', '--transport'],
11728
- doctor: [],
11729
- completion: ['bash', 'zsh', 'install'],
11730
- };
11731
- const TOP_LEVEL_SUBCOMMANDS = Object.keys(SUBCOMMAND_TREE);
11991
+ function unique(values) {
11992
+ return Array.from(new Set(values));
11993
+ }
11994
+ function visibleSubcommands(command) {
11995
+ return command.commands.filter((child) => child.name() !== 'help');
11996
+ }
11997
+ function commandNames(command) {
11998
+ return unique([command.name(), ...command.aliases()]);
11999
+ }
12000
+ function commandChildren(command) {
12001
+ const subcommands = visibleSubcommands(command);
12002
+ if (subcommands.length > 0) {
12003
+ return unique(subcommands.flatMap((child) => commandNames(child)));
12004
+ }
12005
+ return unique(command.options
12006
+ .map((option) => option.long)
12007
+ .filter((flag) => Boolean(flag) && flag !== '--help'));
12008
+ }
12009
+ function buildCompletionGraph(program) {
12010
+ const pathChildren = {};
12011
+ const topLevelCommands = unique(visibleSubcommands(program).flatMap((command) => commandNames(command)));
12012
+ function visit(command, pathVariants) {
12013
+ const children = commandChildren(command);
12014
+ for (const path of pathVariants) {
12015
+ pathChildren[path.join(' ')] = children;
12016
+ }
12017
+ for (const child of visibleSubcommands(command)) {
12018
+ const childVariants = pathVariants.flatMap((path) => commandNames(child).map((name) => [...path, name]));
12019
+ visit(child, childVariants);
12020
+ }
12021
+ }
12022
+ for (const command of visibleSubcommands(program)) {
12023
+ const variants = commandNames(command).map((name) => [name]);
12024
+ visit(command, variants);
12025
+ }
12026
+ const commandPaths = Object.keys(pathChildren).sort((a, b) => {
12027
+ const depth = a.split(' ').length - b.split(' ').length;
12028
+ return depth !== 0 ? depth : a.localeCompare(b);
12029
+ });
12030
+ return { commandPaths, pathChildren, topLevelCommands };
12031
+ }
12032
+ function shellSingleQuote(value) {
12033
+ return `'${value.replace(/'/g, `'\\''`)}'`;
12034
+ }
11732
12035
  // ---------------------------------------------------------------------------
11733
12036
  // Bash completion script generator
11734
12037
  // ---------------------------------------------------------------------------
11735
- function completionBashHandler() {
11736
- const subcommandCases = Object.entries(SUBCOMMAND_TREE)
11737
- .map(([cmd, children]) => {
11738
- const words = children.join(' ');
11739
- return ` ${cmd}) COMPREPLY=( $(compgen -W "${words}" -- "$cur") ) ; return ;;`;
12038
+ function completionBashHandler(program) {
12039
+ const graph = buildCompletionGraph(program);
12040
+ const knownPaths = graph.commandPaths.map((path) => ` ${shellSingleQuote(path)}`).join('\n');
12041
+ const subcommandCases = graph.commandPaths
12042
+ .map((path) => {
12043
+ const words = unique([
12044
+ ...(graph.pathChildren[path] ?? []),
12045
+ ...GLOBAL_FLAGS,
12046
+ ]).join(' ');
12047
+ return ` ${shellSingleQuote(path)}) COMPREPLY=( $(compgen -W "${words}" -- "$cur") ) ; return ;;`;
11740
12048
  })
11741
12049
  .join('\n');
11742
12050
  const script = `# doow bash completion
@@ -11752,10 +12060,13 @@ _doow_completions() {
11752
12060
  }
11753
12061
 
11754
12062
  local global_flags="${GLOBAL_FLAGS.join(' ')}"
11755
- local top_cmds="${TOP_LEVEL_SUBCOMMANDS.join(' ')}"
12063
+ local top_cmds="${graph.topLevelCommands.join(' ')}"
12064
+ local -a known_paths=(
12065
+ ${knownPaths}
12066
+ )
11756
12067
 
11757
- # Determine which top-level subcommand is in the line
11758
- local top_cmd=""
12068
+ local command_path=""
12069
+ local candidate=""
11759
12070
  local i
11760
12071
  for (( i=1; i<COMP_CWORD; i++ )); do
11761
12072
  local w="\${COMP_WORDS[i]}"
@@ -11763,22 +12074,31 @@ _doow_completions() {
11763
12074
  --*) ;;
11764
12075
  -*) ;;
11765
12076
  *)
11766
- if [[ " $top_cmds " == *" $w "* ]]; then
11767
- top_cmd="$w"
12077
+ candidate="\${candidate:+\$candidate }\$w"
12078
+ local matched=0
12079
+ local known
12080
+ for known in "\${known_paths[@]}"; do
12081
+ if [[ "$known" == "$candidate" ]]; then
12082
+ matched=1
12083
+ command_path="$candidate"
12084
+ break
12085
+ fi
12086
+ done
12087
+ if [[ $matched -eq 0 ]]; then
11768
12088
  break
11769
12089
  fi
11770
12090
  ;;
11771
12091
  esac
11772
12092
  done
11773
12093
 
11774
- if [[ -z "$top_cmd" ]]; then
12094
+ if [[ -z "$command_path" ]]; then
11775
12095
  # Complete top-level subcommands and global flags
11776
12096
  COMPREPLY=( $(compgen -W "$top_cmds $global_flags" -- "$cur") )
11777
12097
  return
11778
12098
  fi
11779
12099
 
11780
12100
  # Complete within a subcommand
11781
- case "$top_cmd" in
12101
+ case "$command_path" in
11782
12102
  ${subcommandCases}
11783
12103
  *) COMPREPLY=( $(compgen -W "$global_flags" -- "$cur") ) ; return ;;
11784
12104
  esac
@@ -11791,67 +12111,61 @@ complete -F _doow_completions doow
11791
12111
  // ---------------------------------------------------------------------------
11792
12112
  // Zsh completion script generator
11793
12113
  // ---------------------------------------------------------------------------
11794
- function completionZshHandler() {
11795
- const subcommandDescriptions = TOP_LEVEL_SUBCOMMANDS.map((cmd) => {
11796
- const children = SUBCOMMAND_TREE[cmd] ?? [];
11797
- const desc = children.length > 0 ? `${children.slice(0, 3).join(',')}...` : cmd;
11798
- return ` '${cmd}:${desc}'`;
11799
- }).join('\n');
11800
- const subcommandCases = Object.entries(SUBCOMMAND_TREE)
11801
- .map(([cmd, children]) => {
11802
- if (children.length === 0)
11803
- return ` (${cmd})\n ;;`;
11804
- const isFlags = children.every((c) => c.startsWith('-'));
11805
- if (isFlags) {
11806
- const flagArgs = children.map((f) => `'${f}[${f}]'`).join(' ');
11807
- return ` (${cmd})\n _arguments ${flagArgs}\n ;;`;
11808
- }
11809
- const subcmds = children.map((c) => `'${c}'`).join(' ');
11810
- return ` (${cmd})\n local -a subcmds\n subcmds=(${subcmds})\n _describe 'subcommand' subcmds\n ;;`;
12114
+ function completionZshHandler(program) {
12115
+ const graph = buildCompletionGraph(program);
12116
+ const topLevelCommands = graph.topLevelCommands.map(shellSingleQuote).join(' ');
12117
+ const globalFlags = GLOBAL_FLAGS.map(shellSingleQuote).join(' ');
12118
+ const childAssignments = graph.commandPaths
12119
+ .map((path) => {
12120
+ const words = unique([
12121
+ ...(graph.pathChildren[path] ?? []),
12122
+ ...GLOBAL_FLAGS,
12123
+ ]).join(' ');
12124
+ return ` completion_children[${shellSingleQuote(path)}]=${shellSingleQuote(words)}`;
11811
12125
  })
11812
12126
  .join('\n');
11813
- const globalFlagArgs = GLOBAL_FLAGS.map((f) => `'${f}[${f}]'`).join(' ');
11814
12127
  const script = `#compdef doow
11815
12128
  # doow zsh completion
11816
12129
  # Source this file or add the following line to ~/.zshrc:
11817
12130
  # eval "$(doow completion zsh)"
11818
12131
 
11819
12132
  _doow() {
11820
- local state
11821
-
11822
- _arguments \\
11823
- '(-j --json)'{-j,--json}'[JSON output]' \\
11824
- '--table[Table output]' \\
11825
- '--agent[Agent mode]' \\
11826
- '(-q --quiet)'{-q,--quiet}'[Suppress progress output]' \\
11827
- '--no-input[Disable interactive prompts]' \\
11828
- '(-y --yes)'{-y,--yes}'[Skip confirmation gates]' \\
11829
- '(-n --dry-run)'{-n,--dry-run}'[Preview without executing]' \\
11830
- '--profile[Multi-org profile selection]:profile' \\
11831
- '--api-key[Override auth with dak_ token]:key' \\
11832
- '--api-url[Override API base URL]:url' \\
11833
- '--debug[Verbose HTTP logging to stderr]' \\
11834
- '1: :->command' \\
11835
- '*: :->args'
11836
-
11837
- case $state in
11838
- command)
11839
- local -a commands
11840
- commands=(
11841
- ${subcommandDescriptions}
11842
- )
11843
- _describe 'command' commands
11844
- ;;
11845
- args)
11846
- local cmd="\${words[2]}"
11847
- case $cmd in
11848
- ${subcommandCases}
11849
- *)
11850
- _arguments ${globalFlagArgs}
11851
- ;;
11852
- esac
11853
- ;;
11854
- esac
12133
+ local cur="\${words[CURRENT]}"
12134
+ local candidate=""
12135
+ local command_path=""
12136
+ local -a top_level global_flags
12137
+ typeset -A completion_children
12138
+
12139
+ top_level=(${topLevelCommands})
12140
+ global_flags=(${globalFlags})
12141
+ ${childAssignments}
12142
+
12143
+ local i
12144
+ for (( i=2; i<CURRENT; i++ )); do
12145
+ local w="\${words[i]}"
12146
+ [[ "$w" == -* ]] && continue
12147
+ candidate="\${candidate:+\$candidate }\$w"
12148
+ if [[ -n "\${completion_children[$candidate]+x}" ]]; then
12149
+ command_path="$candidate"
12150
+ else
12151
+ break
12152
+ fi
12153
+ done
12154
+
12155
+ if [[ -z "$command_path" ]]; then
12156
+ compadd -- "\${top_level[@]}" "\${global_flags[@]}"
12157
+ return
12158
+ fi
12159
+
12160
+ local children="\${completion_children[$command_path]}"
12161
+ if [[ -n "$children" ]]; then
12162
+ local -a suggestions
12163
+ suggestions=(\${=children})
12164
+ compadd -- "\${suggestions[@]}"
12165
+ return
12166
+ fi
12167
+
12168
+ compadd -- "\${global_flags[@]}"
11855
12169
  }
11856
12170
 
11857
12171
  compdef _doow doow
@@ -11901,13 +12215,13 @@ function registerCompletionCommands(program) {
11901
12215
  .command('bash')
11902
12216
  .description('Output bash completion script')
11903
12217
  .action(() => {
11904
- completionBashHandler();
12218
+ completionBashHandler(program);
11905
12219
  });
11906
12220
  completion
11907
12221
  .command('zsh')
11908
12222
  .description('Output zsh completion script')
11909
12223
  .action(() => {
11910
- completionZshHandler();
12224
+ completionZshHandler(program);
11911
12225
  });
11912
12226
  completion
11913
12227
  .command('install')
@@ -36634,7 +36948,7 @@ async function startStdioServer(options) {
36634
36948
  await server.connect(transport);
36635
36949
  }
36636
36950
 
36637
- const VERSION = "0.1.6" ;
36951
+ const VERSION = "0.1.7" ;
36638
36952
  const ASCII_LOGO = `
36639
36953
  _
36640
36954
  __| | ___ ___ __ __