@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 CHANGED
@@ -42,7 +42,7 @@ cargo-ai login --token <your-api-token>
42
42
 
43
43
  `--email` and `--oauth` save the signed-in session — a short-lived access token plus the refresh token that renews it — to `~/.config/cargo-ai/credentials.json` (file mode `0600`). The CLI renews it in the background as it expires, so you stay signed in and no API token is created per machine. `--token` saves the API token you supplied instead. The same file backs `cargo-cdk`, so one login serves both.
44
44
 
45
- `cargo-ai logout` revokes the session at the identity provider, so signing out is final. A token you supplied yourself with `--token` is only forgotten locally, since it may be in use elsewhere; pass `--keep-token` to force that behaviour for a session too.
45
+ `cargo-ai logout` revokes the session at the identity provider, so signing out is final. A token you supplied yourself with `--token` is only forgotten locally, since it may be in use elsewhere; pass `--keep-token` to force that behaviour for a session too. If a credentials file cannot be deleted — a read-only config directory, or one owned by another user — `logout` fails and names the file rather than reporting a sign-out the next command would contradict.
46
46
 
47
47
  Treat the file as you would a password. A saved session acts as you across **every** workspace you belong to — unlike an API token, which is limited to one workspace and to the permissions it was created with — and it stays valid until you run `cargo-ai logout` or the identity provider expires it. On a shared or long-lived machine (CI, a build box), prefer an API token created under **Settings → API** and passed via `CARGO_API_TOKEN`, which you can scope down and revoke from the dashboard.
48
48
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAwBxD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAkGN;AAmFD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE,aAAa,GAClB,cAAc,CAchB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAyBxD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAyGN;AAiGD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE,aAAa,GAClB,cAAc,CAchB"}
@@ -2,7 +2,7 @@ import { revokeSession } from "@cargo-ai/cdk/cli";
2
2
  import { Option } from "commander";
3
3
  import { AGENT_SKILLS_NEXT_STEP } from "../../agentSkills.js";
4
4
  import { getConfig } from "../../config.js";
5
- import { clearCredentials, getCredentialsPath, loadCredentials, saveCredentials, } from "../../credentials.js";
5
+ import { clearCredentials, CredentialsNotClearedError, getCredentialsPath, loadCredentials, saveCredentials, } from "../../credentials.js";
6
6
  import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
7
7
  import { runEmailLogin } from "./email.js";
8
8
  import { selectLoginChannel } from "./loginChannel.js";
@@ -40,8 +40,11 @@ that name when one exists, so this is safe to re-run.
40
40
 
41
41
  --email and --oauth save the signed-in session to
42
42
  ~/.config/cargo-ai/credentials.json and renew it automatically, so you stay
43
- signed in without a token being created for each machine. Switch workspace at
44
- any time with CARGO_WORKSPACE_UUID or by signing in again with
43
+ signed in without a token being created for each machine. CARGO_CONFIG_DIR
44
+ (or XDG_CONFIG_HOME) moves that file; where the home directory is not writable
45
+ — agent sandboxes usually run that way — the session is saved under the system
46
+ temp directory instead, which lasts only as long as the sandbox. Switch
47
+ workspace at any time with CARGO_WORKSPACE_UUID or by signing in again with
45
48
  --workspace-uuid <uuid>. An API token passed with --token stays pinned to its
46
49
  own workspace, so the override does not apply to it.
47
50
 
