@sweny-ai/core 0.1.67 → 0.1.68
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.
|
@@ -2,8 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { runWorkflowDiagram } from "../diagram.js";
|
|
5
|
+
import { runWorkflowDiagram, resolveUseBlock } from "../diagram.js";
|
|
7
6
|
const sampleWorkflow = {
|
|
8
7
|
id: "sample",
|
|
9
8
|
name: "Sample",
|
|
@@ -32,6 +31,42 @@ function captureStream() {
|
|
|
32
31
|
},
|
|
33
32
|
};
|
|
34
33
|
}
|
|
34
|
+
describe("resolveUseBlock", () => {
|
|
35
|
+
// Centralized behavior test — the renderer is a tiny switch around this
|
|
36
|
+
// so keeping the rules here as one table makes the policy easy to audit.
|
|
37
|
+
it("returns false by default (stdout, no -o, no flags)", () => {
|
|
38
|
+
expect(resolveUseBlock({})).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("returns true when --block is explicit", () => {
|
|
41
|
+
expect(resolveUseBlock({ block: true })).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("returns false when --no-block is explicit, even for .md", () => {
|
|
44
|
+
expect(resolveUseBlock({ block: false, output: "diagram.md" })).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("infers true for .md output", () => {
|
|
47
|
+
expect(resolveUseBlock({ output: "diagram.md" })).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("infers true for .markdown output", () => {
|
|
50
|
+
expect(resolveUseBlock({ output: "diagram.markdown" })).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("infers false for .mmd output", () => {
|
|
53
|
+
expect(resolveUseBlock({ output: "diagram.mmd" })).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it("infers false for .mermaid output", () => {
|
|
56
|
+
expect(resolveUseBlock({ output: "diagram.mermaid" })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it("infers false for any other extension (.txt, no ext, etc.)", () => {
|
|
59
|
+
expect(resolveUseBlock({ output: "diagram.txt" })).toBe(false);
|
|
60
|
+
expect(resolveUseBlock({ output: "diagram" })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it("is case-insensitive on extension", () => {
|
|
63
|
+
expect(resolveUseBlock({ output: "Diagram.MD" })).toBe(true);
|
|
64
|
+
expect(resolveUseBlock({ output: "Diagram.MMD" })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it("lets explicit --block override .mmd extension", () => {
|
|
67
|
+
expect(resolveUseBlock({ block: true, output: "diagram.mmd" })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
35
70
|
describe("runWorkflowDiagram", () => {
|
|
36
71
|
let tmpdir;
|
|
37
72
|
const loadWorkflowFile = vi.fn((_f) => sampleWorkflow);
|
|
@@ -44,55 +79,78 @@ describe("runWorkflowDiagram", () => {
|
|
|
44
79
|
afterEach(() => {
|
|
45
80
|
fs.rmSync(tmpdir, { recursive: true, force: true });
|
|
46
81
|
});
|
|
47
|
-
describe("stdout", () => {
|
|
48
|
-
it("writes
|
|
82
|
+
describe("stdout — default behavior", () => {
|
|
83
|
+
it("writes RAW mermaid (no fence, no title frontmatter) by default", () => {
|
|
84
|
+
// This is the Mermaid-CLI / Live-Editor-compatible output. No ```,
|
|
85
|
+
// no `---\ntitle: …\n---` — just `graph TB` and the nodes.
|
|
49
86
|
const stdout = captureStream();
|
|
50
87
|
const stderr = captureStream();
|
|
51
88
|
runWorkflowDiagram("any.yml", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: stderr.stream, exit });
|
|
52
|
-
expect(stdout.text).toMatch(
|
|
53
|
-
expect(stdout.text).toMatch(
|
|
89
|
+
expect(stdout.text).not.toMatch(/```mermaid/);
|
|
90
|
+
expect(stdout.text).not.toMatch(/^---\ntitle:/);
|
|
91
|
+
expect(stdout.text).toMatch(/^graph TB/);
|
|
54
92
|
expect(exit).not.toHaveBeenCalled();
|
|
55
93
|
});
|
|
56
|
-
it("
|
|
94
|
+
it("wraps in a ```mermaid fence when --block is explicit", () => {
|
|
57
95
|
const stdout = captureStream();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(stdout.text).
|
|
61
|
-
expect(stdout.text).toMatch(/graph TB/);
|
|
96
|
+
runWorkflowDiagram("any.yml", { block: true }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
97
|
+
expect(stdout.text).toMatch(/^```mermaid\n/);
|
|
98
|
+
expect(stdout.text).toMatch(/\n```\n$/);
|
|
62
99
|
});
|
|
63
100
|
it("honors --direction LR", () => {
|
|
64
101
|
const stdout = captureStream();
|
|
65
102
|
runWorkflowDiagram("any.yml", { direction: "LR" }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
66
103
|
expect(stdout.text).toMatch(/graph LR/);
|
|
67
104
|
});
|
|
68
|
-
it("
|
|
105
|
+
it("emits --title as frontmatter only when explicitly set", () => {
|
|
69
106
|
const stdout = captureStream();
|
|
70
107
|
runWorkflowDiagram("any.yml", { title: "Custom Title" }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
71
|
-
expect(stdout.text).
|
|
108
|
+
expect(stdout.text).toMatch(/^---\ntitle: Custom Title\n---\n/);
|
|
109
|
+
expect(stdout.text).toContain("graph TB");
|
|
110
|
+
});
|
|
111
|
+
it("does NOT default title to the workflow name", () => {
|
|
112
|
+
// Regression guard: previously we auto-filled `title` from workflow.name,
|
|
113
|
+
// which broke mmdc / mermaid-live because they don't accept title frontmatter.
|
|
114
|
+
const stdout = captureStream();
|
|
115
|
+
runWorkflowDiagram("any.yml", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
116
|
+
expect(stdout.text).not.toMatch(/title:/);
|
|
117
|
+
expect(stdout.text).not.toContain("Sample"); // workflow.name
|
|
72
118
|
});
|
|
73
119
|
});
|
|
74
120
|
describe("-o / --output file", () => {
|
|
75
|
-
it("writes raw mermaid to .mmd
|
|
121
|
+
it("writes raw mermaid to .mmd (default)", () => {
|
|
76
122
|
const out = path.join(tmpdir, "diagram.mmd");
|
|
77
123
|
const stderr = captureStream();
|
|
78
124
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: stderr.stream, exit });
|
|
79
125
|
const contents = fs.readFileSync(out, "utf8");
|
|
80
126
|
expect(contents).not.toMatch(/```mermaid/);
|
|
81
|
-
expect(contents).toMatch(/
|
|
127
|
+
expect(contents).not.toMatch(/title:/);
|
|
128
|
+
expect(contents).toMatch(/^graph TB/);
|
|
82
129
|
expect(stderr.text).toMatch(/✓ Wrote diagram to/);
|
|
83
130
|
});
|
|
84
|
-
it("writes
|
|
131
|
+
it("writes raw mermaid to .mermaid (Mermaid CLI's canonical extension)", () => {
|
|
132
|
+
const out = path.join(tmpdir, "diagram.mermaid");
|
|
133
|
+
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
134
|
+
const contents = fs.readFileSync(out, "utf8");
|
|
135
|
+
expect(contents).not.toMatch(/```mermaid/);
|
|
136
|
+
expect(contents).toMatch(/^graph TB/);
|
|
137
|
+
});
|
|
138
|
+
it("writes fenced markdown to .md", () => {
|
|
85
139
|
const out = path.join(tmpdir, "diagram.md");
|
|
86
140
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
87
141
|
const contents = fs.readFileSync(out, "utf8");
|
|
88
142
|
expect(contents).toMatch(/^```mermaid\n/);
|
|
89
143
|
expect(contents).toMatch(/\n```\n$/);
|
|
90
144
|
});
|
|
91
|
-
it("
|
|
145
|
+
it("writes raw mermaid for unknown extensions (no longer falls back to fenced)", () => {
|
|
146
|
+
// Behavior change: previously .txt and friends got fenced as the
|
|
147
|
+
// "safe default". Now raw mermaid is the safe default because that's
|
|
148
|
+
// what every renderer actually accepts.
|
|
92
149
|
const out = path.join(tmpdir, "diagram.txt");
|
|
93
150
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
94
151
|
const contents = fs.readFileSync(out, "utf8");
|
|
95
|
-
expect(contents).toMatch(/```mermaid/);
|
|
152
|
+
expect(contents).not.toMatch(/```mermaid/);
|
|
153
|
+
expect(contents).toMatch(/graph TB/);
|
|
96
154
|
});
|
|
97
155
|
it("creates parent directories as needed (-o nested/subdir/file.mmd)", () => {
|
|
98
156
|
const out = path.join(tmpdir, "a", "b", "c", "diagram.mmd");
|
|
@@ -107,51 +165,15 @@ describe("runWorkflowDiagram", () => {
|
|
|
107
165
|
expect(stdout.text).toBe("");
|
|
108
166
|
});
|
|
109
167
|
});
|
|
110
|
-
describe("explicit --block / --no-block
|
|
111
|
-
|
|
112
|
-
const cmd = new Command();
|
|
113
|
-
cmd.option("--block", "fenced", true);
|
|
114
|
-
cmd.option("--no-block", "raw");
|
|
115
|
-
if (explicit) {
|
|
116
|
-
// Simulate user typing `--block` or `--no-block` on the CLI — commander
|
|
117
|
-
// marks the source as "cli" when the flag is seen in argv.
|
|
118
|
-
cmd.setOptionValueWithSource("block", block, "cli");
|
|
119
|
-
}
|
|
120
|
-
return cmd;
|
|
121
|
-
}
|
|
122
|
-
it("explicit --block wins over .mmd extension (writes fenced to .mmd)", () => {
|
|
168
|
+
describe("explicit --block / --no-block override extension inference", () => {
|
|
169
|
+
it("explicit --block wins over .mmd (writes fenced to .mmd)", () => {
|
|
123
170
|
const out = path.join(tmpdir, "diagram.mmd");
|
|
124
|
-
runWorkflowDiagram("any.yml", { output: out, block: true }, {
|
|
125
|
-
loadWorkflowFile,
|
|
126
|
-
command: cmdWithBlock(true, true),
|
|
127
|
-
stdout: captureStream().stream,
|
|
128
|
-
stderr: captureStream().stream,
|
|
129
|
-
exit,
|
|
130
|
-
});
|
|
171
|
+
runWorkflowDiagram("any.yml", { output: out, block: true }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
131
172
|
expect(fs.readFileSync(out, "utf8")).toMatch(/```mermaid/);
|
|
132
173
|
});
|
|
133
|
-
it("explicit --no-block wins over .md
|
|
174
|
+
it("explicit --no-block wins over .md (writes raw to .md)", () => {
|
|
134
175
|
const out = path.join(tmpdir, "diagram.md");
|
|
135
|
-
runWorkflowDiagram("any.yml", { output: out, block: false }, {
|
|
136
|
-
loadWorkflowFile,
|
|
137
|
-
command: cmdWithBlock(false, true),
|
|
138
|
-
stdout: captureStream().stream,
|
|
139
|
-
stderr: captureStream().stream,
|
|
140
|
-
exit,
|
|
141
|
-
});
|
|
142
|
-
expect(fs.readFileSync(out, "utf8")).not.toMatch(/```mermaid/);
|
|
143
|
-
});
|
|
144
|
-
it("default --block source does not suppress .mmd extension inference", () => {
|
|
145
|
-
// Commander always sets `block: true` via the declared default, but its
|
|
146
|
-
// source is "default" — our inference should still kick in for .mmd.
|
|
147
|
-
const out = path.join(tmpdir, "diagram.mmd");
|
|
148
|
-
runWorkflowDiagram("any.yml", { output: out, block: true }, {
|
|
149
|
-
loadWorkflowFile,
|
|
150
|
-
command: cmdWithBlock(true, false), // not explicit
|
|
151
|
-
stdout: captureStream().stream,
|
|
152
|
-
stderr: captureStream().stream,
|
|
153
|
-
exit,
|
|
154
|
-
});
|
|
176
|
+
runWorkflowDiagram("any.yml", { output: out, block: false }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
155
177
|
expect(fs.readFileSync(out, "utf8")).not.toMatch(/```mermaid/);
|
|
156
178
|
});
|
|
157
179
|
});
|
|
@@ -160,7 +182,9 @@ describe("runWorkflowDiagram", () => {
|
|
|
160
182
|
const stdout = captureStream();
|
|
161
183
|
runWorkflowDiagram("triage", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
162
184
|
expect(loadWorkflowFile).not.toHaveBeenCalled();
|
|
163
|
-
expect(stdout.text).toMatch(
|
|
185
|
+
expect(stdout.text).toMatch(/graph TB/);
|
|
186
|
+
// Confirm built-in renders raw by default too.
|
|
187
|
+
expect(stdout.text).not.toMatch(/```mermaid/);
|
|
164
188
|
});
|
|
165
189
|
it("resolves 'implement' without calling loadWorkflowFile", () => {
|
|
166
190
|
const stdout = captureStream();
|
|
@@ -158,6 +158,17 @@ describe("runUpgrade — no-op paths", () => {
|
|
|
158
158
|
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
159
159
|
expect(exit).toHaveBeenCalledWith(0);
|
|
160
160
|
});
|
|
161
|
+
it("downgrades when --force is set against a local-dev build ahead of the registry", async () => {
|
|
162
|
+
const { deps, runInstall, exit } = makeDeps({
|
|
163
|
+
currentVersion: "0.2.0",
|
|
164
|
+
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
165
|
+
});
|
|
166
|
+
await runUpgrade({ force: true }, deps);
|
|
167
|
+
// --force explicitly overrides the "ahead" short-circuit — this is how
|
|
168
|
+
// someone pins back to the published train from a stale local build.
|
|
169
|
+
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
170
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
171
|
+
});
|
|
161
172
|
});
|
|
162
173
|
describe("runUpgrade — install dispatch", () => {
|
|
163
174
|
it("invokes the detected package manager and exits 0 on success", async () => {
|
|
@@ -166,6 +166,23 @@ describe("maybeNudge", () => {
|
|
|
166
166
|
expect(readCache(cachePath)).toBeNull(); // fetch failed, nothing written
|
|
167
167
|
expect(stderr.text).toBe(""); // silent on error
|
|
168
168
|
});
|
|
169
|
+
it("gives up silently when the registry is slower than the budget", async () => {
|
|
170
|
+
// The race between fetch and the internal timer means a slow fetch should
|
|
171
|
+
// resolve to null → no cache write → no nudge. We pin down the behavior
|
|
172
|
+
// here so a future regression (forgetting the race or the timeout) gets
|
|
173
|
+
// caught even though the budget is configured in the module itself.
|
|
174
|
+
const slowFetch = vi.fn(() => new Promise((resolve) => {
|
|
175
|
+
// Never resolves within the test — the internal timer is what wins.
|
|
176
|
+
const t = setTimeout(() => resolve("9.9.9"), 10_000);
|
|
177
|
+
if (typeof t.unref === "function") {
|
|
178
|
+
t.unref();
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
await maybeNudge(baseDeps({ fetchLatestVersion: slowFetch }));
|
|
182
|
+
expect(slowFetch).toHaveBeenCalled();
|
|
183
|
+
expect(readCache(cachePath)).toBeNull();
|
|
184
|
+
expect(stderr.text).toBe("");
|
|
185
|
+
});
|
|
169
186
|
it("does not refresh when cache is fresh (saves bandwidth)", async () => {
|
|
170
187
|
writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
|
|
171
188
|
await maybeNudge(baseDeps({ now: 1_000_000 + 1 }));
|
package/dist/cli/diagram.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
1
|
import type { Workflow } from "../types.js";
|
|
3
2
|
export interface DiagramOptions {
|
|
4
3
|
direction?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Optional diagram title. When omitted we render pure Mermaid with no
|
|
6
|
+
* `---\ntitle: …\n---` frontmatter — the format that `mmdc`, Mermaid Live
|
|
7
|
+
* Editor, and `@mermaid-js/mermaid-cli` all accept as-is.
|
|
8
|
+
*/
|
|
5
9
|
title?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap output in a ```mermaid fenced code block. Tri-state:
|
|
12
|
+
* - `true` → fenced (forced by explicit `--block`)
|
|
13
|
+
* - `false` → raw (forced by explicit `--no-block`)
|
|
14
|
+
* - `undefined` → inferred from `--output` extension; default is raw
|
|
15
|
+
*/
|
|
6
16
|
block?: boolean;
|
|
7
17
|
output?: string;
|
|
8
18
|
}
|
|
9
19
|
export interface DiagramDeps {
|
|
10
20
|
/** Load a workflow file (YAML or JSON). Injected so tests can stub. */
|
|
11
21
|
loadWorkflowFile: (file: string) => Workflow;
|
|
12
|
-
/** Commander instance used to detect whether --block was set explicitly. */
|
|
13
|
-
command?: Command;
|
|
14
22
|
/** Stream for success/error messages. Defaults to process.stderr. */
|
|
15
23
|
stderr?: NodeJS.WritableStream;
|
|
16
24
|
/** Stream for diagram output when no --output is set. Defaults to process.stdout. */
|
|
@@ -18,13 +26,24 @@ export interface DiagramDeps {
|
|
|
18
26
|
/** Exit handler. Defaults to process.exit. Tests can throw instead. */
|
|
19
27
|
exit?: (code: number) => void;
|
|
20
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve whether output should be wrapped in a ```mermaid fenced block.
|
|
31
|
+
*
|
|
32
|
+
* Precedence:
|
|
33
|
+
* 1. Explicit `--block` / `--no-block` always wins.
|
|
34
|
+
* 2. When `-o` points at `.md`/`.markdown` we fence (markdown context).
|
|
35
|
+
* 3. Everything else — stdout, `.mmd`, `.mermaid`, unknown extensions —
|
|
36
|
+
* emits raw Mermaid syntax that Mermaid CLI / Live Editor / mmdc can
|
|
37
|
+
* consume directly with no preprocessing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveUseBlock(options: DiagramOptions): boolean;
|
|
21
40
|
/**
|
|
22
41
|
* Render a workflow as a Mermaid diagram and write to stdout or a file.
|
|
23
42
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
43
|
+
* By default the output is pure Mermaid — no ```mermaid fence, no
|
|
44
|
+
* `---\ntitle: …\n---` frontmatter — so the result is directly consumable
|
|
45
|
+
* by Mermaid CLI (`mmdc`) and the Live Editor. Pass `--block` to wrap in a
|
|
46
|
+
* fenced code block for README embedding, or `--title` to inject a title
|
|
47
|
+
* header for Mermaid renderers that support it.
|
|
29
48
|
*/
|
|
30
49
|
export declare function runWorkflowDiagram(file: string, options: DiagramOptions, deps: DiagramDeps): void;
|
package/dist/cli/diagram.js
CHANGED
|
@@ -3,14 +3,37 @@ import * as path from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { toMermaid, toMermaidBlock } from "../mermaid.js";
|
|
5
5
|
import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
|
|
6
|
+
const MARKDOWN_EXTS = new Set([".md", ".markdown"]);
|
|
7
|
+
/**
|
|
8
|
+
* Resolve whether output should be wrapped in a ```mermaid fenced block.
|
|
9
|
+
*
|
|
10
|
+
* Precedence:
|
|
11
|
+
* 1. Explicit `--block` / `--no-block` always wins.
|
|
12
|
+
* 2. When `-o` points at `.md`/`.markdown` we fence (markdown context).
|
|
13
|
+
* 3. Everything else — stdout, `.mmd`, `.mermaid`, unknown extensions —
|
|
14
|
+
* emits raw Mermaid syntax that Mermaid CLI / Live Editor / mmdc can
|
|
15
|
+
* consume directly with no preprocessing.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveUseBlock(options) {
|
|
18
|
+
if (options.block === true)
|
|
19
|
+
return true;
|
|
20
|
+
if (options.block === false)
|
|
21
|
+
return false;
|
|
22
|
+
if (options.output) {
|
|
23
|
+
const ext = path.extname(options.output).toLowerCase();
|
|
24
|
+
if (MARKDOWN_EXTS.has(ext))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* Render a workflow as a Mermaid diagram and write to stdout or a file.
|
|
8
31
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
32
|
+
* By default the output is pure Mermaid — no ```mermaid fence, no
|
|
33
|
+
* `---\ntitle: …\n---` frontmatter — so the result is directly consumable
|
|
34
|
+
* by Mermaid CLI (`mmdc`) and the Live Editor. Pass `--block` to wrap in a
|
|
35
|
+
* fenced code block for README embedding, or `--title` to inject a title
|
|
36
|
+
* header for Mermaid renderers that support it.
|
|
14
37
|
*/
|
|
15
38
|
export function runWorkflowDiagram(file, options, deps) {
|
|
16
39
|
const stderr = deps.stderr ?? process.stderr;
|
|
@@ -38,21 +61,14 @@ export function runWorkflowDiagram(file, options, deps) {
|
|
|
38
61
|
}
|
|
39
62
|
}
|
|
40
63
|
const direction = (options.direction === "LR" ? "LR" : "TB");
|
|
41
|
-
const
|
|
42
|
-
// Explicit --block / --no-block always wins. When commander's source is
|
|
43
|
-
// "cli" or "env" the user (or env var) set it; otherwise we may infer.
|
|
44
|
-
const blockSource = deps.command?.getOptionValueSource("block");
|
|
45
|
-
const blockExplicit = blockSource === "cli" || blockSource === "env";
|
|
46
|
-
let useBlock = options.block !== false;
|
|
47
|
-
if (!blockExplicit && options.output) {
|
|
48
|
-
const ext = path.extname(options.output).toLowerCase();
|
|
49
|
-
if (ext === ".mmd")
|
|
50
|
-
useBlock = false;
|
|
51
|
-
else if (ext === ".md")
|
|
52
|
-
useBlock = true;
|
|
53
|
-
}
|
|
64
|
+
const useBlock = resolveUseBlock(options);
|
|
54
65
|
const render = useBlock ? toMermaidBlock : toMermaid;
|
|
55
|
-
|
|
66
|
+
// Title is opt-in only — an unset `--title` produces no frontmatter. Users
|
|
67
|
+
// who want the workflow.name as title can pass `--title "$(name)"`.
|
|
68
|
+
const renderOpts = { direction };
|
|
69
|
+
if (options.title)
|
|
70
|
+
renderOpts.title = options.title;
|
|
71
|
+
const output = render(workflow, renderOpts) + "\n";
|
|
56
72
|
if (options.output) {
|
|
57
73
|
try {
|
|
58
74
|
// Create parent dirs so `-o some/nested/diagram.mmd` works without a
|
package/dist/cli/main.js
CHANGED
|
@@ -799,14 +799,14 @@ workflowCmd
|
|
|
799
799
|
.action(workflowRunAction);
|
|
800
800
|
workflowCmd
|
|
801
801
|
.command("diagram <file>")
|
|
802
|
-
.description("Render a workflow as a Mermaid diagram")
|
|
802
|
+
.description("Render a workflow as a Mermaid diagram (raw .mmd by default; .md output auto-fences)")
|
|
803
803
|
.option("--direction <dir>", "Graph direction: TB (top-bottom) or LR (left-right)", "TB")
|
|
804
|
-
.option("--title <title>", "
|
|
805
|
-
.option("--block", "Wrap in ```mermaid fenced code block (
|
|
806
|
-
.option("--no-block", "
|
|
807
|
-
.option("-o, --output <path>", "Write
|
|
808
|
-
.action(
|
|
809
|
-
runWorkflowDiagram(file, options, { loadWorkflowFile
|
|
804
|
+
.option("--title <title>", "Inject a title header (off by default — raw Mermaid has no title)")
|
|
805
|
+
.option("--block", "Wrap in ```mermaid fenced code block (forces fencing in any output)")
|
|
806
|
+
.option("--no-block", "Force raw Mermaid even when writing to a .md file")
|
|
807
|
+
.option("-o, --output <path>", "Write to a file instead of stdout (.mmd/.mermaid raw; .md fenced)")
|
|
808
|
+
.action((file, options) => {
|
|
809
|
+
runWorkflowDiagram(file, options, { loadWorkflowFile });
|
|
810
810
|
});
|
|
811
811
|
workflowCmd
|
|
812
812
|
.command("export <name>")
|
|
@@ -967,7 +967,7 @@ workflowCmd
|
|
|
967
967
|
// ── sweny upgrade / update ────────────────────────────────────────────
|
|
968
968
|
// Self-update the globally-installed @sweny-ai/core. Mirrors the UX of
|
|
969
969
|
// `bun upgrade`, `deno upgrade`, `rustup update`, etc.
|
|
970
|
-
|
|
970
|
+
program
|
|
971
971
|
.command("upgrade")
|
|
972
972
|
.alias("update")
|
|
973
973
|
.description("Upgrade sweny to the latest published version")
|
|
@@ -991,7 +991,13 @@ const upgradeCmd = program
|
|
|
991
991
|
installPath,
|
|
992
992
|
fetchLatestVersion: fetchLatestFromNpm,
|
|
993
993
|
runInstall: (cmd, args) => {
|
|
994
|
-
|
|
994
|
+
// Windows: `npm`/`pnpm`/`yarn` are `.cmd` shims and Node's spawn can't
|
|
995
|
+
// resolve them without a shell. Everywhere else we skip the shell to
|
|
996
|
+
// keep argv unambiguous and avoid injection surface.
|
|
997
|
+
const res = spawnSync(cmd, args, {
|
|
998
|
+
stdio: "inherit",
|
|
999
|
+
shell: process.platform === "win32",
|
|
1000
|
+
});
|
|
995
1001
|
if (res.error) {
|
|
996
1002
|
process.stderr.write(chalk.red(` Error: couldn't run ${cmd} — ${res.error.message}`) + "\n");
|
|
997
1003
|
return 127;
|
|
@@ -1009,8 +1015,6 @@ const upgradeCmd = program
|
|
|
1009
1015
|
},
|
|
1010
1016
|
});
|
|
1011
1017
|
});
|
|
1012
|
-
// Touch upgradeCmd so TS/ESLint don't flag it unused if someone tightens rules.
|
|
1013
|
-
void upgradeCmd;
|
|
1014
1018
|
// ── Passive "new version available" nudge ─────────────────────────────
|
|
1015
1019
|
// Runs after every command. Non-blocking, bounded to 1.5s, silent on any
|
|
1016
1020
|
// failure, skipped in CI / non-TTY / opt-out environments.
|