bdy 1.23.13 → 1.23.14-dev

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 (38) hide show
  1. package/distTs/package.json +3 -1
  2. package/distTs/src/api/client.js +259 -24
  3. package/distTs/src/cliIndex.js +4 -0
  4. package/distTs/src/command/api/request.js +5 -4
  5. package/distTs/src/command/artifact/list.js +2 -6
  6. package/distTs/src/command/distro/list.js +1 -0
  7. package/distTs/src/command/distro/route/update.js +1 -1
  8. package/distTs/src/command/distro/update.js +9 -9
  9. package/distTs/src/command/environment/create.js +70 -0
  10. package/distTs/src/command/environment/delete.js +30 -0
  11. package/distTs/src/command/environment/get.js +48 -0
  12. package/distTs/src/command/environment/list.js +45 -0
  13. package/distTs/src/command/environment/resolve.js +39 -0
  14. package/distTs/src/command/environment/update.js +60 -0
  15. package/distTs/src/command/environment.js +20 -0
  16. package/distTs/src/command/project/git/credential.js +6 -4
  17. package/distTs/src/command/sandbox/exec/command.js +1 -1
  18. package/distTs/src/command/sandbox/exec/logs.js +1 -1
  19. package/distTs/src/command/target/create.js +33 -0
  20. package/distTs/src/command/target/delete.js +31 -0
  21. package/distTs/src/command/target/exec/command.js +101 -0
  22. package/distTs/src/command/target/exec/kill.js +31 -0
  23. package/distTs/src/command/target/exec/list.js +53 -0
  24. package/distTs/src/command/target/exec/logs.js +27 -0
  25. package/distTs/src/command/target/exec/status.js +44 -0
  26. package/distTs/src/command/target/exec.js +24 -0
  27. package/distTs/src/command/target/get.js +63 -0
  28. package/distTs/src/command/target/list.js +49 -0
  29. package/distTs/src/command/target/scope.js +138 -0
  30. package/distTs/src/command/target/update.js +32 -0
  31. package/distTs/src/command/target.js +22 -0
  32. package/distTs/src/index.js +14 -0
  33. package/distTs/src/input.js +65 -0
  34. package/distTs/src/output/pipeline.js +5 -3
  35. package/distTs/src/output.js +30 -2
  36. package/distTs/src/texts.js +251 -27
  37. package/distTs/src/utils.js +18 -2
  38. package/package.json +3 -1
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../utils");
7
+ const texts_1 = require("../../texts");
8
+ const output_1 = __importDefault(require("../../output"));
9
+ const input_1 = __importDefault(require("../../input"));
10
+ const scope_1 = require("./scope");
11
+ const commandTargetList = (0, utils_1.newCommand)('list', texts_1.DESC_COMMAND_TARGET_LIST);
12
+ commandTargetList.alias('ls');
13
+ commandTargetList.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
14
+ commandTargetList.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
15
+ commandTargetList.option('--pipeline <identifier>', texts_1.OPTION_TARGET_PIPELINE);
16
+ commandTargetList.option('--environment <identifier>', texts_1.OPTION_TARGET_ENVIRONMENT);
17
+ commandTargetList.option('--scope <scope>', texts_1.OPT_COMMAND_TARGET_SCOPE);
18
+ commandTargetList.option('--format <text|json>', texts_1.OPTION_FORMAT);
19
+ commandTargetList.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_TARGET_LIST}`);
20
+ commandTargetList.action(async (options) => {
21
+ const workspace = input_1.default.restApiWorkspace(options.workspace);
22
+ const project = input_1.default.restApiProject(options.project, true);
23
+ const scope = input_1.default.targetScope(project, options.pipeline, options.environment, options.scope);
24
+ input_1.default.projectScopeNote(options, project, scope, 'targets');
25
+ const client = input_1.default.restApiTokenClient();
26
+ const filters = await (0, scope_1.resolveTargetScope)(client, workspace, project, scope, options);
27
+ const result = await client.getTargets(workspace, filters);
28
+ const targets = result.targets || [];
29
+ if (options.format === 'json') {
30
+ output_1.default.json(targets);
31
+ }
32
+ else {
33
+ if (targets.length === 0) {
34
+ output_1.default.exitNormal(texts_1.TXT_TARGET_NOT_FOUND);
35
+ }
36
+ const data = [['NAME', 'IDENTIFIER', 'TYPE', 'SCOPE']];
37
+ for (const target of targets) {
38
+ data.push([
39
+ target.name || '-',
40
+ target.identifier || '-',
41
+ target.type || '-',
42
+ (0, scope_1.targetScopeOf)(target),
43
+ ]);
44
+ }
45
+ output_1.default.table(data);
46
+ }
47
+ output_1.default.exitNormal();
48
+ });
49
+ exports.default = commandTargetList;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveTargetScope = exports.targetScopeOf = exports.targetAddressOf = exports.forwardExecLog = exports.resolveTargetId = void 0;
7
+ const utils_1 = require("../../utils");
8
+ const texts_1 = require("../../texts");
9
+ const output_1 = __importDefault(require("../../output"));
10
+ const input_1 = __importDefault(require("../../input"));
11
+ // Shared resolve preamble for commands taking a target identifier: line
12
+ // context from flags/env -> /identifiers -> hash id, exiting on a miss.
13
+ //
14
+ // strictLine is for mutating commands (update/delete/exec command): an
15
+ // explicit line flag is a statement "the target of this project/pipeline/
16
+ // environment", yet /identifiers falls back to the workspace target of the
17
+ // same identifier when the requested line exists but does not hold it - the
18
+ // operation would silently land on a workspace-wide resource. Reads keep
19
+ // the fallback (workspace targets stay addressable from a line context).
20
+ const resolveTargetId = async (client, workspace, identifier, options, strictLine = false) => {
21
+ const project = input_1.default.restApiProject(options.project, true);
22
+ const line = input_1.default.targetLine(project, options.pipeline, options.environment);
23
+ const resolved = await client.getTargetByIdentifier(workspace, identifier, line);
24
+ if (!resolved.target_id) {
25
+ output_1.default.exitError(texts_1.ERR_TARGET_NOT_FOUND);
26
+ }
27
+ if (strictLine) {
28
+ const requested = options.pipeline
29
+ ? utils_1.TARGET_SCOPE.PIPELINE
30
+ : options.environment
31
+ ? utils_1.TARGET_SCOPE.ENVIRONMENT
32
+ : options.project
33
+ ? utils_1.TARGET_SCOPE.PROJECT
34
+ : null;
35
+ if (requested) {
36
+ const target = await client.getTarget(workspace, resolved.target_id);
37
+ const matches = requested === utils_1.TARGET_SCOPE.PIPELINE
38
+ ? String(target.pipeline?.id) === String(resolved.pipeline_id)
39
+ : requested === utils_1.TARGET_SCOPE.ENVIRONMENT
40
+ ? target.environment?.id === resolved.environment_id
41
+ : (0, exports.targetScopeOf)(target) === utils_1.TARGET_SCOPE.PROJECT &&
42
+ target.project?.name === resolved.project_identifier;
43
+ if (!matches) {
44
+ output_1.default.exitError(texts_1.ERR_TARGET_NOT_FOUND);
45
+ }
46
+ }
47
+ }
48
+ return resolved.target_id;
49
+ };
50
+ exports.resolveTargetId = resolveTargetId;
51
+ // Lines the CLI requests per log fetch.
52
+ const EXEC_LOG_PAGE = 1000;
53
+ // The exec log endpoint pages by line offset; its `truncated` field
54
+ // describes server-side output truncation, not pagination. `lines` may
55
+ // additionally include a trailing line that is still being written -
56
+ // `next_offset` only advances past committed lines - so exactly
57
+ // next_offset - offset lines are emitted to avoid re-printing the partial
58
+ // line on the next pass. A page shorter than requested means the committed
59
+ // log is drained, so the common case costs a single request.
60
+ const forwardExecLog = async (client, workspace, execId, startOffset, emitPage = (lines) => output_1.default.data(lines.join('\n')), maxLines) => {
61
+ let offset = startOffset;
62
+ let emitted = 0;
63
+ let truncated = false;
64
+ for (;;) {
65
+ const pageLimit = maxLines === undefined
66
+ ? EXEC_LOG_PAGE
67
+ : Math.min(EXEC_LOG_PAGE, maxLines - emitted);
68
+ if (pageLimit <= 0)
69
+ break;
70
+ const log = await client.getTargetExecLog(workspace, execId, offset, pageLimit);
71
+ truncated = truncated || !!log.truncated;
72
+ const next = log.next_offset ?? offset;
73
+ if (next <= offset)
74
+ break;
75
+ const lines = (log.lines || []).slice(0, next - offset);
76
+ if (lines.length)
77
+ emitPage(lines);
78
+ emitted += lines.length;
79
+ offset = next;
80
+ if (lines.length < pageLimit)
81
+ break;
82
+ }
83
+ return { offset, truncated };
84
+ };
85
+ exports.forwardExecLog = forwardExecLog;
86
+ // Connection info condensed to one host:port/database cell (auth is left
87
+ // out on purpose - the text view is a summary, credentials stay in json).
88
+ const targetAddressOf = (target) => {
89
+ if (!target.host)
90
+ return '-';
91
+ let address = target.host;
92
+ if (target.port)
93
+ address += `:${target.port}`;
94
+ if (target.database)
95
+ address += `/${target.database}`;
96
+ return address;
97
+ };
98
+ exports.targetAddressOf = targetAddressOf;
99
+ const targetScopeOf = (target) => {
100
+ if (target.pipeline)
101
+ return utils_1.TARGET_SCOPE.PIPELINE;
102
+ if (target.environment)
103
+ return utils_1.TARGET_SCOPE.ENVIRONMENT;
104
+ if (target.project)
105
+ return utils_1.TARGET_SCOPE.PROJECT;
106
+ return utils_1.TARGET_SCOPE.WORKSPACE;
107
+ };
108
+ exports.targetScopeOf = targetScopeOf;
109
+ // Input.targetScope guarantees project is set for the PIPELINE scope.
110
+ // Environments exist on both workspace and project level, so for the
111
+ // ENVIRONMENT scope project is optional and narrows the lookup when given.
112
+ const resolveTargetScope = async (client, workspace, project, scope, options) => {
113
+ const params = {};
114
+ if (scope === utils_1.TARGET_SCOPE.PIPELINE) {
115
+ const { pipeline_id } = await client.getPipelineByIdentifier(workspace, project, options.pipeline);
116
+ if (!pipeline_id)
117
+ output_1.default.exitError(texts_1.ERR_PIPELINE_NOT_FOUND);
118
+ params.pipelineId = pipeline_id;
119
+ }
120
+ else if (scope === utils_1.TARGET_SCOPE.ENVIRONMENT) {
121
+ const query = {
122
+ environment: options.environment,
123
+ };
124
+ if (project)
125
+ query.project = project;
126
+ const resolved = await client.getResourceByIdentifier(workspace, query);
127
+ // a nonexistent project is rejected by getResourceByIdentifier itself
128
+ if (!resolved.environment_id) {
129
+ output_1.default.exitError(texts_1.ERR_TARGET_ENVIRONMENT_NOT_FOUND);
130
+ }
131
+ params.environmentId = resolved.environment_id;
132
+ }
133
+ else if (scope === utils_1.TARGET_SCOPE.PROJECT) {
134
+ params.project = project;
135
+ }
136
+ return params;
137
+ };
138
+ exports.resolveTargetScope = resolveTargetScope;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../utils");
7
+ const texts_1 = require("../../texts");
8
+ const output_1 = __importDefault(require("../../output"));
9
+ const input_1 = __importDefault(require("../../input"));
10
+ const scope_1 = require("./scope");
11
+ const commandTargetUpdate = (0, utils_1.newCommand)('update', texts_1.DESC_COMMAND_TARGET_UPDATE);
12
+ commandTargetUpdate.alias('edit');
13
+ commandTargetUpdate.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
14
+ commandTargetUpdate.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
15
+ commandTargetUpdate.option('--pipeline <identifier>', texts_1.OPTION_TARGET_PIPELINE);
16
+ commandTargetUpdate.option('--environment <identifier>', texts_1.OPTION_TARGET_ENVIRONMENT);
17
+ commandTargetUpdate.option('--yaml <content|@path>', texts_1.OPTION_TARGET_YAML);
18
+ commandTargetUpdate.argument('<identifier>', texts_1.OPTION_TARGET_IDENTIFIER);
19
+ commandTargetUpdate.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_TARGET_UPDATE}`);
20
+ commandTargetUpdate.action(async (identifier, options) => {
21
+ const yaml = input_1.default.restApiYaml(options.yaml);
22
+ const workspace = input_1.default.restApiWorkspace(options.workspace);
23
+ const client = input_1.default.restApiTokenClient();
24
+ const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options,
25
+ /* strictLine */ true);
26
+ const body = {
27
+ yaml: Buffer.from(yaml, 'utf8').toString('base64'),
28
+ };
29
+ const result = await client.updateTargetByYaml(workspace, target_id, body);
30
+ output_1.default.exitSuccess((0, texts_1.TXT_TARGET_UPDATED)(result.identifier, result.html_url));
31
+ });
32
+ exports.default = commandTargetUpdate;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../utils");
7
+ const texts_1 = require("../texts");
8
+ const list_1 = __importDefault(require("./target/list"));
9
+ const get_1 = __importDefault(require("./target/get"));
10
+ const create_1 = __importDefault(require("./target/create"));
11
+ const update_1 = __importDefault(require("./target/update"));
12
+ const delete_1 = __importDefault(require("./target/delete"));
13
+ const exec_1 = __importDefault(require("./target/exec"));
14
+ const commandTarget = (0, utils_1.newCommand)('target', texts_1.DESC_COMMAND_TARGET);
15
+ commandTarget.alias('tg');
16
+ commandTarget.addCommand(list_1.default);
17
+ commandTarget.addCommand(get_1.default);
18
+ commandTarget.addCommand(create_1.default);
19
+ commandTarget.addCommand(update_1.default);
20
+ commandTarget.addCommand(delete_1.default);
21
+ commandTarget.addCommand(exec_1.default);
22
+ exports.default = commandTarget;
@@ -9,7 +9,21 @@ const stream_1 = __importDefault(require("stream"));
9
9
  output_1.default.muteNodeWarnings();
