@cargo-ai/cli 1.0.55 → 1.0.57
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.
- package/README.md +1 -1
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +25 -5
- package/build/commands/orchestration/asciiBlocks.d.ts +42 -0
- package/build/commands/orchestration/asciiBlocks.d.ts.map +1 -0
- package/build/commands/orchestration/asciiBlocks.js +170 -0
- package/build/commands/orchestration/node.d.ts.map +1 -1
- package/build/commands/orchestration/node.js +131 -18
- package/build/commands/orchestration/nodeDiagram.d.ts +42 -0
- package/build/commands/orchestration/nodeDiagram.d.ts.map +1 -1
- package/build/commands/orchestration/nodeDiagram.js +123 -40
- package/build/commands/orchestration/nodeDiagramAscii.d.ts +32 -0
- package/build/commands/orchestration/nodeDiagramAscii.d.ts.map +1 -0
- package/build/commands/orchestration/nodeDiagramAscii.js +528 -0
- package/build/credentials.d.ts +1 -1
- package/build/credentials.d.ts.map +1 -1
- package/build/credentials.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// Render a workflow node graph as a centred ASCII flow, for terminals.
|
|
2
|
+
//
|
|
3
|
+
// Mermaid is the right answer when the output lands somewhere that renders it —
|
|
4
|
+
// a PR, the docs, a published page. It is the wrong answer in a terminal, where
|
|
5
|
+
// it is just source code the reader has to compile in their head. This module
|
|
6
|
+
// draws the same graph as text a person can read where they are standing.
|
|
7
|
+
//
|
|
8
|
+
// Why not shell out to one of the mermaid-to-ASCII renderers: they re-parse the
|
|
9
|
+
// diagram and auto-route edges on a character grid, which collides as soon as a
|
|
10
|
+
// graph has crossings. Measured on the 29-node `Find email` waterfall, they
|
|
11
|
+
// produce rails drawn straight through node boxes, doubled edge labels
|
|
12
|
+
// (`├─noo►`), and junction characters overwriting label text (`Email┴valid?`) —
|
|
13
|
+
// a picture that reads as edges the workflow does not have. We never have to
|
|
14
|
+
// infer layout, because we hold the graph: this renderer lays a spine down the
|
|
15
|
+
// centre and hangs rails off it, so an edge it draws is an edge that exists.
|
|
16
|
+
//
|
|
17
|
+
// The shape it draws, and why:
|
|
18
|
+
//
|
|
19
|
+
// * A **spine** runs down the centre. One child continues it; the others peel
|
|
20
|
+
// off as rails.
|
|
21
|
+
// * A branch whose paths reconverge is a **detour**: the rail leaves at `├──┐`
|
|
22
|
+
// and rejoins at `├──┘`, and the spine keeps its identity across the split.
|
|
23
|
+
// * A branch whose paths never reconverge is a **fork**: `┌──┴──┐`, columns
|
|
24
|
+
// side by side, no spine survives.
|
|
25
|
+
//
|
|
26
|
+
// Which child holds the spine is the whole readability question, and it is not
|
|
27
|
+
// the same answer in both cases — see `chooseSpine`.
|
|
28
|
+
import { displayWidth, emptyBlock, frame, hstack, indent, padColumn, rail, spineBlock, teeAt, textBlock, vstack, } from "./asciiBlocks.js";
|
|
29
|
+
import { continuesOnFailure, findStart, graphWarnings, indexByUuid, isEnd, matchesNode, outgoingEdges, ROUTING_ACTIONS, } from "./nodeDiagram.js";
|
|
30
|
+
export { displayWidth };
|
|
31
|
+
/**
|
|
32
|
+
* Where the incoming line meets the split. A detour lands on the column that
|
|
33
|
+
* keeps the spine; a fork lands between the columns, snapped onto one when it
|
|
34
|
+
* falls beside it — `┴` and `┬` in adjacent cells read as a smudge rather than
|
|
35
|
+
* a junction.
|
|
36
|
+
*/
|
|
37
|
+
function columnAnchor(spines, spineIndex, midpoint) {
|
|
38
|
+
if (spineIndex !== undefined) {
|
|
39
|
+
const at = spines[spineIndex];
|
|
40
|
+
return at === undefined ? 0 : at;
|
|
41
|
+
}
|
|
42
|
+
const nearest = spines.find((at) => Math.abs(at - midpoint) <= 1);
|
|
43
|
+
return nearest === undefined ? midpoint : nearest;
|
|
44
|
+
}
|
|
45
|
+
/** Whether a split column is still carrying the flow. */
|
|
46
|
+
function isColumnOpen(columns, index) {
|
|
47
|
+
if (index === undefined)
|
|
48
|
+
return false;
|
|
49
|
+
const column = columns[index];
|
|
50
|
+
return column === undefined ? false : column.open;
|
|
51
|
+
}
|
|
52
|
+
/** A node's outgoing edges, or none when it has no entry in the graph. */
|
|
53
|
+
function edgesFrom(graph, uuid) {
|
|
54
|
+
const edges = graph.out.get(uuid);
|
|
55
|
+
return edges === undefined ? [] : edges;
|
|
56
|
+
}
|
|
57
|
+
/** What to call a node when nothing better is available. */
|
|
58
|
+
function fallbackLabel(node) {
|
|
59
|
+
return node.slug === null || node.slug === undefined ? node.uuid : node.slug;
|
|
60
|
+
}
|
|
61
|
+
function buildGraph(nodes) {
|
|
62
|
+
const byUuid = indexByUuid(nodes);
|
|
63
|
+
const out = new Map();
|
|
64
|
+
for (const node of nodes)
|
|
65
|
+
out.set(node.uuid, outgoingEdges(node, byUuid));
|
|
66
|
+
return { byUuid, out, reach: new Map() };
|
|
67
|
+
}
|
|
68
|
+
/** Everything reachable from `uuid`, itself excluded. Cycle-safe, memoised. */
|
|
69
|
+
function reachable(graph, uuid) {
|
|
70
|
+
const cached = graph.reach.get(uuid);
|
|
71
|
+
if (cached !== undefined)
|
|
72
|
+
return cached;
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const stack = edgesFrom(graph, uuid).map((edge) => edge.to);
|
|
75
|
+
while (stack.length > 0) {
|
|
76
|
+
const next = stack.pop();
|
|
77
|
+
if (next === undefined || seen.has(next))
|
|
78
|
+
continue;
|
|
79
|
+
seen.add(next);
|
|
80
|
+
for (const edge of edgesFrom(graph, next))
|
|
81
|
+
stack.push(edge.to);
|
|
82
|
+
}
|
|
83
|
+
// A descendant pointing back adds the node to its own set. `chooseSpine`
|
|
84
|
+
// weighs arms by these sizes, so one extra entry is enough to hand the spine
|
|
85
|
+
// to an arm that should have stayed half of a fork.
|
|
86
|
+
seen.delete(uuid);
|
|
87
|
+
graph.reach.set(uuid, seen);
|
|
88
|
+
return seen;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Where a branch's paths come back together: the first node reachable from
|
|
92
|
+
* every child, in one child's own reach order so the choice is deterministic.
|
|
93
|
+
*
|
|
94
|
+
* A merge on an `end` node does not count. Every path in a workflow ends there,
|
|
95
|
+
* so treating it as a reconvergence would wrap the entire graph in one split
|
|
96
|
+
* and nest every later branch inside it.
|
|
97
|
+
*/
|
|
98
|
+
function mergePoint(graph, children) {
|
|
99
|
+
const [first, ...rest] = children;
|
|
100
|
+
if (first === undefined || rest.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
// `reachable` hands back its memoised set, so the child itself is compared
|
|
103
|
+
// separately rather than added to it. Adding would poison the cache, and
|
|
104
|
+
// `chooseSpine` weighs branches by exactly those set sizes — an entry that
|
|
105
|
+
// grew by one flips the `best >= second * 2 + 1` test and picks the wrong
|
|
106
|
+
// arm to carry the spine.
|
|
107
|
+
const others = rest.map((child) => ({
|
|
108
|
+
child,
|
|
109
|
+
reaches: reachable(graph, child),
|
|
110
|
+
}));
|
|
111
|
+
const candidates = [first, ...reachable(graph, first)];
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if (isEnd(graph.byUuid.get(candidate)))
|
|
114
|
+
continue;
|
|
115
|
+
if (others.every(({ child, reaches }) => child === candidate || reaches.has(candidate))) {
|
|
116
|
+
return candidate;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Which child keeps the spine — the one judgement call in the layout, and the
|
|
123
|
+
* answer differs by branch kind:
|
|
124
|
+
*
|
|
125
|
+
* * **Detour** (the paths reconverge): the spine is the *straight-through*
|
|
126
|
+
* path, so source order wins and child 0 keeps it. A "not found → look it
|
|
127
|
+
* up another way → carry on" branch then reads as a short excursion off a
|
|
128
|
+
* continuous main line, which is what it is.
|
|
129
|
+
*
|
|
130
|
+
* * **Fork** (they never reconverge): source order is meaningless, because
|
|
131
|
+
* the two sides are not a main line and an excursion. The path with more
|
|
132
|
+
* work left in it is the main line. In a provider waterfall the "yes" exit
|
|
133
|
+
* is two nodes and the "no" carries the remaining twenty, so weighting by
|
|
134
|
+
* what is left keeps the waterfall vertical instead of stair-stepping it
|
|
135
|
+
* sideways once per stage.
|
|
136
|
+
*
|
|
137
|
+
* The fork rule only applies when one side is decisively bigger. Two ends of
|
|
138
|
+
* comparable weight are a genuine fork and get drawn symmetrically.
|
|
139
|
+
*/
|
|
140
|
+
function chooseSpine(graph, edges, merged) {
|
|
141
|
+
if (merged)
|
|
142
|
+
return 0;
|
|
143
|
+
const weights = edges.map((edge) => reachable(graph, edge.to).size + 1);
|
|
144
|
+
let best = 0;
|
|
145
|
+
let second = 0;
|
|
146
|
+
let bestIndex = 0;
|
|
147
|
+
weights.forEach((weight, index) => {
|
|
148
|
+
if (weight > best) {
|
|
149
|
+
second = best;
|
|
150
|
+
best = weight;
|
|
151
|
+
bestIndex = index;
|
|
152
|
+
}
|
|
153
|
+
else if (weight > second) {
|
|
154
|
+
second = weight;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return best >= second * SPINE_DOMINANCE + 1 ? bestIndex : undefined;
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Layout
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/** Past this the drawing wraps in an ordinary terminal and stops being one. */
|
|
163
|
+
const TERMINAL_WIDTH = 100;
|
|
164
|
+
/**
|
|
165
|
+
* How much bigger one arm of a non-reconverging branch must be before it is
|
|
166
|
+
* treated as the main line rather than one of two equal ends.
|
|
167
|
+
*/
|
|
168
|
+
const SPINE_DOMINANCE = 2;
|
|
169
|
+
const GAP = 5;
|
|
170
|
+
/**
|
|
171
|
+
* `enrichPerson` → `Enrich person`. Only ever a fallback: the catalog's own name
|
|
172
|
+
* is better, and this is what a graph rendered without one falls back to.
|
|
173
|
+
* Splitting on case is safe for the slugs the platform mints; it deliberately
|
|
174
|
+
* does not touch integration slugs, where guessing turns `apolloio` into
|
|
175
|
+
* something that is neither the slug nor the product.
|
|
176
|
+
*/
|
|
177
|
+
function prettyAction(slug) {
|
|
178
|
+
const words = slug
|
|
179
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
180
|
+
.replace(/[_-]+/g, " ")
|
|
181
|
+
.trim()
|
|
182
|
+
.toLowerCase();
|
|
183
|
+
const first = words[0];
|
|
184
|
+
return first === undefined ? slug : first.toUpperCase() + words.slice(1);
|
|
185
|
+
}
|
|
186
|
+
function marks(node, context) {
|
|
187
|
+
let suffix = "";
|
|
188
|
+
if (continuesOnFailure(node))
|
|
189
|
+
suffix += " ↷";
|
|
190
|
+
if (matchesNode(node, context.paid))
|
|
191
|
+
suffix += " 💳";
|
|
192
|
+
if (matchesNode(node, context.highlight))
|
|
193
|
+
suffix += " ←";
|
|
194
|
+
return suffix;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The two label lines: what system the step runs on, then what it does. That
|
|
198
|
+
* ordering is deliberate — scanning a flow, the reader wants the provider first
|
|
199
|
+
* ("Apollo", "Slack") and the specifics second.
|
|
200
|
+
*/
|
|
201
|
+
function nodeLines(node, context) {
|
|
202
|
+
const suffix = marks(node, context);
|
|
203
|
+
const action = node.actionSlug === null || node.actionSlug === undefined
|
|
204
|
+
? ""
|
|
205
|
+
: node.actionSlug;
|
|
206
|
+
if (node.kind === "native" && (action === "start" || action === "end")) {
|
|
207
|
+
return [`${fallbackLabel(node)}${suffix}`];
|
|
208
|
+
}
|
|
209
|
+
const name = node.name === null || node.name === undefined
|
|
210
|
+
? undefined
|
|
211
|
+
: node.name.trim();
|
|
212
|
+
const book = context.names;
|
|
213
|
+
// The action's own name, as the platform publishes it. Falling back to the
|
|
214
|
+
// slug split into words keeps an offline drawing readable without this file
|
|
215
|
+
// inventing a name that disagrees with the product.
|
|
216
|
+
const published = book === undefined || book.native === undefined
|
|
217
|
+
? undefined
|
|
218
|
+
: book.native[action];
|
|
219
|
+
const nativeName = published === undefined ? prettyAction(action) : published;
|
|
220
|
+
// A routing step's second line would read "Branch" under every one of them,
|
|
221
|
+
// so it gets one line. The labelled rails leaving it already say it routes.
|
|
222
|
+
if (node.kind === "native" && ROUTING_ACTIONS.has(action)) {
|
|
223
|
+
const headline = name !== undefined && name.length > 0 ? name : nativeName;
|
|
224
|
+
return [`${headline}${suffix}`];
|
|
225
|
+
}
|
|
226
|
+
if (node.kind === "connector") {
|
|
227
|
+
const slug = node.integrationSlug === null || node.integrationSlug === undefined
|
|
228
|
+
? "connector"
|
|
229
|
+
: node.integrationSlug;
|
|
230
|
+
const named = book === undefined || book.integrations === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: book.integrations[slug];
|
|
233
|
+
const provider = named === undefined ? slug : named;
|
|
234
|
+
const perAction = book === undefined || book.actions === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: book.actions[slug];
|
|
237
|
+
const actionName = perAction === undefined ? undefined : perAction[action];
|
|
238
|
+
const what = name !== undefined && name.length > 0
|
|
239
|
+
? name
|
|
240
|
+
: actionName === undefined
|
|
241
|
+
? prettyAction(action)
|
|
242
|
+
: actionName;
|
|
243
|
+
return provider === what
|
|
244
|
+
? [`${provider}${suffix}`]
|
|
245
|
+
: [`${provider}${suffix}`, what];
|
|
246
|
+
}
|
|
247
|
+
const provider = node.kind === "tool"
|
|
248
|
+
? "Tool"
|
|
249
|
+
: node.kind === "agent"
|
|
250
|
+
? "Agent"
|
|
251
|
+
: nativeName;
|
|
252
|
+
const headline = name !== undefined && name.length > 0 ? name : node.slug;
|
|
253
|
+
if (headline === undefined ||
|
|
254
|
+
headline === null ||
|
|
255
|
+
headline === provider ||
|
|
256
|
+
headline === action) {
|
|
257
|
+
return [`${provider}${suffix}`];
|
|
258
|
+
}
|
|
259
|
+
return [`${provider}${suffix}`, headline];
|
|
260
|
+
}
|
|
261
|
+
function referenceBlock(node, context) {
|
|
262
|
+
// The step's most specific line — for a two-line step that is what it does,
|
|
263
|
+
// not the product it runs on, since the product repeats across steps.
|
|
264
|
+
const lines = nodeLines(node, context);
|
|
265
|
+
const specific = lines[lines.length - 1];
|
|
266
|
+
const label = specific ?? fallbackLabel(node);
|
|
267
|
+
return textBlock([`↑ ${label}`]);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A group's own graph, drawn inside a box rather than left to the reader's
|
|
271
|
+
* imagination. The steps live in `config._nodes` and form a complete graph of
|
|
272
|
+
* their own — start node included — so the same walk draws them one level down.
|
|
273
|
+
* Marks and names carry in; `drawn` does not, because an inner step repeating an
|
|
274
|
+
* outer slug is a different step.
|
|
275
|
+
*/
|
|
276
|
+
function subGraph(node, context) {
|
|
277
|
+
const config = node.config;
|
|
278
|
+
const nested = config === null || config === undefined ? undefined : config["_nodes"];
|
|
279
|
+
// No `_nodes` key at all means the graph came from a source that does not
|
|
280
|
+
// carry loop bodies; an empty one means the loop was left empty, which is a
|
|
281
|
+
// defect the drawing should not hide behind a tidy box.
|
|
282
|
+
if (!Array.isArray(nested))
|
|
283
|
+
return undefined;
|
|
284
|
+
const label = fallbackLabel(node);
|
|
285
|
+
if (nested.length === 0) {
|
|
286
|
+
context.notes.push(`${label}: the loop has no steps`);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const nodes = nested;
|
|
290
|
+
const start = findStart(nodes);
|
|
291
|
+
if (start === undefined)
|
|
292
|
+
return undefined;
|
|
293
|
+
const graph = buildGraph(nodes);
|
|
294
|
+
const inner = { ...context, graph, drawn: new Set() };
|
|
295
|
+
const { block } = layout(start.uuid, new Set(), inner);
|
|
296
|
+
for (const warning of graphWarnings(nodes, graph.byUuid, inner.drawn)) {
|
|
297
|
+
context.notes.push(`${label}: ${warning}`);
|
|
298
|
+
}
|
|
299
|
+
return frame(block, "per item");
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Draw the flow rooted at `uuid`, stopping when it reaches anything in `stop`.
|
|
303
|
+
* The returned block's spine is live at the bottom only when `open` is true —
|
|
304
|
+
* a caller merging rails back together needs to know whether there is anything
|
|
305
|
+
* left to merge.
|
|
306
|
+
*/
|
|
307
|
+
function layout(uuid, stop, context) {
|
|
308
|
+
if (stop.has(uuid))
|
|
309
|
+
return { block: emptyBlock(), open: true };
|
|
310
|
+
const node = context.graph.byUuid.get(uuid);
|
|
311
|
+
if (node === undefined)
|
|
312
|
+
return { block: emptyBlock(), open: false };
|
|
313
|
+
// An `end` is allowed to appear more than once: five paths finishing at `end`
|
|
314
|
+
// read better as five `end`s than as five pointers to one. Every other node
|
|
315
|
+
// is referenced on a second arrival, leaf or not — drawing a named step like
|
|
316
|
+
// "Send to #best-leads" twice reads as two separate sends.
|
|
317
|
+
const edges = edgesFrom(context.graph, uuid);
|
|
318
|
+
if (context.drawn.has(uuid) && !isEnd(node)) {
|
|
319
|
+
return { block: referenceBlock(node, context), open: false };
|
|
320
|
+
}
|
|
321
|
+
context.drawn.add(uuid);
|
|
322
|
+
const nested = subGraph(node, context);
|
|
323
|
+
const head = nested === undefined
|
|
324
|
+
? textBlock(nodeLines(node, context))
|
|
325
|
+
: vstack([textBlock(nodeLines(node, context)), spineBlock(), nested]);
|
|
326
|
+
if (edges.length === 0)
|
|
327
|
+
return { block: head, open: false };
|
|
328
|
+
if (edges.length === 1) {
|
|
329
|
+
const edge = edges[0];
|
|
330
|
+
if (edge === undefined)
|
|
331
|
+
return { block: head, open: false };
|
|
332
|
+
const tail = layout(edge.to, stop, context);
|
|
333
|
+
// A lone edge usually has no label and draws as a bare `│`. When it does
|
|
334
|
+
// carry one — a branch whose other arm is dangling, or a step whose only
|
|
335
|
+
// outgoing edge is its fallback — the label goes above the stem, the way
|
|
336
|
+
// every multi-edge column draws it, so it reads as an edge label rather
|
|
337
|
+
// than as the name of the step below it.
|
|
338
|
+
const stem = spineBlock(edge.dashed === true ? "┆" : "│");
|
|
339
|
+
const connector = edge.label === undefined ? stem : vstack([textBlock([edge.label]), stem]);
|
|
340
|
+
if (tail.block.lines.length === 0) {
|
|
341
|
+
return { block: vstack([head, connector]), open: true };
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
block: vstack([head, connector, tail.block]),
|
|
345
|
+
open: tail.open,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return branch(head, edges, stop, context);
|
|
349
|
+
}
|
|
350
|
+
function branch(head, edges, stop, context) {
|
|
351
|
+
// An elided routing node contributes no rows, and the step above it already
|
|
352
|
+
// drew the `│` leading in — adding another leaves a two-row gap above every
|
|
353
|
+
// split in a graph whose branches are unnamed.
|
|
354
|
+
const lead = head.lines.length === 0 ? emptyBlock() : spineBlock();
|
|
355
|
+
const merge = mergePoint(context.graph, edges.map((edge) => edge.to));
|
|
356
|
+
const spineIndex = chooseSpine(context.graph, edges, merge !== undefined);
|
|
357
|
+
const innerStop = new Set(stop);
|
|
358
|
+
if (merge !== undefined)
|
|
359
|
+
innerStop.add(merge);
|
|
360
|
+
// Columns are label-over-body so `hstack` sizes each one around whichever is
|
|
361
|
+
// wider, and an edge label can never collide with the node beneath it.
|
|
362
|
+
const columns = edges.map((edge) => {
|
|
363
|
+
const drawn = layout(edge.to, innerStop, context);
|
|
364
|
+
const char = edge.dashed === true ? "┆" : "│";
|
|
365
|
+
return {
|
|
366
|
+
block: vstack([
|
|
367
|
+
textBlock([edge.label === undefined ? char : edge.label]),
|
|
368
|
+
spineBlock(char),
|
|
369
|
+
drawn.block,
|
|
370
|
+
]),
|
|
371
|
+
open: drawn.open,
|
|
372
|
+
char,
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
const height = Math.max(...columns.map((column) => column.block.lines.length));
|
|
376
|
+
const padded = columns.map((column) => padColumn(column.block,
|
|
377
|
+
// A column still carrying the flow is drawn to the bottom of the split,
|
|
378
|
+
// so its spine is unbroken all the way to whatever joins it below. One
|
|
379
|
+
// that has ended stops where it ended — the difference is how the reader
|
|
380
|
+
// tells a path that continues from a path that terminates.
|
|
381
|
+
column.open ? height : column.block.lines.length, column.char));
|
|
382
|
+
// The spine column is drawn leftmost, whichever child holds it, so rails
|
|
383
|
+
// always peel off to the right. Ordering columns by edge index instead pushes
|
|
384
|
+
// the spine one indent further right at every nesting level — five stages of
|
|
385
|
+
// a waterfall then stair-step across 80 columns rather than staying in one
|
|
386
|
+
// vertical line. Labels ride their own rail, so nothing is mislabelled by the
|
|
387
|
+
// reordering; only the left-to-right order of the arms changes.
|
|
388
|
+
const order = spineIndex === undefined
|
|
389
|
+
? padded.map((_, index) => index)
|
|
390
|
+
: [
|
|
391
|
+
spineIndex,
|
|
392
|
+
...padded
|
|
393
|
+
.map((_, index) => index)
|
|
394
|
+
.filter((index) => index !== spineIndex),
|
|
395
|
+
];
|
|
396
|
+
const laid = hstack(order.map((index) => {
|
|
397
|
+
const column = padded[index];
|
|
398
|
+
return column === undefined ? emptyBlock() : column;
|
|
399
|
+
}), GAP);
|
|
400
|
+
const body = laid.block;
|
|
401
|
+
const width = body.width;
|
|
402
|
+
const spines = new Array(padded.length).fill(0);
|
|
403
|
+
order.forEach((original, position) => {
|
|
404
|
+
const at = laid.spines[position];
|
|
405
|
+
spines[original] = at === undefined ? 0 : at;
|
|
406
|
+
});
|
|
407
|
+
const isDetour = spineIndex !== undefined;
|
|
408
|
+
const first = spines[0];
|
|
409
|
+
const last = spines[spines.length - 1];
|
|
410
|
+
const midpoint = Math.round(((first === undefined ? 0 : first) + (last === undefined ? 0 : last)) / 2);
|
|
411
|
+
// Snap the incoming line onto a column when it lands beside one: `┴` and `┬`
|
|
412
|
+
// printed in adjacent cells read as a smudge, not a junction.
|
|
413
|
+
const anchor = columnAnchor(spines, spineIndex, midpoint);
|
|
414
|
+
const others = spines.filter((_, index) => index !== spineIndex);
|
|
415
|
+
// A detour keeps its spine and hangs the rails off it; a fork has no spine to
|
|
416
|
+
// keep, so the incoming line lands between the columns instead.
|
|
417
|
+
const splitRail = isDetour
|
|
418
|
+
? rail(width, spines.map((at, index) => ({
|
|
419
|
+
at,
|
|
420
|
+
char: index === spineIndex
|
|
421
|
+
? teeAt(anchor, others, true)
|
|
422
|
+
: at > anchor
|
|
423
|
+
? "┐"
|
|
424
|
+
: "┌",
|
|
425
|
+
})))
|
|
426
|
+
: rail(width, [
|
|
427
|
+
...spines.map((at, index) => ({
|
|
428
|
+
at,
|
|
429
|
+
char: at === anchor
|
|
430
|
+
? "┼"
|
|
431
|
+
: index === 0
|
|
432
|
+
? "┌"
|
|
433
|
+
: index === spines.length - 1
|
|
434
|
+
? "┐"
|
|
435
|
+
: "┬",
|
|
436
|
+
})),
|
|
437
|
+
...(spines.includes(anchor) ? [] : [{ at: anchor, char: "┴" }]),
|
|
438
|
+
]);
|
|
439
|
+
const top = vstack([head, lead]);
|
|
440
|
+
const aligned = indent({ lines: [splitRail, ...body.lines], width, spine: anchor }, Math.max(0, top.spine - anchor));
|
|
441
|
+
const whole = vstack([top, aligned]);
|
|
442
|
+
const mergeAt = aligned.spine;
|
|
443
|
+
// Every column still carrying the flow is joined back to one spine, whether
|
|
444
|
+
// this branch owns the merge point or an ancestor does. A column left hanging
|
|
445
|
+
// reads as a path that stops, which is a different claim about the workflow.
|
|
446
|
+
const shift = aligned.width - width;
|
|
447
|
+
const arms = spines
|
|
448
|
+
.map((at, index) => ({
|
|
449
|
+
at: at + shift,
|
|
450
|
+
open: isColumnOpen(columns, index),
|
|
451
|
+
}))
|
|
452
|
+
.filter(({ at, open }) => open && at !== mergeAt)
|
|
453
|
+
.map(({ at }) => at);
|
|
454
|
+
const joined = arms.length === 0
|
|
455
|
+
? []
|
|
456
|
+
: [
|
|
457
|
+
{
|
|
458
|
+
lines: [
|
|
459
|
+
rail(aligned.width, [
|
|
460
|
+
{ at: mergeAt, char: teeAt(mergeAt, arms, false) },
|
|
461
|
+
...arms.map((at) => ({
|
|
462
|
+
at,
|
|
463
|
+
char: at > mergeAt ? "┘" : "└",
|
|
464
|
+
})),
|
|
465
|
+
]),
|
|
466
|
+
],
|
|
467
|
+
width: aligned.width,
|
|
468
|
+
spine: mergeAt,
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
if (merge === undefined) {
|
|
472
|
+
// For a detour, the spine column's status determines the flow. For a fork,
|
|
473
|
+
// any open column means the flow continues — including the column at the
|
|
474
|
+
// anchor, which `arms` filters out because it needs no rail to reach it.
|
|
475
|
+
const spineOpen = isDetour
|
|
476
|
+
? isColumnOpen(columns, spineIndex)
|
|
477
|
+
: columns.some((column) => column.open);
|
|
478
|
+
return { block: vstack([whole, ...joined]), open: spineOpen };
|
|
479
|
+
}
|
|
480
|
+
const after = layout(merge, stop, context);
|
|
481
|
+
return {
|
|
482
|
+
block: vstack([whole, ...joined, spineBlock(), after.block]),
|
|
483
|
+
open: after.open,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Entry point
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
export function renderNodeAscii(nodes, options = {}) {
|
|
490
|
+
const graph = buildGraph(nodes);
|
|
491
|
+
const warnings = [];
|
|
492
|
+
const start = findStart(nodes);
|
|
493
|
+
if (start === undefined) {
|
|
494
|
+
return { diagram: "", format: "ascii", warnings: ["no nodes to draw"] };
|
|
495
|
+
}
|
|
496
|
+
const context = {
|
|
497
|
+
graph,
|
|
498
|
+
names: options.names,
|
|
499
|
+
paid: options.paid === undefined ? [] : options.paid,
|
|
500
|
+
highlight: options.highlight === undefined ? [] : options.highlight,
|
|
501
|
+
drawn: new Set(),
|
|
502
|
+
notes: warnings,
|
|
503
|
+
};
|
|
504
|
+
const { block } = layout(start.uuid, new Set(), context);
|
|
505
|
+
warnings.push(...graphWarnings(nodes, graph.byUuid, context.drawn));
|
|
506
|
+
// Rails stack rightward, so a graph that branches at every step of a long
|
|
507
|
+
// chain gets wide no matter how the columns are packed — each rail has to
|
|
508
|
+
// clear the one belonging to the step below it. Say so rather than letting it
|
|
509
|
+
// wrap in the reader's terminal and look like a rendering bug.
|
|
510
|
+
const widest = Math.max(0, ...block.lines.map((line) => displayWidth(line)));
|
|
511
|
+
if (widest > TERMINAL_WIDTH) {
|
|
512
|
+
warnings.push(`the drawing is ${String(widest)} columns wide and will wrap in most terminals — try --format mermaid, or diagram a sub-graph`);
|
|
513
|
+
}
|
|
514
|
+
const body = block.lines.map((line) => line.replace(/\s+$/, "")).join("\n");
|
|
515
|
+
const title = options.title === undefined ? undefined : options.title.trim();
|
|
516
|
+
if (title === undefined || title.length === 0) {
|
|
517
|
+
return { diagram: body, format: "ascii", warnings };
|
|
518
|
+
}
|
|
519
|
+
// Centre the title on the spine, not on the drawing's bounding box — the
|
|
520
|
+
// spine is what the eye follows down, and a wide rail off to one side would
|
|
521
|
+
// otherwise drag the title away from it.
|
|
522
|
+
const left = Math.max(0, block.spine - Math.floor(displayWidth(title) / 2));
|
|
523
|
+
return {
|
|
524
|
+
diagram: `${" ".repeat(left)}${title}\n\n${body}`,
|
|
525
|
+
format: "ascii",
|
|
526
|
+
warnings,
|
|
527
|
+
};
|
|
528
|
+
}
|
package/build/credentials.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { clearCredentials, type StoredCredentials as Credentials, getConfigDir, getCredentialsPath, loadCredentials, type RefreshableSession, saveCredentials, } from "@cargo-ai/cdk/cli";
|
|
1
|
+
export { clearCredentials, type StoredCredentials as Credentials, CredentialsNotClearedError, getConfigDir, getCredentialsPath, loadCredentials, type RefreshableSession, saveCredentials, } from "@cargo-ai/cdk/cli";
|
|
2
2
|
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,gBAAgB,EAChB,KAAK,iBAAiB,IAAI,WAAW,EACrC,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,kBAAkB,EACvB,eAAe,GAChB,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,gBAAgB,EAChB,KAAK,iBAAiB,IAAI,WAAW,EACrC,0BAA0B,EAC1B,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,kBAAkB,EACvB,eAAe,GAChB,MAAM,mBAAmB,CAAC"}
|
package/build/credentials.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// The credentials file is shared with `cargo-cdk`, so its format and access
|
|
2
2
|
// live in @cargo-ai/cdk/cli — one login serves both binaries and neither can
|
|
3
3
|
// drift from the other's idea of what is on disk.
|
|
4
|
-
export { clearCredentials, getConfigDir, getCredentialsPath, loadCredentials, saveCredentials, } from "@cargo-ai/cdk/cli";
|
|
4
|
+
export { clearCredentials, CredentialsNotClearedError, getConfigDir, getCredentialsPath, loadCredentials, saveCredentials, } from "@cargo-ai/cdk/cli";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cargo-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.57",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://getcargo.ai",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@cargo-ai/api": "^1.0.60",
|
|
63
63
|
"@cargo-ai/app-sdk": "^1.0.6",
|
|
64
|
-
"@cargo-ai/cdk": "^1.0.
|
|
64
|
+
"@cargo-ai/cdk": "^1.0.46",
|
|
65
65
|
"@cargo-ai/types": "^1.0.57",
|
|
66
66
|
"@cargo-ai/worker-sdk": "^1.0.12",
|
|
67
67
|
"@modelcontextprotocol/sdk": "1.29.0",
|