@langchain/langgraph-cli 1.3.1 → 1.4.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,387 @@
1
+ /**
2
+ * Dual-mode output for the `deploy` command, ported from the Python CLI's
3
+ * `_Emitter`. Either emits JSON-lines (``--json``) or human-readable colored
4
+ * text. The `deploy` command writes directly to stdout (separate from the
5
+ * winston logger) so machine-readable JSON output stays clean.
6
+ */
7
+ /** Whether ANSI colors should be suppressed (no TTY or `NO_COLOR` set). */
8
+ const NO_COLOR = process.env.NO_COLOR != null || !process.stdout.isTTY;
9
+ /** ANSI SGR codes for each supported {@link Color}. */
10
+ const COLOR_CODES = {
11
+ cyan: "36",
12
+ green: "32",
13
+ yellow: "33",
14
+ red: "31",
15
+ };
16
+ /**
17
+ * Wrap a string in ANSI color codes, unless color output is disabled.
18
+ *
19
+ * @param message - Text to colorize.
20
+ * @param color - Color to apply.
21
+ * @returns The colorized string, or `message` unchanged when colors are off.
22
+ */
23
+ function colorize(message, color) {
24
+ if (NO_COLOR)
25
+ return message;
26
+ return `\u001b[${COLOR_CODES[color]}m${message}\u001b[0m`;
27
+ }
28
+ /**
29
+ * Format an elapsed duration as a compact human string.
30
+ *
31
+ * @param seconds - Elapsed time in seconds.
32
+ * @returns A string like `42s` or `3m 05s`.
33
+ */
34
+ function formatElapsed(seconds) {
35
+ const total = Math.trunc(seconds);
36
+ const mins = Math.trunc(total / 60);
37
+ const secs = total % 60;
38
+ return mins ? `${mins}m ${String(secs).padStart(2, "0")}s` : `${secs}s`;
39
+ }
40
+ /**
41
+ * Dual-mode output sink for the `deploy` command.
42
+ *
43
+ * @remarks
44
+ * In JSON mode every call emits a single JSON-lines event to stdout; otherwise
45
+ * calls render human-readable, optionally colorized text. The two modes share
46
+ * the same call sites so the orchestration code stays mode-agnostic.
47
+ */
48
+ export class Emitter {
49
+ /** Whether structured JSON-lines output is enabled. */
50
+ json;
51
+ /**
52
+ * @param jsonMode - When `true`, emit JSON-lines instead of text.
53
+ */
54
+ constructor(jsonMode) {
55
+ this.json = jsonMode;
56
+ }
57
+ /** Whether this emitter is in JSON-lines mode. */
58
+ get jsonMode() {
59
+ return this.json;
60
+ }
61
+ /**
62
+ * Write a single JSON-lines event to stdout.
63
+ *
64
+ * @param obj - The event object to serialize.
65
+ */
66
+ write(obj) {
67
+ process.stdout.write(`${JSON.stringify(obj)}\n`);
68
+ }
69
+ /**
70
+ * Write a plain line of text to stdout.
71
+ *
72
+ * @param message - The line to write (a trailing newline is added).
73
+ */
74
+ echo(message) {
75
+ process.stdout.write(`${message}\n`);
76
+ }
77
+ /**
78
+ * Emit a numbered, top-level step (e.g. `1. Building image`).
79
+ *
80
+ * @param step - The 1-based step number.
81
+ * @param message - Step description.
82
+ * @param extra - Additional fields merged into the JSON event.
83
+ */
84
+ step(step, message, extra) {
85
+ if (this.json) {
86
+ this.write({ event: "step", step, message, ...extra });
87
+ }
88
+ else {
89
+ this.echo(colorize(`${step}. ${message}`, "cyan"));
90
+ }
91
+ }
92
+ /**
93
+ * Emit an informational detail line nested under a step.
94
+ *
95
+ * @param message - The detail message.
96
+ * @param extra - Additional fields merged into the JSON event.
97
+ */
98
+ info(message, extra) {
99
+ if (this.json) {
100
+ this.write({ event: "info", message, ...extra });
101
+ }
102
+ else {
103
+ this.echo(colorize(` ${message}`, "green"));
104
+ }
105
+ }
106
+ /**
107
+ * Emit a warning nested under a step. Leading whitespace is stripped in JSON
108
+ * mode so the structured `message` stays clean.
109
+ *
110
+ * @param message - The warning message.
111
+ * @param extra - Additional fields merged into the JSON event.
112
+ */
113
+ warn(message, extra) {
114
+ if (this.json) {
115
+ this.write({
116
+ event: "warn",
117
+ message: message.replace(/^\s+/, ""),
118
+ ...extra,
119
+ });
120
+ }
121
+ else {
122
+ this.echo(colorize(` ${message}`, "yellow"));
123
+ }
124
+ }
125
+ /**
126
+ * Emit a top-level banner (pre-step). Not indented in text mode.
127
+ *
128
+ * @param message - The banner message.
129
+ * @param extra - Additional fields merged into the JSON event.
130
+ */
131
+ note(message, extra) {
132
+ if (this.json) {
133
+ this.write({ event: "note", message, ...extra });
134
+ }
135
+ else {
136
+ this.echo(colorize(message, "yellow"));
137
+ }
138
+ }
139
+ /**
140
+ * Emit an error line nested under a step.
141
+ *
142
+ * @param message - The error message.
143
+ * @param extra - Additional fields merged into the JSON event.
144
+ */
145
+ error(message, extra) {
146
+ if (this.json) {
147
+ this.write({ event: "error", message, ...extra });
148
+ }
149
+ else {
150
+ this.echo(colorize(` ${message}`, "red"));
151
+ }
152
+ }
153
+ /**
154
+ * Emit a status transition (e.g. when a revision moves to a new phase),
155
+ * annotated with elapsed time.
156
+ *
157
+ * @param status - The new status string.
158
+ * @param elapsedSeconds - Seconds elapsed since polling began.
159
+ */
160
+ statusChange(status, elapsedSeconds) {
161
+ const elapsed = formatElapsed(elapsedSeconds);
162
+ if (this.json) {
163
+ this.write({
164
+ event: "status_change",
165
+ status,
166
+ elapsed_seconds: Math.round(elapsedSeconds * 10) / 10,
167
+ message: `${status}... (${elapsed})`,
168
+ });
169
+ }
170
+ else {
171
+ this.echo(` ${status}... (${elapsed})`);
172
+ }
173
+ }
174
+ /**
175
+ * Emit a streamed build/log line.
176
+ *
177
+ * @param message - The raw log line.
178
+ */
179
+ log(message) {
180
+ if (this.json) {
181
+ this.write({ event: "log", message });
182
+ }
183
+ else {
184
+ this.echo(` | ${message}`);
185
+ }
186
+ }
187
+ /**
188
+ * Emit the LangSmith dashboard URL where deployment status can be viewed.
189
+ *
190
+ * @param url - The dashboard URL.
191
+ */
192
+ statusUrl(url) {
193
+ if (this.json) {
194
+ this.write({ event: "status_url", url });
195
+ }
196
+ else {
197
+ this.echo(colorize(` View status: ${url}`, "cyan"));
198
+ }
199
+ }
200
+ /**
201
+ * Emit a periodic heartbeat while a long-running status is unchanged.
202
+ *
203
+ * @remarks
204
+ * Only emitted in JSON mode (text mode relies on the live spinner instead).
205
+ *
206
+ * @param status - The current status string.
207
+ * @param elapsedSeconds - Seconds elapsed since polling began.
208
+ */
209
+ heartbeat(status, elapsedSeconds) {
210
+ if (!this.json)
211
+ return;
212
+ this.write({
213
+ event: "heartbeat",
214
+ status,
215
+ elapsed_seconds: Math.round(elapsedSeconds * 10) / 10,
216
+ message: `${status}... (${formatElapsed(elapsedSeconds)})`,
217
+ });
218
+ }
219
+ /**
220
+ * Emit source-upload progress. In text mode this rewrites the current line
221
+ * in place; in JSON mode it emits a discrete progress event.
222
+ *
223
+ * @param sizeMb - Total upload size in MiB.
224
+ * @param pct - Completion percentage (0–100).
225
+ */
226
+ uploadProgress(sizeMb, pct) {
227
+ if (this.json) {
228
+ this.write({
229
+ event: "upload_progress",
230
+ size_mb: Math.round(sizeMb * 10) / 10,
231
+ pct,
232
+ });
233
+ }
234
+ else {
235
+ process.stdout.write(`\r Uploading (${sizeMb.toFixed(1)} MB)... ${pct}%`);
236
+ }
237
+ }
238
+ /**
239
+ * Emit the final deploy result.
240
+ *
241
+ * @param status - Terminal outcome (`succeeded`, `failed`, or `timed_out`).
242
+ * @param args - Result details.
243
+ * @param args.deploymentId - The deployment ID.
244
+ * @param args.url - Public deployment URL, when available.
245
+ * @param args.statusUrl - Dashboard status URL, when available.
246
+ * @param args.fallbackStatusMessage - Text-mode fallback shown on timeout
247
+ * when no `statusUrl` is available.
248
+ */
249
+ result(status, args) {
250
+ if (this.json) {
251
+ const message = status === "succeeded"
252
+ ? "Deployment successful!"
253
+ : status === "failed"
254
+ ? "Deployment failed"
255
+ : "Timed out waiting for deployment.";
256
+ const payload = {
257
+ event: "result",
258
+ status,
259
+ deployment_id: args.deploymentId,
260
+ message,
261
+ };
262
+ if (args.url)
263
+ payload.url = args.url;
264
+ if (args.statusUrl)
265
+ payload.status_url = args.statusUrl;
266
+ this.write(payload);
267
+ return;
268
+ }
269
+ if (status === "succeeded") {
270
+ this.echo(colorize(" Deployment successful!", "green"));
271
+ if (args.url)
272
+ this.echo(colorize(` URL: ${args.url}`, "green"));
273
+ if (args.statusUrl) {
274
+ this.echo(colorize(` View status: ${args.statusUrl}`, "green"));
275
+ }
276
+ }
277
+ else if (status === "failed") {
278
+ this.echo(colorize(" Deployment failed", "red"));
279
+ if (args.statusUrl) {
280
+ this.echo(colorize(` View status: ${args.statusUrl}`, "red"));
281
+ }
282
+ }
283
+ else {
284
+ this.echo(colorize(" Timed out waiting for deployment.", "yellow"));
285
+ if (args.statusUrl) {
286
+ this.echo(colorize(` Check status at: ${args.statusUrl}`, "yellow"));
287
+ }
288
+ else if (args.fallbackStatusMessage) {
289
+ this.echo(colorize(` ${args.fallbackStatusMessage}`, "yellow"));
290
+ }
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Minimal spinner used while polling/building. Renders an elapsed-time spinner
296
+ * to stdout when attached to a TTY; otherwise emits each message on its own
297
+ * line to stderr. In JSON mode it is a no-op.
298
+ */
299
+ export class Spinner {
300
+ /** Spinner animation frames cycled on each tick. */
301
+ static FRAMES = ["|", "/", "-", "\\"];
302
+ /** The active animation interval, or `undefined` when stopped. */
303
+ timer;
304
+ /** Index of the next animation frame to render. */
305
+ frameIndex = 0;
306
+ /** Current display message (empty hides the spinner). */
307
+ message;
308
+ /** Last non-empty message, used as the base for elapsed-time rendering. */
309
+ baseMessage;
310
+ /** Timestamp (ms) when the spinner was created, for elapsed display. */
311
+ start = Date.now();
312
+ /** Whether to append an elapsed-time suffix to the message. */
313
+ showElapsed;
314
+ /** Whether the spinner is disabled (JSON mode). */
315
+ jsonMode;
316
+ /**
317
+ * @param message - Initial message (does not start the spinner; call
318
+ * {@link set} to begin animating).
319
+ * @param options - Spinner options.
320
+ * @param options.elapsed - Append an elapsed-time suffix when `true`.
321
+ * @param options.jsonMode - Disable all output when `true`.
322
+ */
323
+ constructor(message, options = {}) {
324
+ this.message = message;
325
+ this.baseMessage = message;
326
+ this.showElapsed = options.elapsed ?? false;
327
+ this.jsonMode = options.jsonMode ?? false;
328
+ }
329
+ /** Render the current frame and message, overwriting the current line. */
330
+ render() {
331
+ const frame = Spinner.FRAMES[this.frameIndex % Spinner.FRAMES.length];
332
+ this.frameIndex += 1;
333
+ let text = this.message;
334
+ if (this.showElapsed && this.baseMessage) {
335
+ const seconds = (Date.now() - this.start) / 1000;
336
+ text = `${this.baseMessage} (${formatElapsed(seconds)})`;
337
+ }
338
+ process.stdout.write(`\r\u001b[K${frame} ${text}`);
339
+ }
340
+ /** Clear the current terminal line. */
341
+ clearLine() {
342
+ process.stdout.write("\r\u001b[K");
343
+ }
344
+ /**
345
+ * Update the spinner message, starting or stopping animation as needed.
346
+ *
347
+ * @remarks
348
+ * Starts the animation when given a non-empty message and stops it when
349
+ * given an empty string. On non-TTY stdout, each non-empty message is
350
+ * written to stderr instead of animating. No-op in JSON mode.
351
+ *
352
+ * @param message - New message, or an empty string to hide the spinner.
353
+ */
354
+ set(message) {
355
+ if (this.jsonMode)
356
+ return;
357
+ if (!process.stdout.isTTY) {
358
+ if (message)
359
+ process.stderr.write(`${message}\n`);
360
+ return;
361
+ }
362
+ this.message = message;
363
+ if (message)
364
+ this.baseMessage = message;
365
+ if (!message) {
366
+ this.clearLine();
367
+ if (this.timer) {
368
+ clearInterval(this.timer);
369
+ this.timer = undefined;
370
+ }
371
+ }
372
+ else if (!this.timer) {
373
+ this.timer = setInterval(() => this.render(), 100);
374
+ }
375
+ }
376
+ /** Stop the animation and clear the spinner line. */
377
+ stop() {
378
+ if (this.timer) {
379
+ clearInterval(this.timer);
380
+ this.timer = undefined;
381
+ }
382
+ if (!this.jsonMode && process.stdout.isTTY) {
383
+ this.clearLine();
384
+ }
385
+ }
386
+ }
387
+ export { formatElapsed };