@cargo-ai/cli 1.0.86 → 1.0.88

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,21 @@
1
+ /**
2
+ * What the spinner says while a turn is thinking rather than running a tool.
3
+ *
4
+ * One fixed word for every wait in every turn reads as a progress bar that is
5
+ * not moving — the same three syllables whether the answer took a second or a
6
+ * minute, and no way to tell a turn that is working from one that is stuck.
7
+ * A word that changes as the turn moves through its steps says the session is
8
+ * alive, which is the only thing a spinner can honestly report.
9
+ *
10
+ * Short, gerunds, and unambiguous about the fact that nothing is being done to
11
+ * anything yet. A few are freighted on purpose.
12
+ */
13
+ export declare const ASK_THINKING_WORDS: readonly string[];
14
+ /**
15
+ * A word for the next wait, never the one just used.
16
+ *
17
+ * Repeating straight after itself is the one draw that looks like the spinner
18
+ * is stuck rather than moving on, which is the whole thing this avoids.
19
+ */
20
+ export declare function pickThinkingWord(previous?: string): string;
21
+ //# sourceMappingURL=thinking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/thinking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAyB/C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAc1D"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What the spinner says while a turn is thinking rather than running a tool.
3
+ *
4
+ * One fixed word for every wait in every turn reads as a progress bar that is
5
+ * not moving — the same three syllables whether the answer took a second or a
6
+ * minute, and no way to tell a turn that is working from one that is stuck.
7
+ * A word that changes as the turn moves through its steps says the session is
8
+ * alive, which is the only thing a spinner can honestly report.
9
+ *
10
+ * Short, gerunds, and unambiguous about the fact that nothing is being done to
11
+ * anything yet. A few are freighted on purpose.
12
+ */
13
+ export const ASK_THINKING_WORDS = [
14
+ "Thinking",
15
+ "Pondering",
16
+ "Mulling",
17
+ "Musing",
18
+ "Noodling",
19
+ "Percolating",
20
+ "Ruminating",
21
+ "Puzzling",
22
+ "Deliberating",
23
+ "Considering",
24
+ "Weighing",
25
+ "Untangling",
26
+ "Wrangling",
27
+ "Churning",
28
+ "Brewing",
29
+ "Digging",
30
+ "Rummaging",
31
+ "Scheming",
32
+ "Plotting",
33
+ "Marshalling",
34
+ "Manifesting",
35
+ "Stowing",
36
+ "Loading",
37
+ "Hauling",
38
+ ];
39
+ /**
40
+ * A word for the next wait, never the one just used.
41
+ *
42
+ * Repeating straight after itself is the one draw that looks like the spinner
43
+ * is stuck rather than moving on, which is the whole thing this avoids.
44
+ */
45
+ export function pickThinkingWord(previous) {
46
+ // Callers hand back what the spinner was showing, ellipsis and all, while
47
+ // the list holds bare words. Comparing the two as they come never matches,
48
+ // and the word repeats as often as chance allows.
49
+ const last = previous === undefined ? undefined : previous.replace(/…$/, "");
50
+ const choices = last === undefined
51
+ ? ASK_THINKING_WORDS
52
+ : ASK_THINKING_WORDS.filter((word) => word !== last);
53
+ const pool = choices.length === 0 ? ASK_THINKING_WORDS : choices;
54
+ const index = Math.floor(Math.random() * pool.length);
55
+ const word = pool[index];
56
+ return `${word === undefined ? "Thinking" : word}…`;
57
+ }
@@ -0,0 +1,67 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /**
3
+ * What the user typed while a turn was running.
4
+ *
5
+ * `submitted` is a line they finished with Enter, meant to be sent as the next
6
+ * question without stopping at the prompt. `pending` is everything typed
7
+ * behind it — a line they were still in the middle of, or a second one they
8
+ * did finish, which is one question too many to queue — and prefills the
9
+ * prompt instead.
10
+ */
11
+ export type TypeAhead = {
12
+ submitted: string | undefined;
13
+ pending: string;
14
+ };
15
+ export declare const EMPTY_TYPE_AHEAD: TypeAhead;
16
+ export type TypeAheadCollector = {
17
+ /**
18
+ * Hand stdin back, so a prompt inside the turn can read it itself.
19
+ *
20
+ * Resolves after a short settle during which keystrokes are read and thrown
21
+ * away, rather than releasing the terminal on the spot. A tool approval
22
+ * appears without warning under whatever the user is already typing, and it
23
+ * defaults to Yes — so an Enter meant to send the queued question would
24
+ * otherwise land on the picker and approve a shell command nobody read.
25
+ * Anything typed during the settle is discarded, which is the right reading
26
+ * of a keystroke aimed at a prompt that was not on screen yet.
27
+ */
28
+ pause: () => Promise<void>;
29
+ /** Take it back once that prompt is done. */
30
+ resume: () => void;
31
+ /** What has been typed so far, for the caller to draw under the spinner. */
32
+ current: () => string;
33
+ /** Stop collecting and report what was typed. */
34
+ stop: () => TypeAhead;
35
+ };
36
+ /**
37
+ * Collect keystrokes while a turn is running, instead of letting the terminal
38
+ * queue them and then lose them.
39
+ *
40
+ * Left alone, the tty stays in canonical mode through a turn: it echoes what
41
+ * is typed, buffers the line, and whether any of it survives the switch to raw
42
+ * mode when the Ask prompt reopens is a race — sometimes the line arrived,
43
+ * sometimes it vanished after being echoed, which is the worst of both. Owning
44
+ * the stream for the whole turn makes it deterministic, and lets a question
45
+ * typed during a reply be waiting when the reply ends.
46
+ *
47
+ * Raw mode is also what makes Ctrl-C reliable here. In canonical mode the
48
+ * terminal turns it into a SIGINT for the whole foreground process group,
49
+ * which is delivered to whatever child happens to own the terminal — during a
50
+ * Claude Code start that is Claude Code, which dies without this process ever
51
+ * hearing about it. As a keypress it always arrives, and `onInterrupt` can
52
+ * unwind the turn properly.
53
+ */
54
+ export declare function collectTypeAhead(input: {
55
+ stream: NodeJS.ReadableStream;
56
+ /** Ctrl-C. Called on the keypress; the caller decides what it means. */
57
+ onInterrupt: () => void;
58
+ /** Called whenever the typed text changes, so it can be redrawn. */
59
+ onChange?: (text: string) => void;
60
+ /**
61
+ * The tail of an earlier type-ahead whose first line was already submitted.
62
+ * Starting from it keeps what was typed behind that question alive across
63
+ * the turn its own Enter kicked off.
64
+ */
65
+ initialLine?: string;
66
+ }): TypeAheadCollector;
67
+ //# sourceMappingURL=typeAhead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeAhead.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/typeAhead.ts"],"names":[],"mappings":";AAGA;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAG9B,CAAC;AA4BF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,iDAAiD;IACjD,IAAI,EAAE,MAAM,SAAS,CAAC;CACvB,CAAC;AAgBF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,wEAAwE;IACxE,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,kBAAkB,CAqJrB"}
@@ -0,0 +1,169 @@
1
+ import * as readline from "node:readline";
2
+ export const EMPTY_TYPE_AHEAD = {
3
+ submitted: undefined,
4
+ pending: "",
5
+ };
6
+ /**
7
+ * How long keystrokes are swallowed before a prompt inside a turn takes the
8
+ * keyboard.
9
+ *
10
+ * Long enough to catch the key already travelling when the prompt was decided
11
+ * on, short enough that nobody waiting for the question notices. The whole
12
+ * budget is spent before a human could have read what they are being asked, so
13
+ * nothing legitimate is lost.
14
+ *
15
+ * `CARGO_ASK_APPROVAL_SETTLE_MS` widens it for the end-to-end test that has to
16
+ * type into the window on purpose.
17
+ */
18
+ const APPROVAL_SETTLE_MS = readSettleMs();
19
+ function readSettleMs() {
20
+ const raw = process.env["CARGO_ASK_APPROVAL_SETTLE_MS"];
21
+ if (raw === undefined) {
22
+ return 250;
23
+ }
24
+ const parsed = Number(raw);
25
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 250;
26
+ }
27
+ function isTTYStream(stream) {
28
+ return (typeof stream.setRawMode === "function" &&
29
+ stream.isTTY === true);
30
+ }
31
+ /**
32
+ * Collect keystrokes while a turn is running, instead of letting the terminal
33
+ * queue them and then lose them.
34
+ *
35
+ * Left alone, the tty stays in canonical mode through a turn: it echoes what
36
+ * is typed, buffers the line, and whether any of it survives the switch to raw
37
+ * mode when the Ask prompt reopens is a race — sometimes the line arrived,
38
+ * sometimes it vanished after being echoed, which is the worst of both. Owning
39
+ * the stream for the whole turn makes it deterministic, and lets a question
40
+ * typed during a reply be waiting when the reply ends.
41
+ *
42
+ * Raw mode is also what makes Ctrl-C reliable here. In canonical mode the
43
+ * terminal turns it into a SIGINT for the whole foreground process group,
44
+ * which is delivered to whatever child happens to own the terminal — during a
45
+ * Claude Code start that is Claude Code, which dies without this process ever
46
+ * hearing about it. As a keypress it always arrives, and `onInterrupt` can
47
+ * unwind the turn properly.
48
+ */
49
+ export function collectTypeAhead(input) {
50
+ const { stream } = input;
51
+ let line = input.initialLine === undefined ? "" : input.initialLine;
52
+ let submitted;
53
+ let stopped = false;
54
+ let listening = false;
55
+ let discarding = false;
56
+ const wasRaw = isTTYStream(stream) ? stream.isRaw === true : false;
57
+ const onKeypress = (str, key) => {
58
+ if (stopped === true) {
59
+ return;
60
+ }
61
+ // Mid-settle: the terminal is about to belong to a prompt that is not up
62
+ // yet, so nothing typed now is aimed at anything. Ctrl-C included — the
63
+ // prompt taking over has its own, and cancelling the turn out from under
64
+ // a question already being asked is not what the press meant either.
65
+ if (discarding === true) {
66
+ return;
67
+ }
68
+ if (key.ctrl === true && key.name === "c") {
69
+ input.onInterrupt();
70
+ return;
71
+ }
72
+ if (key.name === "return" || key.name === "enter") {
73
+ // Only the first finished line is carried forward as a question. A
74
+ // second Enter would mean two turns queued behind one reply, which is
75
+ // more than the user can be shown before it happens.
76
+ //
77
+ // Left alone rather than swallowed, though: the line behind a submitted
78
+ // question is the tail that prefills the next prompt, and it is drawn
79
+ // under a ◆ like any other, so Enter is the obvious way to finish it.
80
+ // Clearing it here destroyed the one thing that is meant to survive the
81
+ // turn its own Enter started.
82
+ if (submitted !== undefined) {
83
+ return;
84
+ }
85
+ const trimmed = line.trim();
86
+ if (trimmed.length > 0) {
87
+ submitted = trimmed;
88
+ }
89
+ line = "";
90
+ report();
91
+ return;
92
+ }
93
+ if (key.name === "backspace") {
94
+ if (line.length > 0) {
95
+ line = line.slice(0, -1);
96
+ report();
97
+ }
98
+ return;
99
+ }
100
+ if (str === undefined || str.length === 0) {
101
+ return;
102
+ }
103
+ if (key.ctrl === true || key.meta === true) {
104
+ return;
105
+ }
106
+ const code = str.codePointAt(0);
107
+ if (code === undefined || code < 32 || code === 127) {
108
+ return;
109
+ }
110
+ line = `${line}${str}`;
111
+ report();
112
+ };
113
+ function report() {
114
+ if (input.onChange !== undefined) {
115
+ input.onChange(line);
116
+ }
117
+ }
118
+ function listen() {
119
+ if (listening === true || stopped === true) {
120
+ return;
121
+ }
122
+ if (isTTYStream(stream) === true) {
123
+ stream.setRawMode(true);
124
+ }
125
+ // An interface closed by a prompt earlier in the turn leaves the stream
126
+ // paused, and a paused stream does not start flowing again just because a
127
+ // listener was added.
128
+ stream.resume();
129
+ readline.emitKeypressEvents(stream);
130
+ stream.on("keypress", onKeypress);
131
+ listening = true;
132
+ }
133
+ function unlisten() {
134
+ if (listening === false) {
135
+ return;
136
+ }
137
+ stream.off("keypress", onKeypress);
138
+ if (isTTYStream(stream) === true) {
139
+ stream.setRawMode(wasRaw);
140
+ }
141
+ listening = false;
142
+ }
143
+ listen();
144
+ return {
145
+ pause: async () => {
146
+ // Keeps reading throughout, so the keystroke already on its way in when
147
+ // the prompt was decided on is swallowed here rather than queued in the
148
+ // terminal for whatever reads next.
149
+ discarding = true;
150
+ await new Promise((resolve) => {
151
+ setTimeout(resolve, APPROVAL_SETTLE_MS);
152
+ });
153
+ unlisten();
154
+ },
155
+ resume: () => {
156
+ discarding = false;
157
+ listen();
158
+ },
159
+ current: () => line,
160
+ stop: () => {
161
+ if (stopped === true) {
162
+ return { submitted, pending: line };
163
+ }
164
+ stopped = true;
165
+ unlisten();
166
+ return { submitted, pending: line };
167
+ },
168
+ };
169
+ }
@@ -24,9 +24,16 @@ type FailWithOpts = {
24
24
  extra?: Record<string, unknown>;
25
25
  };
