@bussolabs/closeyourit-cli 0.13.0 → 0.14.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.
package/dist/base.d.ts CHANGED
@@ -43,4 +43,15 @@ export declare abstract class BaseCommand extends Command {
43
43
  * swallowed (empty stdout, exit looks like a silent success) for machine consumers (CYCL-1).
44
44
  */
45
45
  protected catch(error: Interfaces.CommandError): Promise<unknown>;
46
+ /**
47
+ * After a successful human-mode command, print the pagination footer once. Every list command
48
+ * renders a single backend page; reading `this.api.lastMeta` (saved by CliApi on the last response)
49
+ * here keeps the "there are more pages" hint in one place instead of in all 30 of them (CYCL-11).
50
+ *
51
+ * Gated on the command actually exposing `--page`: on a download, a lookup, or a `statuses`/`tokens`
52
+ * list without that flag, `lastMeta` may be stale (e.g. left by `resolveProjectId`) or from a
53
+ * collection the reader cannot page, and "Use --page N" would point at a flag that does not exist.
54
+ * Also suppressed on error and in `--json` mode, so machine output stays exact.
55
+ */
56
+ protected finally(err: Error | undefined): Promise<unknown>;
46
57
  }
package/dist/base.js CHANGED
@@ -4,6 +4,7 @@ exports.BaseCommand = exports.pageFlag = exports.environmentFlag = exports.proje
4
4
  const core_1 = require("@oclif/core");
5
5
  const config_1 = require("./lib/config");
6
6
  const api_1 = require("./lib/api");
7
+ const output_1 = require("./lib/output");
7
8
  const error_codes_1 = require("./errors/error-codes");
8
9
  exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
10
  /** Shared `--project <id|key>` flag (required). */
@@ -110,5 +111,23 @@ class BaseCommand extends core_1.Command {
110
111
  }
111
112
  return super.catch(error);
112
113
  }
114
+ /**
115
+ * After a successful human-mode command, print the pagination footer once. Every list command
116
+ * renders a single backend page; reading `this.api.lastMeta` (saved by CliApi on the last response)
117
+ * here keeps the "there are more pages" hint in one place instead of in all 30 of them (CYCL-11).
118
+ *
119
+ * Gated on the command actually exposing `--page`: on a download, a lookup, or a `statuses`/`tokens`
120
+ * list without that flag, `lastMeta` may be stale (e.g. left by `resolveProjectId`) or from a
121
+ * collection the reader cannot page, and "Use --page N" would point at a flag that does not exist.
122
+ * Also suppressed on error and in `--json` mode, so machine output stays exact.
123
+ */
124
+ async finally(err) {
125
+ if (!err && !this.jsonEnabled() && 'page' in (this.ctor.flags ?? {})) {
126
+ const footer = (0, output_1.paginationFooter)(this.api?.lastMeta);
127
+ if (footer)
128
+ this.log(footer);
129
+ }
130
+ return super.finally(err);
131
+ }
113
132
  }
114
133
  exports.BaseCommand = BaseCommand;
