primcli 1.34.0 → 1.36.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.
@@ -0,0 +1,254 @@
1
+ import { format } from "node:util";
2
+ //#region src/oclif/json-output.ts
3
+ /**
4
+ * Parse-safe `--json` output.
5
+ *
6
+ * Agents often run commands with stderr merged into stdout (`2>&1`).
7
+ * A command that prints a JSON document on stdout and a notice on
8
+ * stderr then hands the caller text that is not JSON, and a delivered
9
+ * send can look like an unknown outcome. With `--json`, every guarded
10
+ * command therefore prints exactly one JSON document on stdout, on
11
+ * success and on failure, and writes nothing to stderr:
12
+ *
13
+ * - Anything the command would have written to stderr (notices, hints,
14
+ * cursors, warnings) goes into the document's `warnings` array.
15
+ * - A failure adds `error` and `exit_code` to the document. When the
16
+ * command printed no document of its own, the CLI builds one.
17
+ * - A command whose `--json` output is a bare JSON array keeps that
18
+ * documented shape on success; its stderr notices are dropped.
19
+ *
20
+ * Commands that stream several documents (`listen`) are not guarded.
21
+ */
22
+ const GUARDED = Symbol.for("primitive.cli.jsonOutputGuard");
23
+ /** True when `--json` appears before any `--` separator. */
24
+ function jsonOutputRequested(argv) {
25
+ const end = argv.indexOf("--");
26
+ return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
27
+ }
28
+ function chunkToString(chunk, encoding) {
29
+ if (typeof chunk === "string") return chunk;
30
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8");
31
+ return String(chunk);
32
+ }
33
+ function capturingWrite(sink) {
34
+ return (chunk, encoding, callback) => {
35
+ sink.push(chunkToString(chunk, encoding));
36
+ const done = typeof encoding === "function" ? encoding : callback;
37
+ if (typeof done === "function") done();
38
+ return true;
39
+ };
40
+ }
41
+ /**
42
+ * Redirect stdout and stderr (both the streams and the console methods
43
+ * that write to them) into memory until `restore` is called. Nested
44
+ * captures restore in reverse order, so a guarded command that runs
45
+ * another guarded command still produces one document.
46
+ */
47
+ function captureOutput() {
48
+ const stdout = [];
49
+ const stderr = [];
50
+ const originalStdoutWrite = process.stdout.write;
51
+ const originalStderrWrite = process.stderr.write;
52
+ const originalConsole = {
53
+ debug: console.debug,
54
+ error: console.error,
55
+ info: console.info,
56
+ log: console.log,
57
+ warn: console.warn
58
+ };
59
+ const toStdout = (...args) => {
60
+ stdout.push(`${format(...args)}\n`);
61
+ };
62
+ const toStderr = (...args) => {
63
+ stderr.push(`${format(...args)}\n`);
64
+ };
65
+ process.stdout.write = capturingWrite(stdout);
66
+ process.stderr.write = capturingWrite(stderr);
67
+ console.log = toStdout;
68
+ console.info = toStdout;
69
+ console.debug = toStderr;
70
+ console.error = toStderr;
71
+ console.warn = toStderr;
72
+ return {
73
+ stderr,
74
+ stdout,
75
+ restore: () => {
76
+ process.stdout.write = originalStdoutWrite;
77
+ process.stderr.write = originalStderrWrite;
78
+ console.debug = originalConsole.debug;
79
+ console.error = originalConsole.error;
80
+ console.info = originalConsole.info;
81
+ console.log = originalConsole.log;
82
+ console.warn = originalConsole.warn;
83
+ }
84
+ };
85
+ }
86
+ function isPlainObject(value) {
87
+ return value !== null && typeof value === "object" && !Array.isArray(value);
88
+ }
89
+ function parseJson(text) {
90
+ try {
91
+ return {
92
+ ok: true,
93
+ value: JSON.parse(text)
94
+ };
95
+ } catch {
96
+ return { ok: false };
97
+ }
98
+ }
99
+ /**
100
+ * Split captured stderr into an API error payload (the JSON object the
101
+ * CLI prints for a failed request) and plain-text warning lines.
102
+ */
103
+ const ANSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
104
+ function stripAnsi(text) {
105
+ return text.replace(ANSI_SEQUENCE, "");
106
+ }
107
+ function summarizeStderr(chunks) {
108
+ let errorPayload = null;
109
+ const warnings = [];
110
+ for (const chunk of chunks) {
111
+ const trimmed = stripAnsi(chunk).trim();
112
+ if (trimmed === "") continue;
113
+ if (trimmed.startsWith("{")) {
114
+ const parsed = parseJson(trimmed);
115
+ if (parsed.ok && isPlainObject(parsed.value)) {
116
+ if (errorPayload === null) errorPayload = parsed.value;
117
+ else warnings.push(JSON.stringify(parsed.value));
118
+ continue;
119
+ }
120
+ }
121
+ for (const line of trimmed.split("\n")) {
122
+ const text = line.trim();
123
+ if (text !== "" && !warnings.includes(text)) warnings.push(text);
124
+ }
125
+ }
126
+ return {
127
+ errorPayload,
128
+ warnings
129
+ };
130
+ }
131
+ function errorExitCode(error) {
132
+ const exit = error?.oclif?.exit;
133
+ return typeof exit === "number" ? exit : void 0;
134
+ }
135
+ /** An oclif `this.exit(n)` unwinds by throwing; it is not a failure message. */
136
+ function isExitSignal(error) {
137
+ return error?.code === "EEXIT";
138
+ }
139
+ function thrownErrorBody(error) {
140
+ if (error instanceof Error) {
141
+ const body = { message: error.message };
142
+ const code = error.code;
143
+ if (typeof code === "string" && code !== "EEXIT") body.code = code;
144
+ return body;
145
+ }
146
+ return { message: String(error) };
147
+ }
148
+ /**
149
+ * Build the single JSON document a guarded `--json` command prints from
150
+ * what it wrote to stdout and stderr and how it ended.
151
+ */
152
+ function composeJsonDocument(input) {
153
+ const { errorPayload, warnings: stderrLines } = summarizeStderr(input.stderr);
154
+ const failed = input.exitCode !== 0;
155
+ let warnings = stderrLines;
156
+ if (!failed && errorPayload !== null) warnings = [...warnings, JSON.stringify(errorPayload)];
157
+ const failure = () => {
158
+ if (errorPayload !== null) return errorPayload;
159
+ if (input.thrown && !isExitSignal(input.thrown.error)) return thrownErrorBody(input.thrown.error);
160
+ return { message: warnings.at(-1) ?? `The command failed with exit code ${input.exitCode}.` };
161
+ };
162
+ const text = input.stdout.trim();
163
+ let document;
164
+ let hasDocument = false;
165
+ if (text !== "") {
166
+ const parsed = parseJson(text);
167
+ document = parsed.ok ? parsed.value : { output: text };
168
+ hasDocument = true;
169
+ }
170
+ if (!hasDocument) {
171
+ if (failed) return {
172
+ error: failure(),
173
+ exit_code: input.exitCode,
174
+ ...warnings.length > 0 ? { warnings } : {}
175
+ };
176
+ return warnings.length > 0 ? { summary: warnings.join("\n") } : {};
177
+ }
178
+ if (!isPlainObject(document)) {
179
+ if (!failed) return document;
180
+ return {
181
+ data: document,
182
+ error: failure(),
183
+ exit_code: input.exitCode,
184
+ ...warnings.length > 0 ? { warnings } : {}
185
+ };
186
+ }
187
+ const merged = { ...document };
188
+ const stated = new Set([merged.outcome_message, merged.summary].filter((value) => typeof value === "string"));
189
+ warnings = warnings.filter((warning) => !stated.has(warning));
190
+ if (failed) {
191
+ if (merged.error === void 0 || merged.error === null) merged.error = failure();
192
+ if (merged.exit_code === void 0) merged.exit_code = input.exitCode;
193
+ }
194
+ if (warnings.length > 0) {
195
+ const existing = merged.warnings;
196
+ if (existing === void 0 || existing === null) merged.warnings = warnings;
197
+ else if (Array.isArray(existing)) merged.warnings = [...existing, ...warnings.filter((warning) => !existing.includes(warning))];
198
+ else merged.cli_warnings = warnings;
199
+ }
200
+ return merged;
201
+ }
202
+ /**
203
+ * Make `cls` print one JSON document under `--json`. Idempotent per
204
+ * class; command aliases that share a class are guarded once.
205
+ */
206
+ function guardJsonOutput(cls) {
207
+ const prototype = cls.prototype;
208
+ if (Object.hasOwn(prototype, GUARDED)) return;
209
+ const originalRun = prototype._run;
210
+ prototype[GUARDED] = true;
211
+ prototype._run = async function guardedRun() {
212
+ if (!jsonOutputRequested(this.argv)) return originalRun.call(this);
213
+ const capture = captureOutput();
214
+ let result;
215
+ let thrown;
216
+ try {
217
+ result = await originalRun.call(this);
218
+ } catch (error) {
219
+ thrown = { error };
220
+ } finally {
221
+ capture.restore();
222
+ }
223
+ const document = composeJsonDocument({
224
+ exitCode: thrown ? isExitSignal(thrown.error) ? errorExitCode(thrown.error) ?? 0 : errorExitCode(thrown.error) ?? (typeof process.exitCode === "number" && process.exitCode !== 0 ? process.exitCode : 1) : typeof process.exitCode === "number" ? process.exitCode : 0,
225
+ stderr: capture.stderr,
226
+ stdout: capture.stdout.join(""),
227
+ thrown
228
+ });
229
+ console.log(JSON.stringify(document, null, 2));
230
+ if (thrown) {
231
+ if (thrown.error !== null && typeof thrown.error === "object") thrown.error.skipOclifErrorHandling = true;
232
+ throw thrown.error;
233
+ }
234
+ return result;
235
+ };
236
+ }
237
+ /** Command ids whose `--json` streams several documents over time. */
238
+ const STREAMING_JSON_COMMAND_IDS = new Set(["listen"]);
239
+ /**
240
+ * Guard every command in `commands` that accepts `--json`, except the
241
+ * streaming ones. Called once by the command registry.
242
+ */
243
+ function guardJsonOutputCommands(commands) {
244
+ const streaming = /* @__PURE__ */ new Set();
245
+ for (const id of STREAMING_JSON_COMMAND_IDS) if (commands[id] !== void 0) streaming.add(commands[id]);
246
+ for (const command of Object.values(commands)) {
247
+ if (streaming.has(command)) continue;
248
+ const flags = command.flags;
249
+ const enableJsonFlag = command.enableJsonFlag;
250
+ if (flags && "json" in flags || enableJsonFlag === true) guardJsonOutput(command);
251
+ }
252
+ }
253
+ //#endregion
254
+ export { jsonOutputRequested as n, guardJsonOutputCommands as t };