@@ -59,7 +62,11 @@ Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) ta
59
62
  .addHelpText("after", `
60
63
  A session from "cargo-ai login --email" or "--oauth" is revoked at the identity
61
64
  provider, so signing out is final. Tokens you supplied yourself with --token are
62
- only forgotten locally, since they may be in use elsewhere.`)
65
+ only forgotten locally, since they may be in use elsewhere.
66
+
67
+ Signing out fails, naming the file, when a credentials file cannot be deleted —
68
+ a read-only config directory, or one owned by another user — because the next
69
+ command would otherwise read it and still be signed in.`)
63
70
  .action(async (opts) => {
64
71
  await runLogout(opts);
65
72
  });
@@ -123,7 +130,20 @@ async function runLogout(opts) {
123
130
  const credentials = loadCredentials();
124
131
  const plan = planRevocation(credentials, opts);
125
132
  const revoked = await applyRevocation(plan, credentials);
126
- const removed = clearCredentials();
133
+ let removed;
134
+ try {
135
+ removed = clearCredentials();
136
+ }
137
+ catch (error) {
138
+ if (error instanceof CredentialsNotClearedError) {
139
+ // Revocation already happened, so say so: re-running logout will not
140
+ // revoke a second time, and the file is all that is left to deal with.
141
+ failWith(error.message, {
142
+ extra: { revoked, credentialsFiles: error.paths },
143
+ });
144
+ }
145
+ throw error;
146
+ }
127
147
  if (removed === true) {
128
148
  success(revoked === true ? "Signed out and revoked access." : "Signed out.");
129
149
  }
@@ -0,0 +1,42 @@
1
+ export declare function displayWidth(text: string): number;
2
+ /**
3
+ * A rectangle of text plus the column its spine sits on. `spine` is where an
4
+ * incoming `│` attaches at the top and where an outgoing one leaves at the
5
+ * bottom, so stacking is a matter of lining up that column.
6
+ */
7
+ export type Block = {
8
+ lines: string[];
9
+ width: number;
10
+ spine: number;
11
+ };
12
+ export declare function padTo(line: string, width: number): string;
13
+ export declare function indent(block: Block, by: number): Block;
14
+ /** Text lines centred on a shared spine. No lines is a legitimate answer — an
15
+ * unnamed routing node draws as its split and nothing else. */
16
+ export declare function textBlock(texts: string[]): Block;
17
+ /** One row carrying nothing but the spine. */
18
+ export declare function spineBlock(char?: string): Block;
19
+ export declare function emptyBlock(): Block;
20
+ /** Stack blocks vertically, aligning their spines. */
21
+ export declare function vstack(blocks: Block[]): Block;
22
+ /** Place blocks side by side, returning where each one's spine landed. */
23
+ export declare function hstack(blocks: Block[], gap: number): {
24
+ block: Block;
25
+ spines: number[];
26
+ };
27
+ /** Extend a column downwards so every column in a split ends level. */
28
+ export declare function padColumn(block: Block, toHeight: number, char: string): Block;
29
+ /**
30
+ * The glyph where a rail meets the spine. A tee has to point at the rails it
31
+ * actually carries: drawing `├` while the rail leaves to the left produces a
32
+ * junction with an arm attached to nothing and a rail attached to nothing.
33
+ */
34
+ export declare function teeAt(spine: number, others: number[], down: boolean): string;
35
+ /** Draw a horizontal rail across `width`, writing one glyph per marked column. */
36
+ export declare function rail(width: number, marks: {
37
+ at: number;
38
+ char: string;
39
+ }[]): string;
40
+ /** Wrap a block in a captioned box, for a sub-flow that runs inside a step. */
41
+ export declare function frame(block: Block, caption: string): Block;
42
+ //# sourceMappingURL=asciiBlocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciiBlocks.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/asciiBlocks.ts"],"names":[],"mappings":"AAyBA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjD;AAMD;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAIF,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAQtD;AAED;+DAC+D;AAC/D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,CAShD;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,CAAC,IAAI,SAAM,GAAG,KAAK,CAE5C;AAED,wBAAgB,UAAU,IAAI,KAAK,CAElC;AAED,sDAAsD;AACtD,wBAAgB,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAsB7C;AAED,0EAA0E;AAC1E,wBAAgB,MAAM,CACpB,MAAM,EAAE,KAAK,EAAE,EACf,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA6BpC;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAQ7E;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAO5E;AAED,kFAAkF;AAClF,wBAAgB,IAAI,CAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GACpC,MAAM,CAWR;AAED,+EAA+E;AAC/E,wBAAgB,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAU1D"}
@@ -0,0 +1,170 @@
1
+ // Text blocks: the layout algebra the ASCII diagram is assembled from.
2
+ //
3
+ // A Block is a rectangle of text plus the column its spine sits on — the column
4
+ // an incoming `│` attaches to at the top and an outgoing one leaves from at the
5
+ // bottom. Stacking is then a matter of lining that column up, which is what lets
6
+ // the renderer compose a drawing without ever tracking absolute coordinates.
7
+ //
8
+ // Nothing here knows what a workflow is. The invariant every producer maintains
9
+ // is that each line of a Block is exactly `block.width` cells wide, which is why
10
+ // so little of this has to measure anything.
11
+ // ---------------------------------------------------------------------------
12
+ // Character width
13
+ // ---------------------------------------------------------------------------
14
+ // A marker glyph occupies two terminal cells; measuring it as one silently
15
+ // shears every line to its right. `Emoji_Presentation` is the property that
16
+ // means "wide by default" — `Extended_Pictographic` is too broad, matching `◀`,
17
+ // which a terminal draws in a single cell.
18
+ const WIDE = /\p{Emoji_Presentation}/u;
19
+ // Most lines are plain text; the per-character scan is only needed once a line
20
+ // actually carries a marker. One test for the whole string skips it.
21
+ const PLAIN = /^[\x20-\x7E]*$/;
22
+ export function displayWidth(text) {
23
+ if (PLAIN.test(text))
24
+ return text.length;
25
+ let total = 0;
26
+ for (const char of text)
27
+ total += WIDE.test(char) ? 2 : 1;
28
+ return total;
29
+ }
30
+ const blank = (width) => (width > 0 ? " ".repeat(width) : "");
31
+ export function padTo(line, width) {
32
+ const short = width - displayWidth(line);
33
+ return short > 0 ? line + " ".repeat(short) : line;
34
+ }
35
+ export function indent(block, by) {
36
+ if (by <= 0)
37
+ return block;
38
+ const prefix = " ".repeat(by);
39
+ return {
40
+ lines: block.lines.map((line) => prefix + line),
41
+ width: block.width + by,
42
+ spine: block.spine + by,
43
+ };
44
+ }
45
+ /** Text lines centred on a shared spine. No lines is a legitimate answer — an
46
+ * unnamed routing node draws as its split and nothing else. */
47
+ export function textBlock(texts) {
48
+ if (texts.length === 0)
49
+ return emptyBlock();
50
+ const measured = texts.map((text) => ({ text, own: displayWidth(text) }));
51
+ const width = Math.max(...measured.map((entry) => entry.own));
52
+ const lines = measured.map(({ text, own }) => {
53
+ const left = Math.floor((width - own) / 2);
54
+ return " ".repeat(left) + text + blank(width - own - left);
55
+ });
56
+ return { lines, width, spine: Math.floor((width - 1) / 2) };
57
+ }
58
+ /** One row carrying nothing but the spine. */
59
+ export function spineBlock(char = "│") {
60
+ return { lines: [char], width: 1, spine: 0 };
61
+ }
62
+ export function emptyBlock() {
63
+ return { lines: [], width: 0, spine: 0 };
64
+ }
65
+ /** Stack blocks vertically, aligning their spines. */
66
+ export function vstack(blocks) {
67
+ const real = blocks.filter((block) => block.lines.length > 0);
68
+ if (real.length === 0)
69
+ return emptyBlock();
70
+ if (real.length === 1) {
71
+ const only = real[0];
72
+ if (only === undefined)
73
+ return emptyBlock();
74
+ return only;
75
+ }
76
+ const spine = Math.max(...real.map((block) => block.spine));
77
+ const shifted = real.map((block) => indent(block, spine - block.spine));
78
+ const width = Math.max(...shifted.map((block) => block.width));
79
+ return {
80
+ lines: shifted.flatMap((block) => {
81
+ const short = blank(width - block.width);
82
+ return short.length === 0
83
+ ? block.lines
84
+ : block.lines.map((line) => line + short);
85
+ }),
86
+ width,
87
+ spine,
88
+ };
89
+ }
90
+ /** Place blocks side by side, returning where each one's spine landed. */
91
+ export function hstack(blocks, gap) {
92
+ const height = Math.max(...blocks.map((block) => block.lines.length));
93
+ const spines = [];
94
+ let offset = 0;
95
+ const rows = Array.from({ length: height }, () => "");
96
+ blocks.forEach((block, index) => {
97
+ if (index > 0)
98
+ offset += gap;
99
+ spines.push(offset + block.spine);
100
+ for (let row = 0; row < height; row += 1) {
101
+ // Every Block keeps `displayWidth(line) === block.width` on every line, so
102
+ // the line needs no measuring here — only the canvas does.
103
+ const canvas = rows[row];
104
+ const line = block.lines[row];
105
+ rows[row] =
106
+ padTo(canvas === undefined ? "" : canvas, offset) +
107
+ (line === undefined ? blank(block.width) : line);
108
+ }
109
+ offset += block.width;
110
+ });
111
+ return {
112
+ block: {
113
+ lines: rows.map((row) => padTo(row, offset)),
114
+ width: offset,
115
+ spine: 0,
116
+ },
117
+ spines,
118
+ };
119
+ }
120
+ /** Extend a column downwards so every column in a split ends level. */
121
+ export function padColumn(block, toHeight, char) {
122
+ const missing = toHeight - block.lines.length;
123
+ if (missing <= 0)
124
+ return block;
125
+ const filler = padTo(" ".repeat(block.spine) + char, block.width);
126
+ return {
127
+ ...block,
128
+ lines: [...block.lines, ...Array.from({ length: missing }, () => filler)],
129
+ };
130
+ }
131
+ /**
132
+ * The glyph where a rail meets the spine. A tee has to point at the rails it
133
+ * actually carries: drawing `├` while the rail leaves to the left produces a
134
+ * junction with an arm attached to nothing and a rail attached to nothing.
135
+ */
136
+ export function teeAt(spine, others, down) {
137
+ const left = others.some((at) => at < spine);
138
+ const right = others.some((at) => at > spine);
139
+ if (left && right)
140
+ return "┼";
141
+ if (left)
142
+ return "┤";
143
+ if (right)
144
+ return "├";
145
+ return down ? "┬" : "┴";
146
+ }
147
+ /** Draw a horizontal rail across `width`, writing one glyph per marked column. */
148
+ export function rail(width, marks) {
149
+ const sorted = [...marks].sort((a, b) => a.at - b.at);
150
+ const first = sorted[0];
151
+ const last = sorted[sorted.length - 1];
152
+ if (first === undefined || last === undefined)
153
+ return " ".repeat(width);
154
+ const cells = Array.from({ length: width }, (_, column) => column > first.at && column < last.at ? "─" : " ");
155
+ for (const mark of sorted)
156
+ cells[mark.at] = mark.char;
157
+ return cells.join("");
158
+ }
159
+ /** Wrap a block in a captioned box, for a sub-flow that runs inside a step. */
160
+ export function frame(block, caption) {
161
+ const inner = Math.max(block.width, displayWidth(caption) + 2);
162
+ const width = inner + 4;
163
+ const label = ` ${caption} `;
164
+ const lines = [
165
+ `┌─${label}${"─".repeat(Math.max(0, width - 3 - displayWidth(label)))}┐`,
166
+ ...block.lines.map((line) => `│ ${padTo(line, inner)} │`),
167
+ `└${"─".repeat(width - 2)}┘`,
168
+ ];
169
+ return { lines, width, spine: block.spine + 2 };
170
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/node.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAexC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAoK7E"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/node.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAgBxC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAoK7E"}
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { ExitCodes, failWith, handleApiCall, outputJson, parseJson, } from "../runHandler.js";
3
3
  import { extractNodes, renderNodeDiagram, toMermaidBlock, } from "./nodeDiagram.js";
4
+ import { renderNodeAscii } from "./nodeDiagramAscii.js";
4
5
  export function registerNodeCommands(parent, getApi) {
5
6
  const node = parent
6
7
  .command("node")
@@ -105,7 +106,7 @@ Examples:
105
106
  function registerDiagramCommand(node, getApi) {
106
107
  node
107
108
  .command("diagram")
108
- .description("Draw a node graph as a Mermaid flowchart (free, runs nothing, no credits)")
109
+ .description("Draw a node graph ASCII for a terminal, Mermaid to paste elsewhere (free, runs nothing, no credits)")
109
110
  .option("--nodes <json>", "Node definitions (JSON array, or '-' to read from stdin)")
110
111
  .option("--file <path>", "Read a JSON payload or node array from a file")
111
112
  .option("--workflow-uuid <uuid>", "Diagram a workflow's deployed release (add --draft for its draft)")
@@ -113,37 +114,74 @@ function registerDiagramCommand(node, getApi) {
113
114
  .option("--release-uuid <uuid>", "Diagram a specific release")
114
115
  .option("--run-uuid <uuid>", "Diagram the graph a run executed (follows the run's release when it has one)")
115
116
  .option("--title <text>", "Title rendered above the diagram")
116
- .option("--direction <TD|LR>", "Flow direction (default: TD)", "TD")
117
+ .option("--format <ascii|mermaid>", "ascii = readable in a terminal · mermaid = paste into a PR, docs or a page (default: mermaid)", "mermaid")
118
+ .option("--direction <TD|LR>", "Mermaid flow direction, ignored by --format ascii (default: TD)", "TD")
117
119
  .option("--paid <slugs>", "Comma-separated node slugs/uuids that bill credits — marked 💳")
118
- .option("--highlight <slugs>", "Comma-separated node slugs/uuids to mark red, e.g. a failing node")
119
- .option("--raw", "Print the fenced Mermaid block instead of JSON")
120
+ .option("--highlight <slugs>", "Comma-separated node slugs/uuids to mark (red in Mermaid, in ASCII)")
121
+ .option("--raw", "Print the diagram itself instead of JSON")
120
122
  .addHelpText("after", `
