@burdenoff/bigconsole-cli 2026.802.1 → 2026.1029.1

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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Text hygiene for everything the CLI prints.
3
+ *
4
+ * Remote data (resource names, server error messages, HTTP response bodies)
5
+ * ends up in terminals, CI logs and support captures. The helpers here:
6
+ * - keep terminal output inert: escape sequences and control characters are
7
+ * removed, and bidirectional formatting characters are made visible;
8
+ * - mask credential-shaped values (bearer tokens, JWTs, URL credentials and
9
+ * values stored under secret-like keys);
10
+ * - turn any thrown value into a short, single-line message that is built
11
+ * from an allowlist of fields and never includes request variables,
12
+ * request headers or raw response bodies.
13
+ *
14
+ * The module is side-effect free and imports nothing from the rest of the CLI,
15
+ * so every layer can use it (and unit-test module mocks never replace it).
16
+ */
17
+ /** Longest error message shown to the user. */
18
+ export const MAX_ERROR_MESSAGE_LENGTH = 500;
19
+ /** Longest excerpt of an HTTP response body or server message kept in an error. */
20
+ export const MAX_HTTP_BODY_EXCERPT_LENGTH = 200;
21
+ /** Text is cut to this length before the redaction patterns scan it. */
22
+ const MAX_SCAN_LENGTH = 4000;
23
+ const REDACTED = '[REDACTED]';
24
+ const ELLIPSIS = '\u2026';
25
+ // ---------------------------------------------------------------------------
26
+ // Terminal output
27
+ // ---------------------------------------------------------------------------
28
+ const ESC = 0x1b;
29
+ const BEL = 0x07;
30
+ const TAB = 0x09;
31
+ const LF = 0x0a;
32
+ const CR = 0x0d;
33
+ const DEL = 0x7f;
34
+ const NEL = 0x85;
35
+ const C1_CSI = 0x9b;
36
+ const C1_ST = 0x9c;
37
+ const LINE_SEPARATOR = 0x2028;
38
+ const PARAGRAPH_SEPARATOR = 0x2029;
39
+ /** 8-bit introducers of DCS, SOS, OSC, PM and APC control strings. */
40
+ const C1_STRING_INTRODUCERS = new Set([0x90, 0x98, 0x9d, 0x9e, 0x9f]);
41
+ /** Final bytes after ESC that open DCS, SOS, OSC, PM and APC control strings. */
42
+ const ESC_STRING_INTRODUCERS = new Set(['P', 'X', ']', '^', '_'].map((c) => c.charCodeAt(0)));
43
+ /** Bidirectional embedding, override and isolate controls. */
44
+ const BIDI_CONTROLS = /[\u202a-\u202e\u2066-\u2069]/g;
45
+ /** `\uXXXX` spelling of a single UTF-16 code unit (valid in JSON and YAML). */
46
+ export function toUnicodeEscape(char) {
47
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
48
+ }
49
+ /** Make bidirectional formatting controls visible as `\uXXXX` text. */
50
+ export function escapeBidiControls(text) {
51
+ return text.replace(BIDI_CONTROLS, toUnicodeEscape);
52
+ }
53
+ function isInRange(code, low, high) {
54
+ return code >= low && code <= high;
55
+ }
56
+ /** Index just past a CSI sequence whose parameters start at `index`. */
57
+ function skipControlSequence(text, index) {
58
+ let i = index;
59
+ while (i < text.length && isInRange(text.charCodeAt(i), 0x30, 0x3f))
60
+ i++;
61
+ while (i < text.length && isInRange(text.charCodeAt(i), 0x20, 0x2f))
62
+ i++;
63
+ if (i < text.length && isInRange(text.charCodeAt(i), 0x40, 0x7e))
64
+ i++;
65
+ return i;
66
+ }
67
+ /**
68
+ * Index just past a control string (OSC, DCS, ...) whose payload starts at
69
+ * `index`. Terminated by BEL, 8-bit ST or ESC \; an unterminated string runs
70
+ * to the end of the text, exactly as a terminal would consume it.
71
+ */
72
+ function skipControlString(text, index) {
73
+ let i = index;
74
+ while (i < text.length) {
75
+ const code = text.charCodeAt(i);
76
+ if (code === BEL || code === C1_ST)
77
+ return i + 1;
78
+ if (code === ESC)
79
+ return text.charCodeAt(i + 1) === 0x5c ? i + 2 : i;
80
+ i++;
81
+ }
82
+ return i;
83
+ }
84
+ /** Index just past an ESC-introduced sequence; `index` points at the ESC. */
85
+ function skipEscapeSequence(text, index) {
86
+ const next = text.charCodeAt(index + 1);
87
+ if (Number.isNaN(next))
88
+ return index + 1;
89
+ if (next === 0x5b)
90
+ return skipControlSequence(text, index + 2);
91
+ if (ESC_STRING_INTRODUCERS.has(next))
92
+ return skipControlString(text, index + 2);
93
+ let i = index + 1;
94
+ while (i < text.length && isInRange(text.charCodeAt(i), 0x20, 0x2f))
95
+ i++;
96
+ if (i < text.length && isInRange(text.charCodeAt(i), 0x30, 0x7e))
97
+ i++;
98
+ return i;
99
+ }
100
+ /**
101
+ * Make a value safe to print on a terminal.
102
+ *
103
+ * - ANSI/VT escape sequences (CSI, OSC, DCS, SOS, PM, APC and two-character
104
+ * ESC sequences, in 7-bit and 8-bit form) are removed entirely.
105
+ * - Other C0 controls, DEL and C1 controls are removed.
106
+ * - Line breaks and TAB become a space (see {@link TerminalSanitizeOptions}).
107
+ * - Bidirectional formatting controls are shown as `\uXXXX` text so the
108
+ * printed text reads in its stored order.
109
+ *
110
+ * `null`/`undefined` become an empty string. The function is idempotent.
111
+ */
112
+ export function sanitizeForTerminal(value, options = {}) {
113
+ const text = value === null || value === undefined ? '' : String(value);
114
+ const multiline = options.preserveNewlines === true;
115
+ let out = '';
116
+ let i = 0;
117
+ while (i < text.length) {
118
+ const code = text.charCodeAt(i);
119
+ if (code === ESC) {
120
+ i = skipEscapeSequence(text, i);
121
+ continue;
122
+ }
123
+ if (code === C1_CSI) {
124
+ i = skipControlSequence(text, i + 1);
125
+ continue;
126
+ }
127
+ if (C1_STRING_INTRODUCERS.has(code)) {
128
+ i = skipControlString(text, i + 1);
129
+ continue;
130
+ }
131
+ if (code === CR || code === LF) {
132
+ const crlf = code === CR && text.charCodeAt(i + 1) === LF;
133
+ out += multiline ? '\n' : ' ';
134
+ i += crlf ? 2 : 1;
135
+ continue;
136
+ }
137
+ if (code === NEL || code === LINE_SEPARATOR || code === PARAGRAPH_SEPARATOR) {
138
+ out += multiline ? toUnicodeEscape(text.charAt(i)) : ' ';
139
+ i++;
140
+ continue;
141
+ }
142
+ if (code === TAB) {
143
+ out += multiline ? '\t' : ' ';
144
+ i++;
145
+ continue;
146
+ }
147
+ if (code < 0x20 || isInRange(code, DEL, 0x9f)) {
148
+ i++;
149
+ continue;
150
+ }
151
+ out += text.charAt(i);
152
+ i++;
153
+ }
154
+ return escapeBidiControls(out);
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Credential masking
158
+ // ---------------------------------------------------------------------------
159
+ /** A key whose value is treated as a credential when it contains one of these. */
160
+ const SECRET_KEY_HINT = /pass(?:word|wd|phrase)?|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|credential|authori[sz]ation|cookie|session[-_]?id/i;
161
+ /**
162
+ * `key: value` / `key=value` / `"key": value` openings. Keys may be quoted,
163
+ * including with backslash-escaped quotes (JSON serialised inside a string).
164
+ */
165
+ const KEY_VALUE_OPENING = /\b([A-Za-z_$][\w$-]{0,127})\\*["']?\s*[:=]\s*/g;
166
+ /** Opening quote of a value, optionally backslash-escaped. */
167
+ const QUOTE_OPENING = /^(\\*)(["'])/;
168
+ /** Unquoted value: runs to whitespace, a quote, a backslash or a delimiter. */
169
+ const UNQUOTED_VALUE = /^[^\s"'\\,;&)}\]]*/;
170
+ /** An auth scheme word that precedes the credential in header values. */
171
+ const AUTH_SCHEME = /^(?:Bearer|Basic|Digest|Token)\s+/i;
172
+ const BEARER_CREDENTIAL = /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/g;
173
+ const JWT_LIKE = /\beyJ[\w-]{4,}\.[\w-]{4,}\.[\w-]*/g;
174
+ const URL_USERINFO = /\b([a-z][a-z0-9+.-]{1,31}:\/\/)[^\s/?#@"'\\]+@/gi;
175
+ /** Number of consecutive backslashes immediately before `index`, not before `floor`. */
176
+ function backslashRun(text, index, floor) {
177
+ let run = 0;
178
+ while (index - run - 1 >= floor && text.charAt(index - run - 1) === '\\')
179
+ run++;
180
+ return run;
181
+ }
182
+ /**
183
+ * Masked replacement for the value starting at `start`, and the index just
184
+ * past that value. A quoted value ends at the matching quote (same escaping
185
+ * depth) or, when unterminated, at the end of the text.
186
+ */
187
+ function maskValue(text, start) {
188
+ if (text.startsWith(REDACTED, start)) {
189
+ return { replacement: REDACTED, end: start + REDACTED.length };
190
+ }
191
+ const quote = QUOTE_OPENING.exec(text.slice(start, start + 64));
192
+ if (quote) {
193
+ const opener = quote[0];
194
+ const depth = (quote[1] ?? '').length;
195
+ const quoteChar = quote[2] ?? '"';
196
+ let search = start + opener.length;
197
+ while (search < text.length) {
198
+ const candidate = text.indexOf(quoteChar, search);
199
+ if (candidate === -1)
200
+ break;
201
+ if (backslashRun(text, candidate, start + opener.length) === depth) {
202
+ return { replacement: `${opener}${REDACTED}${opener}`, end: candidate + 1 };
203
+ }
204
+ search = candidate + 1;
205
+ }
206
+ return { replacement: `${opener}${REDACTED}`, end: text.length };
207
+ }
208
+ const rest = text.slice(start);
209
+ const scheme = AUTH_SCHEME.exec(rest);
210
+ const offset = scheme ? scheme[0].length : 0;
211
+ const run = UNQUOTED_VALUE.exec(rest.slice(offset));
212
+ const length = offset + (run ? run[0].length : 0);
213
+ return { replacement: length > 0 ? REDACTED : '', end: start + length };
214
+ }
215
+ function redactKeyValues(text) {
216
+ let out = '';
217
+ let copiedUpTo = 0;
218
+ KEY_VALUE_OPENING.lastIndex = 0;
219
+ let match;
220
+ while ((match = KEY_VALUE_OPENING.exec(text)) !== null) {
221
+ if (!SECRET_KEY_HINT.test(match[1] ?? ''))
222
+ continue;
223
+ const start = match.index + match[0].length;
224
+ const { replacement, end } = maskValue(text, start);
225
+ if (end === start)
226
+ continue;
227
+ out += text.slice(copiedUpTo, start) + replacement;
228
+ copiedUpTo = end;
229
+ KEY_VALUE_OPENING.lastIndex = end;
230
+ }
231
+ return out + text.slice(copiedUpTo);
232
+ }
233
+ /**
234
+ * Mask credential-shaped values in free text: values under secret-like keys
235
+ * (`password`, `token`, `refreshToken`, `clientSecret`, `Authorization`, ...),
236
+ * `Bearer` credentials, JWT-shaped strings and credentials embedded in URLs.
237
+ */
238
+ export function redactSecrets(text) {
239
+ return redactKeyValues(text)
240
+ .replace(BEARER_CREDENTIAL, `Bearer ${REDACTED}`)
241
+ .replace(JWT_LIKE, REDACTED)
242
+ .replace(URL_USERINFO, `$1${REDACTED}@`);
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // Display lines and error messages
246
+ // ---------------------------------------------------------------------------
247
+ const LINE_BREAK = /\r\n|[\r\n\u0085\u2028\u2029]/;
248
+ /**
249
+ * Reduce untrusted text to one printable, credential-masked line of at most
250
+ * `maxLength` characters. Returns an empty string when nothing is left.
251
+ */
252
+ export function toDisplayLine(value, maxLength = MAX_ERROR_MESSAGE_LENGTH) {
253
+ if (value === null || value === undefined)
254
+ return '';
255
+ const firstLine = String(value).split(LINE_BREAK, 1)[0] ?? '';
256
+ const scanned = firstLine.slice(0, MAX_SCAN_LENGTH);
257
+ const line = redactSecrets(sanitizeForTerminal(scanned)).trim();
258
+ return line.length > maxLength ? `${line.slice(0, maxLength)}${ELLIPSIS}` : line;
259
+ }
260
+ /** Short, printable excerpt of an HTTP response body for an error message. */
261
+ export function summarizeHttpBody(body) {
262
+ return toDisplayLine(body, MAX_HTTP_BODY_EXCERPT_LENGTH) || 'empty response body';
263
+ }
264
+ const ERROR_CODE = /^[A-Z][A-Z0-9_]{1,63}$/;
265
+ function asRecord(value) {
266
+ // Narrowing an arbitrary thrown value; every field read below is type-checked.
267
+ return typeof value === 'object' && value !== null
268
+ ? value
269
+ : undefined;
270
+ }
271
+ function stringField(record, key) {
272
+ const value = record?.[key];
273
+ return typeof value === 'string' ? value : undefined;
274
+ }
275
+ function isAbortError(error) {
276
+ const name = stringField(asRecord(error), 'name');
277
+ return name === 'AbortError' || name === 'TimeoutError';
278
+ }
279
+ /**
280
+ * Describe an error that carries an HTTP/GraphQL `response` (graphql-request's
281
+ * ClientError). The library-composed `message` of such errors embeds the full
282
+ * request (query and variables) and the raw response body, so it is never
283
+ * read: the description is built only from the status, the first GraphQL
284
+ * error's message and its `extensions.code`.
285
+ */
286
+ function describeResponseError(response) {
287
+ const status = typeof response.status === 'number' ? response.status : undefined;
288
+ const errors = Array.isArray(response.errors) ? response.errors : [];
289
+ const first = asRecord(errors[0]);
290
+ const extensions = asRecord(first?.extensions);
291
+ // Federated subgraph errors carry the useful text under
292
+ // extensions.originalError.message; the top-level message is often generic.
293
+ const original = toDisplayLine(stringField(asRecord(extensions?.originalError), 'message'));
294
+ const wrapper = toDisplayLine(stringField(first, 'message'));
295
+ const rawCode = stringField(extensions, 'code');
296
+ const code = rawCode !== undefined && ERROR_CODE.test(rawCode) ? rawCode : undefined;
297
+ const base = original ||
298
+ wrapper ||
299
+ (status !== undefined ? `Request failed (HTTP ${status})` : 'Request failed');
300
+ return {
301
+ message: code ? `${base} [${code}]` : base,
302
+ ...(status !== undefined ? { status } : {}),
303
+ ...(code ? { code } : {}),
304
+ };
305
+ }
306
+ function networkErrorCode(error) {
307
+ const code = stringField(asRecord(asRecord(error)?.cause), 'code');
308
+ return code !== undefined && ERROR_CODE.test(code) ? code : undefined;
309
+ }
310
+ /**
311
+ * Describe a failure of a GraphQL request for display. Network-level
312
+ * `TypeError`s (fetch failures) are reported as network errors; aborted
313
+ * requests as timeouts.
314
+ */
315
+ export function describeRequestError(error) {
316
+ const response = asRecord(asRecord(error)?.response);
317
+ if (response)
318
+ return describeResponseError(response);
319
+ if (isAbortError(error))
320
+ return { message: 'Request timed out' };
321
+ if (error instanceof TypeError) {
322
+ const detail = toDisplayLine(error.message, MAX_HTTP_BODY_EXCERPT_LENGTH);
323
+ const code = networkErrorCode(error);
324
+ return {
325
+ message: `Network error${detail ? `: ${detail}` : ''}${code ? ` (${code})` : ''}`,
326
+ };
327
+ }
328
+ return { message: toSafeErrorMessage(error) };
329
+ }
330
+ /**
331
+ * Convert any thrown value into a message that is safe to print: a single
332
+ * line, at most {@link MAX_ERROR_MESSAGE_LENGTH} characters, free of terminal
333
+ * control sequences and with credential-shaped values masked. Response-bearing
334
+ * errors are described from an allowlist of fields only.
335
+ */
336
+ export function toSafeErrorMessage(error) {
337
+ const response = asRecord(asRecord(error)?.response);
338
+ if (response)
339
+ return describeResponseError(response).message;
340
+ if (isAbortError(error))
341
+ return 'Request timed out';
342
+ if (error instanceof Error || typeof error === 'string') {
343
+ const text = typeof error === 'string' ? error : error.message;
344
+ return toDisplayLine(text) || 'Unexpected error';
345
+ }
346
+ return 'Unexpected error';
347
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/bigconsole-cli",
3
- "version": "2026.802.1",
3
+ "version": "2026.1029.1",
4
4
  "description": "Command-line interface for BigConsole platform operations and management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,8 @@
33
33
  "audit": "bunx better-npm-audit audit",
34
34
  "start": "bun dist/cli.js",
35
35
  "publish:alpha": "bun run build && bunx npm@latest publish --tag alpha",
36
- "publish:prod": "bun run build && bunx npm@latest publish"
36
+ "publish:prod": "bun run build && bunx npm@latest publish",
37
+ "schema:pull": "bash scripts/schema-pull.sh"
37
38
  },
38
39
  "keywords": [
39
40
  "algoshred",
@@ -56,41 +57,42 @@
56
57
  },
57
58
  "homepage": "https://github.com/Algoshred/bigconsole-cli#readme",
58
59
  "publishConfig": {
59
- "access": "restricted"
60
+ "access": "public"
60
61
  },
61
62
  "dependencies": {
62
63
  "boxen": "^8.0.1",
63
- "chalk": "^5.5.0",
64
- "commander": "^14.0.0",
64
+ "chalk": "^6.0.0",
65
+ "commander": "^15.0.0",
65
66
  "cross-fetch": "^4.1.0",
66
- "graphql": "^16.11.0",
67
- "graphql-request": "^7.2.0",
68
- "inquirer": "^12.9.2",
69
- "js-yaml": "^4.1.0",
70
- "ora": "^8.2.0",
67
+ "graphql": "^17.0.2",
68
+ "graphql-request": "^7.4.0",
69
+ "inquirer": "^14.1.0",
70
+ "js-yaml": "^5.4.1",
71
+ "ora": "^9.4.1",
71
72
  "table": "^6.9.0",
72
73
  "update-notifier": "^7.3.1"
73
74
  },
74
75
  "devDependencies": {
75
- "@playwright/test": "^1.59.1",
76
- "@types/inquirer": "^9.0.9",
77
- "@types/js-yaml": "^4.0.9",
78
- "@types/node": "^24.3.0",
76
+ "@eslint/js": "^10.0.1",
77
+ "@playwright/test": "^1.62.1",
78
+ "@types/inquirer": "^9.0.10",
79
+ "@types/node": "^26.3.0",
79
80
  "@types/update-notifier": "^6.0.8",
80
- "@typescript-eslint/eslint-plugin": "^8.39.1",
81
- "@typescript-eslint/parser": "^8.39.1",
82
- "@vitest/coverage-v8": "^3.2.4",
83
- "@vitest/ui": "^3.2.4",
84
- "eslint": "^9.33.0",
81
+ "@typescript-eslint/eslint-plugin": "^8.68.0",
82
+ "@typescript-eslint/parser": "^8.68.0",
83
+ "@vitest/coverage-v8": "^4.1.11",
84
+ "@vitest/ui": "^4.1.11",
85
+ "eslint": "^10.9.1",
85
86
  "eslint-config-prettier": "^10.1.8",
86
- "eslint-plugin-prettier": "^5.5.4",
87
- "globals": "^16.3.0",
88
- "prettier": "^3.6.2",
89
- "rimraf": "^6.0.1",
90
- "tsc-alias": "^1.8.16",
91
- "tsx": "^4.20.4",
92
- "typescript": "^5.9.2",
93
- "vitest": "^3.2.4"
87
+ "eslint-plugin-prettier": "^5.5.6",
88
+ "globals": "^17.11.0",
89
+ "prettier": "^3.9.6",
90
+ "rimraf": "^6.1.3",
91
+ "tsc-alias": "^1.9.2",
92
+ "tsx": "^4.23.12",
93
+ "typescript": "^5.9.3",
94
+ "vitest": "^4.1.11",
95
+ "yaml": "^2.8.3"
94
96
  },
95
97
  "engines": {
96
98
  "node": ">=24.11.0",
@@ -103,14 +105,19 @@
103
105
  "fast-xml-parser": ">=4.5.5",
104
106
  "@xmldom/xmldom": ">=0.8.12",
105
107
  "swiper": ">=12.1.2",
106
- "brace-expansion": ">=1.1.13",
108
+ "brace-expansion": "5.0.9",
107
109
  "follow-redirects": ">=1.15.12",
108
110
  "smol-toml": ">=1.6.1",
109
111
  "axios": ">=1.15.0",
110
- "flatted": ">=3.4.1",
111
- "yaml": ">=2.8.3",
112
- "picomatch": ">=2.3.2",
112
+ "flatted": "3.4.4",
113
+ "yaml": "$yaml",
114
+ "picomatch": "4.0.7",
113
115
  "immutable": ">=3.8.3",
114
- "minimatch": ">=3.1.3"
116
+ "minimatch": ">=3.1.3",
117
+ "nanoid": "6.0.1",
118
+ "postcss": "8.5.26",
119
+ "vite": "8.2.2",
120
+ "fast-uri": "4.1.3",
121
+ "esbuild": "0.28.2"
115
122
  }
116
123
  }