flowfig 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -24
- package/dist/check.js +6 -1
- package/dist/cli.js +107 -49
- package/dist/diff.d.ts +9 -0
- package/dist/diff.js +59 -0
- package/dist/guide.d.ts +2 -2
- package/dist/guide.js +25 -10
- package/dist/index.js +1 -0
- package/dist/init.d.ts +15 -0
- package/dist/init.js +92 -6
- package/dist/load.d.ts +12 -0
- package/dist/load.js +59 -0
- package/dist/mcp.d.ts +139 -0
- package/dist/mcp.js +216 -0
- package/dist/model.d.ts +7 -0
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.js +24 -0
- package/dist/svg.d.ts +2 -0
- package/dist/svg.js +3 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +36 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +5 -1
package/dist/guide.js
CHANGED
|
@@ -63,6 +63,7 @@ If the code does not show a fact, leave the fact out. Do not guess a part, a nam
|
|
|
63
63
|
|
|
64
64
|
Each fact becomes a box, an edge, a beat, a \`data\` card or a \`show\` row. Use the real names from the code as labels, and
|
|
65
65
|
real example data from the code or its tests. Keep each fact that you leave out, with the reason, for the reply.
|
|
66
|
+
Give each box and edge that draws code a \`source\`, from the fact list: \`"src/auth/login.ts#verifyPassword"\`. A store or an outside part may have none.
|
|
66
67
|
|
|
67
68
|
Mermaid mapping:
|
|
68
69
|
|
|
@@ -89,8 +90,10 @@ command writes no SVG and lists the faults. Fix each fault and render again.
|
|
|
89
90
|
|
|
90
91
|
- Fix a layout fault by changing the layout first: put parts in rows or columns, route an edge \`around\`, or change a gap.
|
|
91
92
|
Never remove a fact from the fact list to pass the check. If you must remove one, name it in the reply.
|
|
92
|
-
- If \`small-text\` appears, the figure is too wide.
|
|
93
|
-
you make a second figure.
|
|
93
|
+
- If \`small-text\` appears, the figure is too wide. Put the parts in two rows, move a detail from \`label\` to \`sub\`, or split
|
|
94
|
+
the figure. Use more steps in one figure before you make a second figure. Keep the real name from the code in the label:
|
|
95
|
+
\`ReportsController\` stays \`ReportsController\`, not \`Controller\`. Do not change \`--width\` to pass the check: the
|
|
96
|
+
reader sees the figure at the real page width.
|
|
94
97
|
- Fix every other warning, or tell the user why it stays.
|
|
95
98
|
- Do not use \`--no-check\` to hide a fault.
|
|
96
99
|
|
|
@@ -99,6 +102,8 @@ command writes no SVG and lists the faults. Fix each fault and render again.
|
|
|
99
102
|
Run \`npx flowfig --spec out.svg\`. Compare the spec with the fact list. Check that each edge goes from the caller to the callee
|
|
100
103
|
that its fact names. Add each fact that is missing, fix each wrong edge, and render again.
|
|
101
104
|
|
|
105
|
+
Run \`npx flowfig verify out.svg\` from the repo root. Fix each \`missing-file\` or \`missing-symbol\`.
|
|
106
|
+
|
|
102
107
|
### 6. Look at the SVG
|
|
103
108
|
|
|
104
109
|
The check estimates the text width, so it cannot see everything. If you can open a browser, open the SVG and take a
|
|
@@ -115,7 +120,8 @@ The reply has these parts, in this order:
|
|
|
115
120
|
from the read-back spec. Never count from memory;
|
|
116
121
|
3. the scope: the flow you drew, why, and the other flows that exist;
|
|
117
122
|
4. what the figure leaves out, and why;
|
|
118
|
-
5. the check
|
|
123
|
+
5. the two check lines that the render printed, \`0 errors, 0 warnings\` and \`figure: ...\`, copied as printed, and whether
|
|
124
|
+
you looked at the SVG.
|
|
119
125
|
|
|
120
126
|
To change an SVG later, print its spec with \`--spec\`, change the spec, and render again.
|
|
121
127
|
|
|
@@ -151,6 +157,7 @@ Every other item is a box.
|
|
|
151
157
|
| \`sub\` | A smaller line under the label. |
|
|
152
158
|
| \`shape\` | \`"box"\` (default), \`"decision"\` (a diamond) or \`"store"\` (a database cylinder for data at rest). |
|
|
153
159
|
| \`width\` | Width in px. This overrides the width that the layout picks. |
|
|
160
|
+
| \`source\` | The code this draws: \`path\` or \`path#symbol\`, relative to the repo root. \`flowfig verify\` checks it. |
|
|
154
161
|
| \`lines\` | The least number of text lines that a content card keeps. The card still grows to fit its content. |
|
|
155
162
|
|
|
156
163
|
### edges
|
|
@@ -162,6 +169,7 @@ An edge has these fields.
|
|
|
162
169
|
- \`label\`: text on the edge, drawn as a small pill.
|
|
163
170
|
- \`around\`: \`"above"\` or \`"below"\`. This routes the edge over or under the boxes in between (loops, skip-ahead edges).
|
|
164
171
|
- \`quiet\`: if \`true\`, the SVG draws the edge only while a step uses it.
|
|
172
|
+
- \`source\`: the code this edge draws, as \`path\` or \`path#symbol\`. \`flowfig verify\` checks it.
|
|
165
173
|
|
|
166
174
|
### steps
|
|
167
175
|
|
|
@@ -176,7 +184,7 @@ boxes to highlight for the whole step).
|
|
|
176
184
|
|
|
177
185
|
The beat fields:
|
|
178
186
|
|
|
179
|
-
- \`edges\`: an edge id, a hop object, or an array of these. A hop object is \`{ edge, back?, data?, async? }\`.
|
|
187
|
+
- \`edges\`: an edge id, a hop object, or an array of these. A hop object is \`{ edge, back?, data?, async?, source? }\`.
|
|
180
188
|
- \`back: true\` runs the packet from the \`to\` box to the \`from\` box.
|
|
181
189
|
- \`data\` is a small card that moves with the packet.
|
|
182
190
|
- \`async: true\` marks a message that does not wait for an answer. The rail draws it dashed, with an \`async\` tag.
|
|
@@ -199,7 +207,11 @@ A row of a content card is \`{ tag?, tone?, text, meta?, mark?, mono? }\`.
|
|
|
199
207
|
- Give every box a stable \`id\`.
|
|
200
208
|
- Use \`shape: "store"\` for data at rest (a database, a cache, a file, a queue). Use a plain box for a part that does work. Use \`shape: "decision"\` for a branch.
|
|
201
209
|
- Keep one clear main path, left to right. Put side parts in a \`column\` group.
|
|
202
|
-
- Use \`quiet: true\` on a long edge that crosses the picture.
|
|
210
|
+
- Use \`quiet: true\` on a long edge that crosses the picture. A quiet edge needs a beat that uses it, or the check reports
|
|
211
|
+
\`hidden-edge\`.
|
|
212
|
+
- In a rail, draw an error reply as a hop back on the same edge, with the error in \`data\` (\`{ "edge": "login", "back": true,
|
|
213
|
+
"data": "401 CREDENTIALS_INVALID" }\`). A call that stays inside one part (a hash check, a guard) is not a message: put it in
|
|
214
|
+
\`say\` or in a \`show\` row of that part.
|
|
203
215
|
- Keep an SVG to one or two steps. The SVG plays every step in a loop, with no controls.
|
|
204
216
|
- Use the React player, \`<Flow {...props} />\` from \`flowfig\`, for a page that needs tabs, pause or hover.
|
|
205
217
|
|
|
@@ -240,6 +252,8 @@ npx flowfig - out.svg < spec.json # render a spec from stdin
|
|
|
240
252
|
npx flowfig spec.json out.svg # render a spec file
|
|
241
253
|
npx flowfig figure.ts out.svg # render a module whose default export is a spec
|
|
242
254
|
npx flowfig check <input> [--json] # list the faults. The input can also be an SVG that flowfig wrote.
|
|
255
|
+
npx flowfig verify <input>... # check that the code each source names still exists
|
|
256
|
+
npx flowfig diff <old> <new> # list what changed in the spec between two figures
|
|
243
257
|
npx flowfig --spec out.svg # print the spec that an SVG carries
|
|
244
258
|
npx flowfig docs # print this guide
|
|
245
259
|
npx flowfig init # write these instructions for the coding agents of a repo
|
|
@@ -266,15 +280,16 @@ flowfig does not draw class or ER diagrams, Gantt charts or charts of numbers. F
|
|
|
266
280
|
3. Write a fact list from the code and from its wiring (config, compose, route and queue files). Write each call as
|
|
267
281
|
\`caller → callee: payload (file:line of the call)\`, in order. Include each async call and every consumer of a queue, each
|
|
268
282
|
branch and error path, and each end state. Leave out what the code does not show.
|
|
269
|
-
4. Write the spec from the fact list, with the real names and data. Render it with \`npx flowfig - out.svg
|
|
270
|
-
changing the layout first (rows, \`around\`, the gap). Never remove a fact to pass
|
|
271
|
-
in the reply. If \`small-text\` appears, split the figure;
|
|
283
|
+
4. Write the spec from the fact list, with the real names and data. Render it with \`npx flowfig - out.svg <<'SPEC'\` and the
|
|
284
|
+
JSON in the heredoc. Fix every fault by changing the layout first (rows, \`around\`, the gap). Never remove a fact to pass
|
|
285
|
+
the check; if you must remove one, name it in the reply. If \`small-text\` appears, use two rows or split the figure; keep
|
|
286
|
+
the real names; do not change \`--width\`. Give each box and edge that draws code a \`source\` (\`file#symbol\`) from the fact list.
|
|
272
287
|
5. Run \`npx flowfig --spec out.svg\` and compare it with the fact list: each edge must go from the caller to the callee that the
|
|
273
|
-
fact names. Fix each gap.
|
|
288
|
+
fact names. Fix each gap. Then run \`npx flowfig verify out.svg\` from the repo root and fix each fault.
|
|
274
289
|
6. If you can open a browser, look at the SVG at two moments of the loop. If you cannot, write "not looked at" in the reply.
|
|
275
290
|
Never report a look that you did not do.
|
|
276
291
|
7. Reply with: the SVG path; what it shows, with the counts copied from the \`figure:\` line and the names copied from the read-back spec; what it leaves out and why;
|
|
277
|
-
the
|
|
292
|
+
the two lines the render printed, \`0 errors, 0 warnings\` and \`figure: ...\`, copied as printed.
|
|
278
293
|
`;
|
|
279
294
|
/** The description of the Claude skill: the words that make the agent load it. */
|
|
280
295
|
export const SKILL_DESCRIPTION = 'Use when the user asks for a diagram, figure or animation of how code or a system works: an architecture, a data flow, a pipeline, a request lifecycle, a call sequence, a state lifecycle or a flowchart; when the user pastes a Mermaid flowchart, stateDiagram or sequenceDiagram to convert; or when the user asks for a class, ER or Gantt diagram.';
|
package/dist/index.js
CHANGED
package/dist/init.d.ts
CHANGED
|
@@ -10,6 +10,19 @@ type Agent = {
|
|
|
10
10
|
/** Text before the marked block in a whole file. */
|
|
11
11
|
head?: string;
|
|
12
12
|
note?: string;
|
|
13
|
+
/** The MCP config file the agent reads, and the top-level key that holds the servers. `type` adds `"type": "stdio"`. */
|
|
14
|
+
mcp?: {
|
|
15
|
+
file: string;
|
|
16
|
+
key: 'mcpServers' | 'servers';
|
|
17
|
+
type?: true;
|
|
18
|
+
global?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Shown in place of the MCP file when init cannot write the config. */
|
|
21
|
+
mcpNote?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const MCP_ENTRY: {
|
|
24
|
+
command: string;
|
|
25
|
+
args: string[];
|
|
13
26
|
};
|
|
14
27
|
export declare const AGENTS: Agent[];
|
|
15
28
|
/** The ids of the agents that the repo at `dir` already uses. */
|
|
@@ -20,6 +33,8 @@ export declare function toggle(input: string, selected: Set<string>, count: numb
|
|
|
20
33
|
export declare function renderList(selected: Set<string>, detected: string[], dir: string): string;
|
|
21
34
|
/** The new file text, or `undefined` if the file belongs to the user and init must leave it. `old` is `undefined` for a missing file. */
|
|
22
35
|
export declare function place(a: Agent, old: string | undefined): string | undefined;
|
|
36
|
+
/** The MCP file with the flowfig entry merged in, or `undefined` when the file is not a JSON object that can hold it. */
|
|
37
|
+
export declare function registerMcp(a: Agent, old: string | undefined): string | undefined;
|
|
23
38
|
/** Run `flowfig init`. Returns the exit code. */
|
|
24
39
|
export declare function runInit(argv: string[]): Promise<number>;
|
|
25
40
|
export {};
|
package/dist/init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// `flowfig init`: write flowfig instructions for the coding agents of a repo. Pure functions where possible, so the tests need no TTY.
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { AGENT_TEXT, SKILL_DESCRIPTION } from './guide.js';
|
|
7
7
|
const START = '<!-- flowfig:start -->';
|
|
8
8
|
const END = '<!-- flowfig:end -->';
|
|
9
|
+
export const MCP_ENTRY = { command: 'npx', args: ['flowfig', 'mcp'] };
|
|
9
10
|
export const AGENTS = [
|
|
10
11
|
{
|
|
11
12
|
id: 'claude',
|
|
@@ -13,6 +14,7 @@ export const AGENTS = [
|
|
|
13
14
|
marks: ['.claude/', 'CLAUDE.md'],
|
|
14
15
|
file: '.claude/skills/figure/SKILL.md',
|
|
15
16
|
kind: 'whole',
|
|
17
|
+
mcp: { file: '.mcp.json', key: 'mcpServers', type: true, global: '.claude.json' },
|
|
16
18
|
head: `---\nname: figure\ndescription: ${JSON.stringify(SKILL_DESCRIPTION)}\nuser_invocable: true\n---\n\n`,
|
|
17
19
|
},
|
|
18
20
|
{ id: 'agents', name: 'AGENTS.md', marks: ['AGENTS.md'], file: 'AGENTS.md', kind: 'section', note: 'Codex, Amp, Jules, ...' },
|
|
@@ -22,6 +24,7 @@ export const AGENTS = [
|
|
|
22
24
|
marks: ['.cursor/', '.cursorrules'],
|
|
23
25
|
file: '.cursor/rules/flowfig.mdc',
|
|
24
26
|
kind: 'whole',
|
|
27
|
+
mcp: { file: '.cursor/mcp.json', key: 'mcpServers', type: true, global: '.cursor/mcp.json' },
|
|
25
28
|
head: '---\ndescription: Draw diagrams and flow figures with flowfig\nalwaysApply: false\n---\n\n',
|
|
26
29
|
},
|
|
27
30
|
{
|
|
@@ -30,10 +33,32 @@ export const AGENTS = [
|
|
|
30
33
|
marks: ['.github/copilot-instructions.md'],
|
|
31
34
|
file: '.github/copilot-instructions.md',
|
|
32
35
|
kind: 'section',
|
|
36
|
+
mcp: { file: '.vscode/mcp.json', key: 'servers', type: true },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'gemini',
|
|
40
|
+
name: 'Gemini CLI',
|
|
41
|
+
marks: ['GEMINI.md', '.gemini/'],
|
|
42
|
+
file: 'GEMINI.md',
|
|
43
|
+
kind: 'section',
|
|
44
|
+
mcp: { file: '.gemini/settings.json', key: 'mcpServers', global: '.gemini/settings.json' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'windsurf',
|
|
48
|
+
name: 'Windsurf',
|
|
49
|
+
marks: ['.windsurf/', '.windsurfrules'],
|
|
50
|
+
file: '.windsurf/rules/flowfig.md',
|
|
51
|
+
kind: 'whole',
|
|
52
|
+
mcpNote: 'add {"command":"npx","args":["flowfig","mcp"]} to the MCP config by hand',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'kiro',
|
|
56
|
+
name: 'Kiro',
|
|
57
|
+
marks: ['.kiro/'],
|
|
58
|
+
file: '.kiro/steering/flowfig.md',
|
|
59
|
+
kind: 'whole',
|
|
60
|
+
mcp: { file: '.kiro/settings/mcp.json', key: 'mcpServers', global: '.kiro/settings/mcp.json' },
|
|
33
61
|
},
|
|
34
|
-
{ id: 'gemini', name: 'Gemini CLI', marks: ['GEMINI.md', '.gemini/'], file: 'GEMINI.md', kind: 'section' },
|
|
35
|
-
{ id: 'windsurf', name: 'Windsurf', marks: ['.windsurf/', '.windsurfrules'], file: '.windsurf/rules/flowfig.md', kind: 'whole' },
|
|
36
|
-
{ id: 'kiro', name: 'Kiro', marks: ['.kiro/'], file: '.kiro/steering/flowfig.md', kind: 'whole' },
|
|
37
62
|
];
|
|
38
63
|
/** The ids of the agents that the repo at `dir` already uses. */
|
|
39
64
|
export const detect = (dir) => AGENTS.filter((a) => a.marks.some((m) => existsSync(join(dir, m)))).map((a) => a.id);
|
|
@@ -77,12 +102,34 @@ export function place(a, old) {
|
|
|
77
102
|
return old.slice(0, s) + mine.trimEnd() + old.slice(e + END.length);
|
|
78
103
|
return old + (old.endsWith('\n\n') ? '' : old.endsWith('\n') ? '\n' : '\n\n') + mine;
|
|
79
104
|
}
|
|
105
|
+
/** The MCP file with the flowfig entry merged in, or `undefined` when the file is not a JSON object that can hold it. */
|
|
106
|
+
export function registerMcp(a, old) {
|
|
107
|
+
if (!a.mcp)
|
|
108
|
+
return undefined;
|
|
109
|
+
let json;
|
|
110
|
+
try {
|
|
111
|
+
json = old === undefined ? {} : JSON.parse(old);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json))
|
|
117
|
+
return undefined;
|
|
118
|
+
const servers = json[a.mcp.key] ?? {};
|
|
119
|
+
if (typeof servers !== 'object' || servers === null || Array.isArray(servers))
|
|
120
|
+
return undefined;
|
|
121
|
+
if ('flowfig' in servers)
|
|
122
|
+
return old; // the user may have pinned a version or set env: keep the entry
|
|
123
|
+
const entry = a.mcp.type ? { ...MCP_ENTRY, type: 'stdio' } : MCP_ENTRY;
|
|
124
|
+
json[a.mcp.key] = { ...servers, flowfig: entry };
|
|
125
|
+
return JSON.stringify(json, null, 2) + '\n';
|
|
126
|
+
}
|
|
80
127
|
const HINT = 'run with -y or --agents <ids>';
|
|
81
128
|
/** Run `flowfig init`. Returns the exit code. */
|
|
82
129
|
export async function runInit(argv) {
|
|
83
130
|
const args = [...argv];
|
|
84
131
|
const flag = (n, ...alias) => [n, ...alias].some((x) => args.includes(x) && args.splice(args.indexOf(x), 1).length > 0);
|
|
85
|
-
const listAgents = flag('--list-agents'), all = flag('--all-agents'), yes = flag('-y', '--yes'), global = flag('--global'), dry = flag('--dry-run');
|
|
132
|
+
const listAgents = flag('--list-agents'), all = flag('--all-agents'), yes = flag('-y', '--yes'), global = flag('--global'), dry = flag('--dry-run'), noMcp = flag('--no-mcp');
|
|
86
133
|
let ids;
|
|
87
134
|
const at = args.indexOf('--agents');
|
|
88
135
|
if (at !== -1)
|
|
@@ -100,7 +147,7 @@ export async function runInit(argv) {
|
|
|
100
147
|
if (unknown)
|
|
101
148
|
return fail(`init: unknown agent "${unknown}" (ids: ${AGENTS.map((a) => a.id).join(', ')})`);
|
|
102
149
|
if (global && ids?.some((i) => i !== 'claude'))
|
|
103
|
-
return fail('init: --global writes only the
|
|
150
|
+
return fail('init: --global writes only the Claude skill and the MCP entry (use --agents claude)');
|
|
104
151
|
const detected = detect(dir);
|
|
105
152
|
const auto = new Set(detected.length ? detected : ['agents']);
|
|
106
153
|
let chosen;
|
|
@@ -142,6 +189,45 @@ export async function runInit(argv) {
|
|
|
142
189
|
}
|
|
143
190
|
}
|
|
144
191
|
console.log(`${status.padEnd(9)} ${path}`);
|
|
192
|
+
if (noMcp)
|
|
193
|
+
continue;
|
|
194
|
+
if (a.mcpNote) {
|
|
195
|
+
console.log(`note ${a.id}: ${a.mcpNote}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!a.mcp || (global && !a.mcp.global))
|
|
199
|
+
continue;
|
|
200
|
+
const mcpPath = global ? join(homedir(), a.mcp.global) : join(dir, a.mcp.file);
|
|
201
|
+
const oldMcp = existsSync(mcpPath) ? readFileSync(mcpPath, 'utf8') : undefined;
|
|
202
|
+
const nextMcp = registerMcp(a, oldMcp);
|
|
203
|
+
let mcpStatus;
|
|
204
|
+
if (nextMcp === undefined)
|
|
205
|
+
mcpStatus = `skipped (not a JSON object with ${a.mcp.key})`;
|
|
206
|
+
else if (nextMcp === oldMcp)
|
|
207
|
+
mcpStatus = 'unchanged';
|
|
208
|
+
else {
|
|
209
|
+
mcpStatus = oldMcp === undefined ? 'created' : 'updated';
|
|
210
|
+
if (dry)
|
|
211
|
+
mcpStatus = `would ${mcpStatus === 'created' ? 'create' : 'update'}`;
|
|
212
|
+
else {
|
|
213
|
+
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
214
|
+
// Same-folder temp file, then rename: a crash or a parallel write cannot leave half a file (~/.claude.json holds user settings).
|
|
215
|
+
// Resolve a symlink first, so the rename replaces the target and not the link. Copy the old mode to the new file.
|
|
216
|
+
const real = existsSync(mcpPath) ? realpathSync(mcpPath) : mcpPath;
|
|
217
|
+
const tmp = `${real}.${process.pid}.tmp`;
|
|
218
|
+
try {
|
|
219
|
+
writeFileSync(tmp, nextMcp);
|
|
220
|
+
if (existsSync(real))
|
|
221
|
+
chmodSync(tmp, statSync(real).mode & 0o777);
|
|
222
|
+
renameSync(tmp, real);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
rmSync(tmp, { force: true });
|
|
226
|
+
throw e;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
console.log(`${mcpStatus.padEnd(9)} ${mcpPath}`);
|
|
145
231
|
}
|
|
146
232
|
return 0;
|
|
147
233
|
}
|
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FlowProps } from './model.ts';
|
|
2
|
+
import { type Finding, type SvgOptions } from './svg.ts';
|
|
3
|
+
/** The spec JSON inside an SVG this package wrote. */
|
|
4
|
+
export declare function specOf(svg: string, name: string): string;
|
|
5
|
+
/** A spec from a `.json` or SVG path, or an object (`{ props }` or the props). Every error message starts with the name. */
|
|
6
|
+
export declare function loadSpec(input: string | object, name?: string): FlowProps;
|
|
7
|
+
/** The SVG with its spec in `<metadata>`: an SVG is then its own source. `]]>` would close the CDATA early, so it is escaped. */
|
|
8
|
+
export declare function svgWithSpec(props: FlowProps, opts?: SvgOptions): string;
|
|
9
|
+
/** Errors first, then by rule name. `strict` makes every warning an error. */
|
|
10
|
+
export declare function sortFindings(findings: Finding[], strict: boolean): Finding[];
|
|
11
|
+
/** One line per finding, the counts line (unless `withCounts` is false), and the `figure:` line. No color: the CLI adds it. */
|
|
12
|
+
export declare function reportLines(props: FlowProps, findings: Finding[], withCounts?: boolean): string[];
|
package/dist/load.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What the CLI and the MCP server share: read a spec, write an SVG that carries its spec, and print the check lines.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { counts } from './model.js';
|
|
4
|
+
import { toSvg } from './svg.js';
|
|
5
|
+
const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
|
|
6
|
+
const SPEC_CLOSE = ']]></metadata>';
|
|
7
|
+
/** The spec JSON inside an SVG this package wrote. */
|
|
8
|
+
export function specOf(svg, name) {
|
|
9
|
+
const at = svg.indexOf(SPEC_OPEN);
|
|
10
|
+
if (at === -1)
|
|
11
|
+
throw new Error(`${name}: no figure spec inside this SVG`);
|
|
12
|
+
return svg.slice(at + SPEC_OPEN.length, svg.indexOf(SPEC_CLOSE, at));
|
|
13
|
+
}
|
|
14
|
+
/** A spec from a `.json` or SVG path, or an object (`{ props }` or the props). Every error message starts with the name. */
|
|
15
|
+
export function loadSpec(input, name = typeof input === 'string' ? input : 'spec') {
|
|
16
|
+
let loaded = input;
|
|
17
|
+
if (typeof input === 'string') {
|
|
18
|
+
if (!input.endsWith('.json') && !input.endsWith('.svg'))
|
|
19
|
+
throw new Error(`${input}: expected a .json or .svg path`);
|
|
20
|
+
try {
|
|
21
|
+
const text = readFileSync(input, 'utf8');
|
|
22
|
+
loaded = JSON.parse(input.endsWith('.svg') ? specOf(text, input) : text);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
const m = e.message;
|
|
26
|
+
throw new Error(m.startsWith(`${input}: `) ? m : `${input}: ${m}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const props = (loaded?.props ?? loaded);
|
|
30
|
+
if (!props?.layout)
|
|
31
|
+
throw new Error(`${name}: no figure props (expected { props: { layout, edges, steps } })`);
|
|
32
|
+
if (!Array.isArray(props.edges))
|
|
33
|
+
throw new Error(`${name}: edges must be an array (use [] for none)`);
|
|
34
|
+
if (props.steps?.some((s) => !Array.isArray(s?.flow)))
|
|
35
|
+
throw new Error(`${name}: each step needs a flow array`);
|
|
36
|
+
return props;
|
|
37
|
+
}
|
|
38
|
+
/** The SVG with its spec in `<metadata>`: an SVG is then its own source. `]]>` would close the CDATA early, so it is escaped. */
|
|
39
|
+
export function svgWithSpec(props, opts = {}) {
|
|
40
|
+
const spec = JSON.stringify({ props }).replaceAll(']]>', ']]\\u003e');
|
|
41
|
+
return toSvg(props, opts).replace(/(<svg[^>]*>\n?)/, (tag) => `${tag}${SPEC_OPEN}${spec}${SPEC_CLOSE}\n`);
|
|
42
|
+
}
|
|
43
|
+
/** Errors first, then by rule name. `strict` makes every warning an error. */
|
|
44
|
+
export function sortFindings(findings, strict) {
|
|
45
|
+
return findings
|
|
46
|
+
.map((f) => (strict ? { ...f, severity: 'error' } : f))
|
|
47
|
+
.sort((a, b) => (a.severity === b.severity ? a.rule.localeCompare(b.rule) : a.severity === 'error' ? -1 : 1));
|
|
48
|
+
}
|
|
49
|
+
const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
|
|
50
|
+
/** One line per finding, the counts line (unless `withCounts` is false), and the `figure:` line. No color: the CLI adds it. */
|
|
51
|
+
export function reportLines(props, findings, withCounts = true) {
|
|
52
|
+
const out = findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.message}`);
|
|
53
|
+
const errors = findings.filter((f) => f.severity === 'error').length;
|
|
54
|
+
if (withCounts)
|
|
55
|
+
out.push(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
|
|
56
|
+
const c = counts(props);
|
|
57
|
+
out.push(`figure: ${n(c.boxes, 'box').replace('boxs', 'boxes')}, ${n(c.groups, 'group')}, ${n(c.edges, 'edge')}, ${n(c.steps, 'step')}, ${n(c.messages, 'message')}`);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type Response = {
|
|
2
|
+
jsonrpc: '2.0';
|
|
3
|
+
id: unknown;
|
|
4
|
+
result?: unknown;
|
|
5
|
+
error?: {
|
|
6
|
+
code: number;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const TOOLS: ({
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties: {
|
|
16
|
+
paths?: undefined;
|
|
17
|
+
root?: undefined;
|
|
18
|
+
strict?: undefined;
|
|
19
|
+
old?: undefined;
|
|
20
|
+
new?: undefined;
|
|
21
|
+
};
|
|
22
|
+
required?: undefined;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: string;
|
|
29
|
+
properties: {
|
|
30
|
+
spec: {
|
|
31
|
+
description: string;
|
|
32
|
+
anyOf: {
|
|
33
|
+
type: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
strict: {
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
width: {
|
|
41
|
+
type: string;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
minText: {
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
required: string[];
|
|
50
|
+
};
|
|
51
|
+
} | {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: string;
|
|
56
|
+
properties: {
|
|
57
|
+
spec: {
|
|
58
|
+
description: string;
|
|
59
|
+
anyOf: {
|
|
60
|
+
type: string;
|
|
61
|
+
}[];
|
|
62
|
+
};
|
|
63
|
+
strict: {
|
|
64
|
+
type: string;
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
width: {
|
|
68
|
+
type: string;
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
minText: {
|
|
72
|
+
type: string;
|
|
73
|
+
description: string;
|
|
74
|
+
};
|
|
75
|
+
out: {
|
|
76
|
+
type: string;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
paths?: undefined;
|
|
80
|
+
root?: undefined;
|
|
81
|
+
old?: undefined;
|
|
82
|
+
new?: undefined;
|
|
83
|
+
};
|
|
84
|
+
required: string[];
|
|
85
|
+
};
|
|
86
|
+
} | {
|
|
87
|
+
name: string;
|
|
88
|
+
description: string;
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: string;
|
|
91
|
+
properties: {
|
|
92
|
+
paths: {
|
|
93
|
+
type: string;
|
|
94
|
+
items: {
|
|
95
|
+
type: string;
|
|
96
|
+
};
|
|
97
|
+
description: string;
|
|
98
|
+
};
|
|
99
|
+
root: {
|
|
100
|
+
type: string;
|
|
101
|
+
description: string;
|
|
102
|
+
};
|
|
103
|
+
strict: {
|
|
104
|
+
type: string;
|
|
105
|
+
};
|
|
106
|
+
old?: undefined;
|
|
107
|
+
new?: undefined;
|
|
108
|
+
};
|
|
109
|
+
required: string[];
|
|
110
|
+
};
|
|
111
|
+
} | {
|
|
112
|
+
name: string;
|
|
113
|
+
description: string;
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: string;
|
|
116
|
+
properties: {
|
|
117
|
+
paths?: undefined;
|
|
118
|
+
root?: undefined;
|
|
119
|
+
strict?: undefined;
|
|
120
|
+
old: {
|
|
121
|
+
description: string;
|
|
122
|
+
anyOf: {
|
|
123
|
+
type: string;
|
|
124
|
+
}[];
|
|
125
|
+
};
|
|
126
|
+
new: {
|
|
127
|
+
description: string;
|
|
128
|
+
anyOf: {
|
|
129
|
+
type: string;
|
|
130
|
+
}[];
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
required: string[];
|
|
134
|
+
};
|
|
135
|
+
})[];
|
|
136
|
+
/** One JSON-RPC message in, one response out; `undefined` for a notification. Pure, so the tests call it directly. */
|
|
137
|
+
export declare function handle(message: unknown): Response | undefined;
|
|
138
|
+
/** Read one message per line from `input`, write one response per line to `output`. Returns when `input` ends. */
|
|
139
|
+
export declare function serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<void>;
|