121
123
  Free, runs nothing: turns a node graph into a picture so a reviewer can see the
122
124
  routing, the fallback paths and which steps bill before approving or deploying
123
125
  it. Pass exactly one source.
124
126
 
125
- Returns {"diagram":"flowchart TD…","format":"mermaid","warnings":[…]}, or the
126
- fenced block itself with --raw. 'warnings' reports structural problems worth
127
- saying out loud nodes unreachable from start, dangling childrenUuids.
127
+ Which format:
128
+ --format ascii SHOWING a graph to someone at a terminal. Renders as a
129
+ centred flow with named steps and labelled branches, so it
130
+ is readable as-is. Use this whenever you are about to
131
+ explain what a workflow, tool or play does.
132
+ --format ascii resolves step names from the integration catalog, so a step
133
+ reads 'Apollo.io / Enrich person'. --nodes and --file skip that lookup and
134
+ keep the slugs, so they work with no credential at all.
135
+
136
+ --format mermaid PASTING the graph somewhere that renders it — a PR body, a
137
+ markdown doc, a published page. In a terminal it is source
138
+ code, not a picture. This is the default, for
139
+ compatibility.
140
+
141
+ Returns {"diagram":"…","format":"ascii"|"mermaid","warnings":[…]}, or the
142
+ diagram itself with --raw (fenced for mermaid, plain text for ascii).
143
+ 'warnings' reports structural problems worth saying out loud — nodes
144
+ unreachable from start, dangling childrenUuids.
128
145
 