10
10
  stream_1.default.setDefaultHighWaterMark(false, 65536);
11
11
  process.title = 'bdy';
12
+ // Node writes to a pipe asynchronously and process.exit() does not wait for
13
+ // the buffer to drain, so large output piped to another process was cut at a
14
+ // pipe-buffer boundary (files and POSIX TTYs are synchronous already).
15
+ // Blocking mode makes every write land before exit, on all Output paths.
16
+ for (const s of [process.stdout, process.stderr]) {
17
+ s._handle?.setBlocking?.(true);
18
+ }
19
+ // The cost of blocking mode: a reader that closes early (`bdy ... | head`)
20
+ // turns further writes into EPIPE errors, which surface here as uncaught
21
+ // exceptions. That is the reader saying "got enough", not a failure - end
22
+ // quietly like any well-behaved pipe writer.
23
+ const isEpipe = (err) => err?.code === 'EPIPE' || err?.cause?.code === 'EPIPE';
12
24
  process.on('uncaughtException', (err) => {
25
+ if (isEpipe(err))
26
+ process.exit(0);
13
27
  logger_1.default.fatal(err);
14
28
  output_1.default.exitError(err);
15
29
  });
@@ -368,8 +368,19 @@ class Input {
368
368
  const ApiClient = require('./api/client').default;
369
369
  const { token: t, refreshToken, clientId, clientToken, clientSecret, } = this.restApiToken(allowNoToken, token);
370
370
  const baseUrl = this.restApiBaseUrl(api, region, t);
371
+ ApiClient.onRateLimitLow = (remaining, reset) => output_1.default.noteWarning((0, texts_1.WARN_REST_API_RATE_LIMIT_LOW)(remaining, reset));
371
372
  return new ApiClient(baseUrl, t, refreshToken, clientId, clientSecret, clientToken);
372
373
  }