26
26
  export declare function failWith(message: string, opts?: FailWithOpts): never;
27
+ /**
28
+ * Cut a plain line down to one screen row, with an ellipsis where it was cut.
29
+ *
30
+ * Exported for tests.
31
+ */
32
+ export declare function clipToWidth(text: string, columns?: number): string;
27
33
  export type Spinner = {
28
34
  update: (message: string) => void;
29
35
  log: (line: string) => void;
36
+ footer: (text: string) => void;
30
37
  stop: () => void;
31
38
  };
32
39
  export declare function startSpinner(message: string): Spinner;
@@ -1 +1 @@
1
- {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAclE,eAAO,MAAM,MAAM;aACR,MAAM,KAAG,MAAM;eACb,MAAM,KAAG,MAAM;gBACd,MAAM,KAAG,MAAM;cACjB,MAAM,KAAG,MAAM;aAChB,MAAM,KAAG,MAAM;cACd,MAAM,KAAG,MAAM;CAC1B,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAM/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAID,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,KAAK,YAAY,GAAG;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CAgCpE;AAKD,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAuBrD;AAkDD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAQhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ1E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxD;AAKD,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAgEZ;AA0ED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
1
+ {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAclE,eAAO,MAAM,MAAM;aACR,MAAM,KAAG,MAAM;eACb,MAAM,KAAG,MAAM;gBACd,MAAM,KAAG,MAAM;cACjB,MAAM,KAAG,MAAM;aAChB,MAAM,KAAG,MAAM;cACd,MAAM,KAAG,MAAM;CAC1B,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAM/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAID,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,KAAK,YAAY,GAAG;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CAgCpE;AAQD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAiBlE;AAED,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAU5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBrD;AAgED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAQhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ1E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxD;AAKD,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAgEZ;AA0ED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
@@ -83,6 +83,27 @@ export function failWith(message, opts) {
83
83
  }
84
84
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
85
85
  const SPINNER_INTERVAL_MS = 80;
86
+ /** Assumed width when the terminal will not say — the conventional default. */
87
+ const FALLBACK_COLUMNS = 80;
88
+ /**
89
+ * Cut a plain line down to one screen row, with an ellipsis where it was cut.
90
+ *
91
+ * Exported for tests.
92
+ */
93
+ export function clipToWidth(text, columns) {
94
+ const width = columns !== undefined
95
+ ? columns
96
+ : typeof process.stderr.columns === "number" && process.stderr.columns > 0
97
+ ? process.stderr.columns
98
+ : FALLBACK_COLUMNS;
99
+ // One column short of the edge: writing into the last cell makes some
100
+ // terminals wrap to the next row, which is the thing being avoided.
101
+ const limit = width - 1;
102
+ if (limit <= 1 || text.length <= limit) {
103
+ return text;
104
+ }
105
+ return `${text.slice(0, limit - 1)}…`;
106
+ }
86
107
  let activeSpinner;
87
108
  export function startSpinner(message) {
88
109
  // Only one spinner can own the line, so retire any predecessor: otherwise its
@@ -94,6 +115,7 @@ export function startSpinner(message) {
94
115
  const tracked = {
95
116
  update: spinner.update,
96
117
  log: spinner.log,
118
+ footer: spinner.footer,
97
119
  stop: () => {
98
120
  if (activeSpinner === tracked) {
99
121
  activeSpinner = undefined;
@@ -112,17 +134,27 @@ function createSpinner(message) {
112
134
  return {
113
135
  update: (m) => process.stderr.write(`${m}\n`),
114
136
  log: (line) => process.stderr.write(`${line}\n`),
137
+ // Nothing is being redrawn, so there is no line to keep in step.
138
+ footer: () => undefined,
115
139
  stop: () => undefined,
116
140
  };
117
141
  }
118
142
  process.stderr.write("\x1B[?25l");
119
143
  let current = message;
120
144
  let frame = 0;
145
+ let footerLine = "";
121
146
  const render = () => {
122
147
  const ch = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
123
148
  if (ch !== undefined) {
124
- // \x1B[K clears any stale characters when the message shrinks.
125
- process.stderr.write(`\r\x1B[K${colors.cyan(ch)} ${colors.dim(current)}`);
149
+ // \x1B[J clears the spinner line *and* any footer under it, so a footer
150
+ // that just shrank or went away leaves nothing behind.
151
+ process.stderr.write(`\r\x1B[J${colors.cyan(ch)} ${colors.dim(current)}`);
152
+ // Drawn below, then the cursor comes back up: the next frame's \r has to
153
+ // land on the spinner line, not the footer. Clipped first, because that
154
+ // \x1B[1A is only right while the footer fits on one row.
155
+ if (footerLine !== "") {
156
+ process.stderr.write(`\n${colors.dim(clipToWidth(footerLine))}\x1B[1A`);
157
+ }
126
158
  }
127
159
  frame += 1;
128
160
  };
@@ -135,7 +167,10 @@ function createSpinner(message) {
135
167
  log: (line) => {
136
168
  // Wipe the spinner line, drop a permanent line, let the next frame redraw
137
169
  // the spinner beneath it.
138
- process.stderr.write(`\r\x1B[K${line}\n`);
170
+ process.stderr.write(`\r\x1B[J${line}\n`);
171
+ },
172
+ footer: (line) => {
173
+ footerLine = line;
139
174
  },
140
175
  stop: () => {
141
176
  if (stopped) {
@@ -143,7 +178,7 @@ function createSpinner(message) {
143
178
  }
144
179
  stopped = true;
145
180
  clearInterval(id);
146
- process.stderr.write("\r\x1B[K\x1B[?25h");
181
+ process.stderr.write("\r\x1B[J\x1B[?25h");
147
182
  },
148
183
  };
149
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cargo-ai/cli",
3
- "version": "1.0.86",
3
+ "version": "1.0.88",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://getcargo.ai",
@@ -55,8 +55,9 @@
55
55
  "build": "tsc --build",
56
56
  "dev": "tsx watch src/index.ts",
57
57
  "start": "node build/index.js",
58
- "test": "npm run build && node --test \"test/**/*.test.mjs\"",
59
- "test:watch": "node --test --watch \"test/**/*.test.mjs\"",
58
+ "test": "npm run build && node --test \"test/*.test.mjs\"",
59
+ "test:watch": "node --test --watch \"test/*.test.mjs\"",
60
+ "test:e2e": "npm run build && node --test --test-concurrency=1 \"test/e2e/*.e2e.mjs\"",
60
61
  "type:check": "tsc --noEmit",
61
62
  "lint": "eslint ./ --fix",
62
63
  "lint:check": "eslint ./ --max-warnings=0",
@@ -68,7 +69,7 @@
68
69
  "@ai-sdk/harness-claude-code": "^1.0.99",
69
70
  "@cargo-ai/api": "^1.0.76",
70
71
  "@cargo-ai/app-sdk": "^1.0.10",
71
- "@cargo-ai/cdk": "^1.0.70",
72
+ "@cargo-ai/cdk": "^1.0.71",
72
73
  "@cargo-ai/types": "^1.0.73",
73
74
  "@cargo-ai/utils": "^1.0.28",
74
75
  "@cargo-ai/worker-sdk": "^1.0.17",
@@ -85,6 +86,7 @@
85
86
  "@cargo-ai/ts-config": "*",
86
87
  "@types/node": "^20.10.8",
87
88
  "eslint": "9.26.0",
89
+ "node-pty": "^1.0.0",
88
90
  "prettier": "3.3.3",
89
91
  "typescript": "5.3.2"
90
92
  }