129
146
  A run created by 'action execute' carries its own graph; a run of a deployed
130
147
  tool or play carries only a releaseUuid, and --run-uuid follows it either way.
131
148
 
132
149
  Examples:
133
- $ cargo-ai orchestration node diagram --workflow-uuid <uuid> --raw
134
- $ cargo-ai orchestration node diagram --workflow-uuid <uuid> --draft --paid enrich,verify
135
- $ cargo-ai orchestration node diagram --run-uuid <uuid> --highlight branch_1 --raw
136
- $ cargo-ai orchestration release get-deployed --workflow-uuid <uuid> | cargo-ai orchestration node diagram --nodes -`)
150
+ $ cargo-ai orchestration node diagram --workflow-uuid <uuid> --format ascii --raw
151
+ $ cargo-ai orchestration node diagram --workflow-uuid <uuid> --draft --paid enrich,verify --format ascii --raw
152
+ $ cargo-ai orchestration node diagram --run-uuid <uuid> --highlight branch_1 --format ascii --raw
153
+ $ cargo-ai orchestration node diagram --workflow-uuid <uuid> --raw # mermaid, for a PR
154
+ $ cargo-ai orchestration release get-deployed --workflow-uuid <uuid> | cargo-ai orchestration node diagram --nodes - --format ascii --raw`)
137
155
  .action(async (opts) => {
156
+ const format = (opts.format === undefined ? "mermaid" : opts.format).toLowerCase();
157
+ if (format !== "ascii" && format !== "mermaid") {
158
+ failWith(`--format must be 'ascii' or 'mermaid', got '${format}'`, {
159
+ code: ExitCodes.InvalidUsage,
160
+ });
161
+ }
138
162
  const nodes = await resolveNodes(getApi, opts);
139
- const result = renderNodeDiagram(nodes, {
140
- title: opts.title,
141
- direction: (opts.direction ?? "TD").toUpperCase(),
142
- paid: splitList(opts.paid),
143
- highlight: splitList(opts.highlight),
144
- });
163
+ const result = format === "ascii"
164
+ ? renderNodeAscii(nodes, {
165
+ title: opts.title,
166
+ paid: splitList(opts.paid),
167
+ highlight: splitList(opts.highlight),
168
+ // `--nodes` / `--file` are the offline sources: resolving them
169
+ // touched no API, and reaching for one now to prettify a label
170
+ // would make an offline command demand a login.
171
+ names: opts.nodes === undefined && opts.file === undefined
172
+ ? await catalogNames(getApi, nodes)
173
+ : undefined,
174
+ })
175
+ : renderNodeDiagram(nodes, {
176
+ title: opts.title,
177
+ direction: (opts.direction === undefined
178
+ ? "TD"
179
+ : opts.direction).toUpperCase(),
180
+ paid: splitList(opts.paid),
181
+ highlight: splitList(opts.highlight),
182
+ });
145
183
  if (opts.raw === true) {
146
- console.log(toMermaidBlock(result.diagram));
184
+ console.log(format === "ascii" ? result.diagram : toMermaidBlock(result.diagram));
147
185
  for (const warning of result.warnings)
148
186
  console.error(`⚠ ${warning}`);
149
187
  return;
@@ -151,6 +189,81 @@ Examples:
151
189
  outputJson(result);
152
190
  });
153
191
  }