374
+ // Stderr note for list commands whose PROJECT scope was inferred (linked
375
+ // project / BUDDY_PROJECT) rather than requested via -p or --scope, so the
376
+ // narrowing is never silent.
377
+ static projectScopeNote(options, project, scope, resource) {
378
+ if (options.format === 'json' || options.scope || options.project)
379
+ return;
380
+ if (scope !== 'PROJECT' || !project)
381
+ return;
382
+ output_1.default.note((0, texts_1.TXT_LIST_PROJECT_SCOPE_NOTE)(resource, project));
383
+ }
373
384
  static routeType(type) {
374
385
  if (!type)
375
386
  return distro_1.ROUTE_TYPE.DIRECT_PROXY;
@@ -1087,6 +1098,54 @@ class Input {
1087
1098
  return distro_1.SCOPE.PROJECT;
1088
1099
  return distro_1.SCOPE.WORKSPACE;
1089
1100
  }
1101
+ static targetScope(project, pipeline, environment, scope) {
1102
+ Input.targetLine(project, pipeline, environment);
1103
+ if (scope) {
1104
+ scope = scope.toUpperCase();
1105
+ if (scope === utils_1.TARGET_SCOPE.WORKSPACE) {
1106
+ return scope;
1107
+ }
1108
+ if (scope === utils_1.TARGET_SCOPE.PROJECT) {
1109
+ if (!project)
1110
+ output_1.default.exitError(texts_1.ERR_COMMAND_SCOPE_NO_PROJECT);
1111
+ return scope;
1112
+ }
1113
+ if (scope === utils_1.TARGET_SCOPE.PIPELINE) {
1114
+ if (!pipeline)
1115
+ output_1.default.exitError(texts_1.ERR_TARGET_SCOPE_NO_PIPELINE);
1116
+ return scope;
1117
+ }
1118
+ if (scope === utils_1.TARGET_SCOPE.ENVIRONMENT) {
1119
+ if (!environment)
1120
+ output_1.default.exitError(texts_1.ERR_TARGET_SCOPE_NO_ENVIRONMENT);
1121
+ return scope;
1122
+ }
1123
+ return output_1.default.exitError(texts_1.ERR_TARGET_SCOPE);
1124
+ }
1125
+ if (pipeline)
1126
+ return utils_1.TARGET_SCOPE.PIPELINE;
1127
+ if (environment)
1128
+ return utils_1.TARGET_SCOPE.ENVIRONMENT;
1129
+ if (project)
1130
+ return utils_1.TARGET_SCOPE.PROJECT;
1131
+ return utils_1.TARGET_SCOPE.WORKSPACE;
1132
+ }
1133
+ // Line context for resolving a target identifier via /identifiers:
1134
+ // params describe one line from workspace down (ws -> project -> pipeline
1135
+ // or environment), searched from the deepest scope up.
1136
+ static targetLine(project, pipeline, environment) {
1137
+ if (pipeline && !project) {
1138
+ output_1.default.exitError(texts_1.ERR_TARGET_PIPELINE_REQUIRES_PROJECT);
1139
+ }
1140
+ if (pipeline && environment) {
1141
+ output_1.default.exitError(texts_1.ERR_TARGET_PIPELINE_ENV_CONFLICT);
1142
+ }
1143
+ return {
1144
+ project: project || undefined,
1145
+ pipeline,
1146
+ environment,
1147
+ };
1148
+ }
1090
1149
  static restApiProject(project, allowNull) {
1091
1150
  const ProjectCfg = require('./project/cfg').default;
1092
1151
  let p = process.env.BUDDY_PROJECT;
@@ -1107,6 +1166,12 @@ class Input {
1107
1166
  output_1.default.exitError(texts_1.ERR_SANDBOX_BOOT_SCRIPT_NOT_EXISTS);
1108
1167
  return node_fs_1.default.readFileSync(p, 'utf8');
1109
1168
  }
1169
+ static tags(tags) {
1170
+ return tags
1171
+ .split(',')
1172
+ .map((t) => t.trim())
1173
+ .filter(Boolean);
1174
+ }
1110
1175
  static restApiYaml(yaml) {
1111
1176
  if (!yaml)
1112
1177
  output_1.default.exitError(texts_1.ERR_REST_API_YAML);
@@ -1191,7 +1191,9 @@ class OutputPipeline {
1191
1191
  this.jsonLog(logs[actionLogsStart]);
1192
1192
  }
1193
1193
  else {
1194
- output_1.default.normal(output_1.default.getTermKitDimColor(` ${logs[actionLogsStart]}`));
1194
+ // log lines are payload inside markup - escape so % and ^
1195
+ // sequences in build output render verbatim
1196
+ output_1.default.normal(output_1.default.getTermKitDimColor(` ${output_1.default.escapeTermKit(logs[actionLogsStart])}`));
1195
1197
  }
1196
1198
  }
1197
1199
  if (action &&
@@ -1221,9 +1223,9 @@ class OutputPipeline {
1221
1223
  let line = '';
1222
1224
  if (i > 0)
1223
1225
  line += ' ';
1224
- line += output_1.default.getTermKitMutedColor(v.key);
1226
+ line += output_1.default.getTermKitMutedColor(output_1.default.escapeTermKit(v.key));
1225
1227
  line += output_1.default.getTermKitDimColor(` = "`);
1226
- line += output_1.default.getTermKitBlueColor(v.value);
1228
+ line += output_1.default.getTermKitBlueColor(output_1.default.escapeTermKit(v.value));
1227
1229
  line += output_1.default.getTermKitDimColor('"');
1228
1230
  output_1.default.normal(line);
1229
1231
  });
@@ -134,6 +134,32 @@ class Output {
134
134
  msg += '\n';
135
135
  getTerminal()(msg);
136
136
  }
137
+ // Payload output (command results, logs, JSON): bypasses terminal-kit,
138
+ // whose printf-style formatting and ^-markup corrupt data containing
139
+ // % or ^ sequences. newLine=false emits a mid-stream chunk, keeping
140
+ // chunk boundaries out of the payload.
141
+ static data(txt, newLine = true) {
142
+ process.stdout.write(newLine ? `${txt}\n` : txt);
143
+ }
144
+ // For payload embedded in terminal-kit markup (e.g. a dimmed log line):
145
+ // neutralizes the ^-markup and %-formatting the payload would otherwise
146
+ // trigger, so it renders verbatim while the surrounding markup still
147
+ // works. Only for strings printed via terminal() - ScreenBuffer.put does
148
+ // not printf-format, so there %% would render double.
149
+ static escapeTermKit(txt) {
150
+ return txt.replace(/[%^]/g, '$&$&');
151
+ }
152
+ static writeStderr(txt) {
153
+ process.stderr.write(`${txt}\n`);
154
+ }
155
+ // Narration accompanying payload output goes to stderr so stdout stays
156
+ // parseable by scripts.
157
+ static note(txt) {
158
+ this.writeStderr(this.getMutedColor(txt));
159
+ }
160
+ static noteWarning(txt) {
161
+ this.writeStderr(this.getYellowColor(txt));
162
+ }
137
163
  static gray(txt, newLine = true) {
138
164
  this.normal(this.getMutedColor(txt), newLine);
139
165
  }
@@ -624,7 +650,7 @@ class Output {
624
650
  _terminal.grabInput(false);
625
651
  }
626
652
  static json(json) {
627
- this.normal(JSON.stringify(json));
653
+ this.data(JSON.stringify(json));
628
654
  }
629
655
  static exitError(err) {
630
656
  this.clearUndici();
@@ -642,7 +668,9 @@ class Output {
642
668
  if (isDebug)
643
669
  msg += `\n${e.stack}`;
644
670
  }
645
- getTerminal().red.error(`${msg}\n`);
671
+ // Raw stderr: error messages carry user/API data (SQL, URLs) that
672
+ // terminal-kit's %-formatting would corrupt.
673
+ this.writeStderr(this.getRedColor(String(msg)));
646
674
  process.exit(1);
647
675
  }
648
676
  }