package/dist/lib/api.d.ts CHANGED
@@ -25,6 +25,13 @@ export interface UploadFile {
25
25
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
26
26
  export declare class CliApi {
27
27
  private readonly config;
28
+ /**
29
+ * Meta of the most recent successful response envelope (`{page, per, total, total_pages}`, or
30
+ * undefined for non-paginated responses). `BaseCommand.finally` reads it to print the "there are
31
+ * more pages" footer once per command, instead of threading the envelope through all 30 list
32
+ * commands (CYCL-11). The instance is per-command, so this is that command's last response.
33
+ */
34
+ lastMeta?: Record<string, unknown>;
28
35
  constructor(config: CliConfig);
29
36
  request<T = unknown>(method: HttpMethod, path: string, opts?: RequestOptions): Promise<Envelope<T>>;
30
37
  /**
package/dist/lib/api.js CHANGED
@@ -18,6 +18,13 @@ class ApiRequestError extends Error {
18
18
  exports.ApiRequestError = ApiRequestError;
19
19
  class CliApi {
20
20
  config;
21
+ /**
22
+ * Meta of the most recent successful response envelope (`{page, per, total, total_pages}`, or
23
+ * undefined for non-paginated responses). `BaseCommand.finally` reads it to print the "there are
24
+ * more pages" footer once per command, instead of threading the envelope through all 30 list
25
+ * commands (CYCL-11). The instance is per-command, so this is that command's last response.
26
+ */
27
+ lastMeta;
21
28
  constructor(config) {
22
29
  this.config = config;
23
30
  }
@@ -97,7 +104,9 @@ class CliApi {
97
104
  const error = envelope.error ?? {};
98
105
  throw new ApiRequestError(res.status, error.code ?? `C${res.status}-API-000`, error.message ?? res.statusText ?? `Request failed (${res.status})`, error.details);
99
106
  }
100
- return (json ?? {});
107
+ const envelope = (json ?? {});
108
+ this.lastMeta = envelope.meta;
109
+ return envelope;
101
110
  }
102
111
  get(path, opts) {
103
112
  return this.request('GET', path, opts);
@@ -132,6 +141,10 @@ class CliApi {
132
141
  await this.handleResponse(res); // always throws: maps the JSON error envelope
133
142
  throw new ApiRequestError(res.status, `C${res.status}-API-000`, `Download failed (${res.status})`);
134
143
  }
144
+ // A binary payload is not a paginated collection; clear any meta a prior lookup (e.g.
145
+ // resolveProjectId) left behind so it cannot leak into the pagination footer (CYCL-11). The
146
+ // success path bypasses handleResponse, so this reset must be explicit.
147
+ this.lastMeta = undefined;
135
148
  const data = new Uint8Array(await res.arrayBuffer());
136
149
  const disposition = res.headers.get('content-disposition') ?? '';
137
150
  const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(disposition);
@@ -15,6 +15,14 @@ export declare function sanitizeMultiline(value: unknown): string;
15
15
  export declare function displayWidth(value: string): number;
16
16
  /** Minimal dependency-free table renderer for human output. */
17
17
  export declare function renderTable(headers: string[], rows: string[][]): string;
18
+ /**
19
+ * Footer line telling the reader that the human table is only one backend page of a larger set:
20
+ * without it, page 1 reads as the whole collection and a 900-ticket project looks like 10 (CYCL-11).
21
+ * Returns undefined when nothing more follows — a single page, the last page, or a collection already
22
+ * aggregated by `fetchAllPages` (whose last saved meta has `page === total_pages`) — so complete
23
+ * output stays clean. Callers gate on `!jsonEnabled()`, so machine output never sees this.
24
+ */
25
+ export declare function paginationFooter(meta: unknown): string | undefined;
18
26
  /**
19
27
  * Status dots make problems scannable at a glance. Four fixed-width circles (no variation
20
28
  * selectors, so table columns stay aligned): down=🔴, warn=🟡, ok=🟢, neutral=⚪.
@@ -4,6 +4,7 @@ exports.sanitize = sanitize;
4
4
  exports.sanitizeMultiline = sanitizeMultiline;
5
5
  exports.displayWidth = displayWidth;
6
6
  exports.renderTable = renderTable;
7
+ exports.paginationFooter = paginationFooter;
7
8
  exports.statusDot = statusDot;
8
9
  exports.statusCell = statusCell;
9
10
  exports.section = section;
@@ -72,6 +73,25 @@ function renderTable(headers, rows) {
72
73
  const separator = widths.map((width) => '-'.repeat(width));
73
74
  return [line(safeHeaders), line(separator), ...safeRows.map(line)].join('\n');
74
75
  }
76
+ /**
77
+ * Footer line telling the reader that the human table is only one backend page of a larger set:
78
+ * without it, page 1 reads as the whole collection and a 900-ticket project looks like 10 (CYCL-11).
79
+ * Returns undefined when nothing more follows — a single page, the last page, or a collection already
80
+ * aggregated by `fetchAllPages` (whose last saved meta has `page === total_pages`) — so complete
81
+ * output stays clean. Callers gate on `!jsonEnabled()`, so machine output never sees this.
82
+ */
83
+ function paginationFooter(meta) {
84
+ if (!meta || typeof meta !== 'object')
85
+ return undefined;
86
+ const { page, total, total_pages: totalPages } = meta;
87
+ const current = Number(page);
88
+ const last = Number(totalPages);
89
+ if (!Number.isInteger(current) || !Number.isInteger(last) || current < 1 || current >= last)
90
+ return undefined;
91
+ const count = Number(total);
92
+ const totalPart = Number.isFinite(count) ? ` — ${count} items total` : '';
93
+ return `Showing page ${current} of ${last}${totalPart}. Use --page ${current + 1} to see more.`;
94
+ }
75
95
  const TONE_DOT = {
76
96
  down: '🔴',
77
97
  neutral: '⚪',