192
+ /**
193
+ * Display names for the steps a graph actually uses, so it reads
194
+ * `Apollo.io` / `Enrich person` rather than `apolloio` / `enrichPerson`.
195
+ *
196
+ * Both halves come from the platform's own catalogs — the integration catalog
197
+ * for connector steps, the native-integration catalog for built-in ones — so a
198
+ * step is never labelled with a name this CLI invented. Renaming an action on
199
+ * the platform reaches the drawing without a code change.
200
+ *
201
+ * Best-effort, and only for graphs that came from the API in the first place.
202
+ * `--nodes` and `--file` skip it entirely: those resolve without a credential,
203
+ * and `getApi()` exits the process when there is none, so asking for names
204
+ * there would turn an offline command into one that demands a login. A catalog
205
+ * that is reachable but fails — a token without the scope, a network blip —
206
+ * falls back to slugs rather than losing the drawing.
207
+ */
208
+ async function catalogNames(getApi, nodes) {
209
+ const used = new Set();
210
+ let hasNative = false;
211
+ // Group bodies live in `config._nodes` and are drawn too, so their steps need
212
+ // names as much as the outer ones — otherwise a loop shows wire slugs beside
213
+ // an outer graph showing product names.
214
+ const collect = (list) => {
215
+ for (const node of list) {
216
+ if (node.kind === "connector" &&
217
+ typeof node.integrationSlug === "string") {
218
+ used.add(node.integrationSlug);
219
+ }
220
+ if (node.kind === "native")
221
+ hasNative = true;
222
+ const config = node.config;
223
+ if (config !== null && config !== undefined) {
224
+ const nested = config["_nodes"];
225
+ if (Array.isArray(nested))
226
+ collect(nested);
227
+ }
228
+ }
229
+ };
230
+ collect(nodes);
231
+ if (used.size === 0 && !hasNative)
232
+ return undefined;
233
+ try {
234
+ const api = getApi();
235
+ const [integrations, native] = await Promise.all([
236
+ used.size === 0
237
+ ? undefined
238
+ : api.connection.integration
239
+ .list({ slugs: [...used] })
240
+ .catch(() => undefined),
241
+ hasNative
242
+ ? api.connection.nativeIntegration.get().catch(() => undefined)
243
+ : undefined,
244
+ ]);
245
+ const byIntegration = {};
246
+ const byAction = {};
247
+ const listed = integrations === undefined ? [] : integrations.integrations;
248
+ for (const integration of listed) {
249
+ const actions = integration.actions === undefined ? {} : integration.actions;
250
+ byIntegration[integration.slug] = integration.name;
251
+ byAction[integration.slug] = Object.fromEntries(Object.entries(actions).map(([slug, action]) => [slug, action.name]));
252
+ }
253
+ const nativeActions = native === undefined ? {} : native.nativeIntegration.actions;
254
+ return {
255
+ integrations: byIntegration,
256
+ actions: byAction,
257
+ native: Object.fromEntries(Object.entries(nativeActions).map(([slug, action]) => [
258
+ slug,
259
+ action.name,
260
+ ])),
261
+ };
262
+ }
263
+ catch {
264
+ return undefined;
265
+ }
266
+ }
154
267
  function splitList(value) {
155
268
  if (value === undefined)
156
269
  return [];
@@ -30,8 +30,50 @@ export type DiagramResult = {
30
30
  /** Structural problems worth saying out loud rather than drawing over. */
31
31
  warnings: string[];
32
32
  };
33
+ /** Actions that route rather than do work. Shared, so adding one to the
34
+ * platform does not have to be mirrored in each renderer. */
35
+ export declare const ROUTING_ACTIONS: Set<string>;
36
+ export declare function isStart(node: DiagramNode | undefined): boolean;
37
+ export declare function isEnd(node: DiagramNode | undefined): boolean;
38
+ export declare function indexByUuid(nodes: DiagramNode[]): Map<string, DiagramNode>;
39
+ /** Where the graph begins — the `start` node, or the first node if it has none. */
40
+ export declare function findStart(nodes: DiagramNode[]): DiagramNode | undefined;
41
+ /**
42
+ * Whether a user-supplied `--paid` / `--highlight` token names this node.
43
+ * Slugs repeat within a release, so a slug token marks every node carrying it;
44
+ * pass a uuid to mark exactly one.
45
+ */
46
+ export declare function matchesNode(node: DiagramNode, needles: string[]): boolean;
47
+ /**
48
+ * A node's outgoing edges, in `childrenUuids` order, labelled by what the
49
+ * routing node means. Shared because this encodes routing *semantics* — index
50
+ * order, and the rule that a fallback pointing at the node's own next step is
51
+ * the same arrow rather than a second one. Two copies would let the two formats
52
+ * draw different graphs from one release.
53
+ */
54
+ /**
55
+ * A fallback pointing at the node's own next step: a failure here does not stop
56
+ * the run. It is not a second arrow — it is the same one — so it shows as a mark
57
+ * on the step rather than an edge. Silently dropping it makes a step that
58
+ * survives a provider outage look like one that dies on it, which is the exact
59
+ * misread this command exists to prevent.
60
+ */
61
+ export declare function continuesOnFailure(node: DiagramNode): boolean;
62
+ export declare function outgoingEdges(node: DiagramNode, byUuid: Map<string, DiagramNode>): {
63
+ to: string;
64
+ label?: string;
65
+ dashed?: boolean;
66
+ }[];
67
+ /** Structural problems worth saying out loud rather than drawing over. */
68
+ export declare function graphWarnings(nodes: DiagramNode[], byUuid: Map<string, DiagramNode>, reached: Set<string>): string[];
33
69
  /** What the node is, in words a user recognises. */
34
70
  export declare function labelFor(node: DiagramNode): string;
71
+ /**
72
+ * `childrenUuids` order carries the routing semantics — index 0 of a `branch`
73
+ * is the matched path, index 1 the unmatched one. Getting this backwards
74
+ * inverts the diagram's meaning, so it is table-driven, never inferred.
75
+ */
76
+ export declare function edgeLabels(node: DiagramNode, count: number): (string | undefined)[];
35
77
  export declare function renderNodeDiagram(nodes: DiagramNode[], options?: DiagramOptions): DiagramResult;
36
78
  /** Node-shaped means: a uuid plus the fields a graph is walked by. */
37
79
  export declare function isNodeArray(value: unknown): value is DiagramNode[];
@@ -1 +1 @@
1
- {"version":3,"file":"nodeDiagram.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/nodeDiagram.ts"],"names":[],"mappings":"AAgBA;;2CAE2C;AAC3C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAsHF,oDAAoD;AACpD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CASlD;AAyND,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAkCf;AAMD,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,EAAE,CAYlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,EAAE,GAAG,SAAS,CAexE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD"}
1
+ {"version":3,"file":"nodeDiagram.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/nodeDiagram.ts"],"names":[],"mappings":"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"}