@pyreon/mcp 0.12.13 → 0.12.14
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +258 -35
- package/lib/index.js.map +1 -1
- package/package.json +5 -2
- package/src/api-reference.ts +158 -29
- package/src/index.ts +169 -6
- package/src/tests/api-reference.test.ts +91 -1
package/lib/index.js
CHANGED
|
@@ -12750,7 +12750,7 @@ var StdioServerTransport = class {
|
|
|
12750
12750
|
|
|
12751
12751
|
//#endregion
|
|
12752
12752
|
//#region package.json
|
|
12753
|
-
var version = "0.12.
|
|
12753
|
+
var version = "0.12.14";
|
|
12754
12754
|
|
|
12755
12755
|
//#endregion
|
|
12756
12756
|
//#region src/api-reference.ts
|
|
@@ -13345,7 +13345,7 @@ await doc.toNotion() // Notion blocks`,
|
|
|
13345
13345
|
notes: "14+ output formats. JSX primitives: Document, Page, Heading, Text, Table, Image, List, Code, etc. Heavy renderers lazy-loaded."
|
|
13346
13346
|
},
|
|
13347
13347
|
"flow/createFlow": {
|
|
13348
|
-
signature: "
|
|
13348
|
+
signature: "<TData = Record<string, unknown>>(config: FlowConfig<TData>) => FlowInstance<TData>",
|
|
13349
13349
|
example: `// Generic over node data shape — typed consumers get strong narrowing
|
|
13350
13350
|
interface WorkflowData {
|
|
13351
13351
|
kind: 'trigger' | 'filter' | 'transform' | 'notify'
|
|
@@ -13364,13 +13364,50 @@ const flow = createFlow<WorkflowData>({
|
|
|
13364
13364
|
const trigger = flow.findNodes((n) => n.data.kind === 'trigger')
|
|
13365
13365
|
|
|
13366
13366
|
flow.addNode({ id: '3', type: 'custom', position: { x: 100, y: 200 }, data: { kind: 'transform', label: 'New' } })
|
|
13367
|
-
await flow.layout('layered', { direction: 'RIGHT', nodeSpacing: 50, layerSpacing: 100 })
|
|
13368
|
-
// LayoutOptions applicability: direction/layerSpacing/edgeRouting apply to layered/tree only;
|
|
13367
|
+
await flow.layout('layered', { direction: 'RIGHT', nodeSpacing: 50, layerSpacing: 100 })
|
|
13368
|
+
// LayoutOptions applicability: direction / layerSpacing / edgeRouting apply to layered/tree only;
|
|
13369
13369
|
// force/stress/radial/box/rectpacking silently ignore them. nodeSpacing applies to all algorithms.
|
|
13370
|
-
const json = flow.toJSON(); flow.fromJSON(json) // round-trip serialization
|
|
13370
|
+
const json = flow.toJSON(); flow.fromJSON(json) // round-trip serialization`,
|
|
13371
|
+
notes: "Create a reactive flow instance. Generic over node data shape — `createFlow<MyData>(...)` returns `FlowInstance<MyData>` so `node.data.kind` narrows correctly without an `[key: string]: unknown` index signature on consumer types. Defaults to `Record<string, unknown>` when no generic is supplied. The returned instance owns signal-native nodes / edges and exposes CRUD, selection, viewport (zoom / pan / fitView), and auto-layout via lazy-loaded elkjs (first `.layout()` call fetches a ~1.4MB chunk). Pan / zoom uses pointer events + CSS transforms — no D3. See also: useFlow, FlowInstance, Flow.",
|
|
13372
|
+
mistakes: `- Forgetting to declare \`@pyreon/runtime-dom\` in consumer app deps — flow's JSX emits \`_tpl()\` which needs runtime-dom imports
|
|
13373
|
+
- Reading \`NodeComponentProps.data\` / \`.selected\` / \`.dragging\` as plain values — all three are REACTIVE ACCESSORS: \`props.data()\`, \`props.selected()\`, \`props.dragging()\`
|
|
13374
|
+
- Calling \`props.data()\` OUTSIDE a reactive scope — captures the value once at component setup, defeating the per-node reactivity. Read it inside JSX expression thunks, \`effect\`, or \`computed\`
|
|
13375
|
+
- Adding \`[key: string]: unknown\` index signature to your node data interface — no longer needed now that \`createFlow\` is generic. Pass \`createFlow<MyData>(...)\` instead
|
|
13376
|
+
- Setting \`LayoutOptions.direction\` (or \`layerSpacing\`, or \`edgeRouting\`) on a force / stress / radial / box / rectpacking layout and expecting a directional result — these options are namespaced under ELK's layered / tree pipelines and silently ignored by the geometric algorithms. Dev-mode \`console.warn\` fires when this happens
|
|
13377
|
+
- Missing \`<Flow nodeTypes={{ key: Component }}>\` registration — \`node.type\` strings dispatch to that map, unregistered types fall through to the default renderer
|
|
13378
|
+
- Using \`createFlow\` inside a component body without \`onUnmount(() => flow.dispose())\` — prefer \`useFlow\` which auto-disposes
|
|
13379
|
+
- Using \`direction: 'row'\` on flow's containing Element layout — Pyreon \`Element\` accepts \`'inline'\` / \`'rows'\` / \`'reverseInline'\` / \`'reverseRows'\`, not CSS flex-direction values like \`'row'\` or \`'column'\``
|
|
13380
|
+
},
|
|
13381
|
+
"flow/useFlow": {
|
|
13382
|
+
signature: "<TData = Record<string, unknown>>(config: FlowConfig<TData>) => FlowInstance<TData>",
|
|
13383
|
+
example: `// Component-scoped flow — auto-disposes when the component unmounts.
|
|
13384
|
+
// Identical shape to createFlow, plus an implicit onUnmount(() => flow.dispose()).
|
|
13385
|
+
const MyDiagram = () => {
|
|
13386
|
+
const flow = useFlow<WorkflowData>({
|
|
13387
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: { kind: 'trigger', label: 'Start' } }],
|
|
13388
|
+
edges: [],
|
|
13389
|
+
})
|
|
13390
|
+
return (
|
|
13391
|
+
<Flow instance={flow}>
|
|
13392
|
+
<Background />
|
|
13393
|
+
</Flow>
|
|
13394
|
+
)
|
|
13395
|
+
}`,
|
|
13396
|
+
notes: `Component-scoped wrapper around \`createFlow\` — identical shape plus an implicit \`onUnmount(() => flow.dispose())\`. Prefer inside component bodies; use \`createFlow\` directly only for flows owned outside the component tree (app stores, singletons, SSR-shared state) where you'll dispose at the correct lifecycle point yourself. See also: createFlow.`,
|
|
13397
|
+
mistakes: `- Using \`useFlow\` outside a component body — the \`onUnmount\` hook registration requires an active component setup context, same constraint as every \`useX\` hook
|
|
13398
|
+
- Using \`createFlow\` inside a component and forgetting \`onUnmount(() => flow.dispose())\` — that was the footgun \`useFlow\` exists to prevent
|
|
13399
|
+
- Storing the returned instance in a module-level variable — bypasses the auto-dispose guarantee; use \`createFlow\` for that pattern`
|
|
13400
|
+
},
|
|
13401
|
+
"flow/Flow": {
|
|
13402
|
+
signature: "(props: FlowComponentProps) => VNodeChild",
|
|
13403
|
+
example: `<Flow instance={flow} nodeTypes={{ custom: MyNode }} edgeTypes={{ arrow: ArrowEdge }}>
|
|
13404
|
+
<Background variant="dots" gap={20} />
|
|
13405
|
+
<Controls position="bottom-left" />
|
|
13406
|
+
<MiniMap nodeColor={(node) => '#6366f1'} />
|
|
13407
|
+
</Flow>
|
|
13371
13408
|
|
|
13372
13409
|
// Custom node renderer — every prop except id is a REACTIVE ACCESSOR
|
|
13373
|
-
function
|
|
13410
|
+
function MyNode(props: NodeComponentProps<WorkflowData>) {
|
|
13374
13411
|
return (
|
|
13375
13412
|
<div
|
|
13376
13413
|
class={() => (props.selected() ? 'selected' : '')}
|
|
@@ -13379,21 +13416,61 @@ function CustomNode(props: NodeComponentProps<WorkflowData>) {
|
|
|
13379
13416
|
{() => props.data().label}
|
|
13380
13417
|
</div>
|
|
13381
13418
|
)
|
|
13419
|
+
}`,
|
|
13420
|
+
notes: "Main flow container. Accepts a `FlowInstance` via the `instance` prop plus optional `nodeTypes` / `edgeTypes` maps for custom renderers. Internally uses `<For>` keyed by `node.id` plus per-node reactive accessors that read live state from `instance.nodes()` — each node mounts EXACTLY ONCE across the lifetime of the graph regardless of drags, selection clicks, or `updateNode` mutations. A 60fps drag in a 1000-node graph stays O(1) per frame. JSX components are NOT generic at the call site (`<Flow<MyData> />` is invalid JSX); `FlowProps.instance` is typed as `FlowInstance<any>` so typed consumers can pass `FlowInstance<MyData>` without casting. See also: createFlow, Background, Controls, MiniMap, Handle.",
|
|
13421
|
+
mistakes: `- \`<Flow<MyData> />\` is invalid JSX — the component is not generic at the call site; pass a typed \`FlowInstance<MyData>\` via \`instance\` prop
|
|
13422
|
+
- Missing \`nodeTypes\` entry for a \`node.type\` string — falls through to the default renderer
|
|
13423
|
+
- Mutating \`instance.nodes()\` return value directly — use \`instance.addNode\` / \`updateNode\` / \`removeNode\` so the internal signals fire`
|
|
13424
|
+
},
|
|
13425
|
+
"flow/Background": {
|
|
13426
|
+
signature: "(props: { variant?: \"dots\" | \"lines\"; gap?: number; color?: string }) => VNodeChild",
|
|
13427
|
+
example: `<Flow instance={flow}>
|
|
13428
|
+
<Background variant="dots" gap={24} color="#e5e7eb" />
|
|
13429
|
+
</Flow>`,
|
|
13430
|
+
notes: "Dot or line grid background inside a `<Flow>`. Place as a direct child. `variant` defaults to `\"dots\"`, `gap` controls pattern spacing, `color` sets the pattern color. Renders as an SVG pattern at the back of the z-order. See also: Flow, Controls, MiniMap."
|
|
13431
|
+
},
|
|
13432
|
+
"flow/Controls": {
|
|
13433
|
+
signature: "(props?: { position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\" }) => VNodeChild",
|
|
13434
|
+
example: `<Flow instance={flow}>
|
|
13435
|
+
<Controls position="bottom-left" />
|
|
13436
|
+
</Flow>`,
|
|
13437
|
+
notes: "Zoom in / zoom out / fit-view button cluster. Renders absolutely inside the flow viewport at the configured corner (default `\"bottom-right\"`). Each button dispatches to the corresponding `FlowInstance` viewport method. See also: Flow, Background, MiniMap."
|
|
13438
|
+
},
|
|
13439
|
+
"flow/MiniMap": {
|
|
13440
|
+
signature: "(props?: { nodeColor?: (node: FlowNode) => string; maskColor?: string }) => VNodeChild",
|
|
13441
|
+
example: `<Flow instance={flow}>
|
|
13442
|
+
<MiniMap nodeColor={(node) => node.data.highlighted ? '#f59e0b' : '#6366f1'} />
|
|
13443
|
+
</Flow>`,
|
|
13444
|
+
notes: "Overview minimap of the full graph. `nodeColor` is a per-node color function (default grey), `maskColor` fills the area outside the current viewport (default semi-transparent black). Clicks on the minimap recenter the main viewport. See also: Flow, Background, Controls."
|
|
13445
|
+
},
|
|
13446
|
+
"flow/Handle": {
|
|
13447
|
+
signature: "(props: { type: \"source\" | \"target\"; position: Position; id?: string }) => VNodeChild",
|
|
13448
|
+
example: `function CustomNode(props: NodeComponentProps<MyData>) {
|
|
13449
|
+
return (
|
|
13450
|
+
<div>
|
|
13451
|
+
<Handle type="target" position={Position.Left} />
|
|
13452
|
+
{() => props.data().label}
|
|
13453
|
+
<Handle type="source" position={Position.Right} id="out-primary" />
|
|
13454
|
+
<Handle type="source" position={Position.Bottom} id="out-fallback" />
|
|
13455
|
+
</div>
|
|
13456
|
+
)
|
|
13382
13457
|
}
|
|
13383
13458
|
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13459
|
+
// Edge referencing a specific source handle by id
|
|
13460
|
+
flow.addEdge({ source: '1', sourceHandle: 'out-primary', target: '2' })`,
|
|
13461
|
+
notes: "Connection handle on a custom node — exposes a connectable point that edges attach to. `type` picks direction (`\"source\"` emits edges, `\"target\"` receives), `position` is a `Position` enum (`Top` / `Right` / `Bottom` / `Left`). Provide a distinct `id` when a node has multiple source or target handles so edges can reference the specific one via `edge.sourceHandle` / `edge.targetHandle`. See also: Flow, Position.",
|
|
13462
|
+
mistakes: `- Multiple \`source\` / \`target\` handles on one node without distinct \`id\` values — edges cannot disambiguate which handle they connect to
|
|
13463
|
+
- Nesting a \`<Handle>\` inside a non-node component (a \`<Background>\` child, a \`<Panel>\`, etc.) — the connection machinery expects handles to live inside a node renderer`
|
|
13464
|
+
},
|
|
13465
|
+
"flow/Panel": {
|
|
13466
|
+
signature: "(props: { position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"; children: VNodeChild }) => VNodeChild",
|
|
13467
|
+
example: `<Flow instance={flow}>
|
|
13468
|
+
<Panel position="top-right">
|
|
13469
|
+
<button onClick={() => flow.fitView()}>Fit</button>
|
|
13470
|
+
<button onClick={() => flow.toJSON()}>Export</button>
|
|
13471
|
+
</Panel>
|
|
13388
13472
|
</Flow>`,
|
|
13389
|
-
notes: "
|
|
13390
|
-
mistakes: `- Forgetting to declare @pyreon/runtime-dom in consumer app deps — flow's JSX emits _tpl() which needs runtime-dom imports
|
|
13391
|
-
- Reading props.data, props.selected, or props.dragging as plain values — they're ALL accessors, call them: props.data().kind, props.selected(), props.dragging()
|
|
13392
|
-
- Calling props.data() OUTSIDE a reactive scope — captures the value once at component setup, defeating reactivity. Read it inside JSX expression thunks, effect, or computed: {() => props.data().label}
|
|
13393
|
-
- Adding [key: string]: unknown index signature to your node data interface — no longer needed now that createFlow is generic. Just pass createFlow<MyData>(...)
|
|
13394
|
-
- Using direction: 'row' on flow's containing layout — Pyreon Element accepts 'inline'|'rows'|'reverseInline'|'reverseRows', not 'row'
|
|
13395
|
-
- Setting LayoutOptions.direction (or layerSpacing, or edgeRouting) on a force/stress/radial/box/rectpacking layout and expecting a directional result — these options are namespaced under ELK's layered/tree pipelines and silently ignored by the geometric algorithms. Switch the algorithm to 'layered' or 'tree' if you need a directional layout.
|
|
13396
|
-
- Missing the <Flow nodeTypes={{ key: Component }}> registration — node.type strings dispatch to that map`
|
|
13473
|
+
notes: "Overlay panel positioned absolutely relative to the flow viewport. Use for toolbars, legend badges, or contextual action buttons. Pass any JSX as children — the panel is a plain positioned container, not a predefined chrome component. See also: Flow, Controls."
|
|
13397
13474
|
},
|
|
13398
13475
|
"code/createEditor": {
|
|
13399
13476
|
signature: "createEditor(config: { value?: string, language?: EditorLanguage, theme?: EditorTheme, onChange?: (val: string) => void, minimap?: boolean, lineNumbers?: boolean, ... }): EditorInstance",
|
|
@@ -13509,28 +13586,38 @@ Posts.useTable() // TanStack Table config`,
|
|
|
13509
13586
|
|
|
13510
13587
|
const result = lint({ paths: ["src/"], preset: "recommended" })
|
|
13511
13588
|
console.log(result.totalErrors, result.totalWarnings)
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13589
|
+
// Config-level diagnostics (malformed rule options, etc.)
|
|
13590
|
+
for (const d of result.configDiagnostics) console.log(d.ruleId, d.message)
|
|
13591
|
+
|
|
13592
|
+
// Severity overrides + per-rule options overrides
|
|
13593
|
+
lint({
|
|
13594
|
+
paths: ["."],
|
|
13595
|
+
ruleOverrides: { "pyreon/no-classname": "off" },
|
|
13596
|
+
ruleOptionsOverrides: {
|
|
13597
|
+
"pyreon/no-window-in-ssr": { exemptPaths: ["src/foundation/"] },
|
|
13598
|
+
},
|
|
13599
|
+
})`,
|
|
13600
|
+
notes: "Programmatic API. 59 rules across 12 categories. Auto-loads .pyreonlintrc.json. Presets: recommended, strict, app, lib. Per-rule options via tuple form in config (`[\"error\", { exemptPaths: [...] }]`) or `ruleOptionsOverrides`. Wrong-typed options surface on `result.configDiagnostics`. Uses oxc-parser with AST caching."
|
|
13516
13601
|
},
|
|
13517
13602
|
"lint/lintFile": {
|
|
13518
|
-
signature: "lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache): LintFileResult",
|
|
13603
|
+
signature: "lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache, configDiagnosticsSink?: ConfigDiagnostic[]): LintFileResult",
|
|
13519
13604
|
example: `import { lintFile, allRules, getPreset, AstCache } from "@pyreon/lint"
|
|
13520
13605
|
|
|
13521
13606
|
const cache = new AstCache()
|
|
13522
13607
|
const config = getPreset("recommended")
|
|
13523
|
-
const
|
|
13524
|
-
|
|
13608
|
+
const configSink: ConfigDiagnostic[] = []
|
|
13609
|
+
const result = lintFile("app.tsx", source, allRules, config, cache, configSink)`,
|
|
13610
|
+
notes: "Low-level single-file API. Optional AstCache for repeat runs (FNV-1a hash keyed). Optional `configDiagnosticsSink` collects malformed-option diagnostics; without it they print to stderr."
|
|
13525
13611
|
},
|
|
13526
13612
|
"lint/cli": {
|
|
13527
|
-
signature: "pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [path...]",
|
|
13613
|
+
signature: "pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [--rule-options id='{json}'] [path...]",
|
|
13528
13614
|
example: `pyreon-lint --preset strict --quiet # CI mode
|
|
13529
13615
|
pyreon-lint --fix # auto-fix
|
|
13530
13616
|
pyreon-lint --watch src/ # watch mode
|
|
13531
|
-
pyreon-lint --list # list all
|
|
13532
|
-
pyreon-lint --format json # machine-readable
|
|
13533
|
-
|
|
13617
|
+
pyreon-lint --list # list all 59 rules
|
|
13618
|
+
pyreon-lint --format json # machine-readable
|
|
13619
|
+
pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/`,
|
|
13620
|
+
notes: "CLI entry. Config: .pyreonlintrc.json (reference schema/pyreonlintrc.schema.json for IDE autocomplete), package.json 'pyreonlint' field. Ignore: .pyreonlintignore + .gitignore. Watch: fs.watch recursive with 100ms debounce. `--rule-options id='{json}'` passes per-rule options on a single run."
|
|
13534
13621
|
},
|
|
13535
13622
|
"lint/no-process-dev-gate": {
|
|
13536
13623
|
signature: "rule: pyreon/no-process-dev-gate (architecture, error, auto-fixable)",
|
|
@@ -13548,6 +13635,35 @@ if (__DEV__) console.warn('hello')`,
|
|
|
13548
13635
|
- Adding \`process: { env: { ... } }\` polyfills to vite.config.ts as a workaround — fix the source instead
|
|
13549
13636
|
- Using the rule for server-only packages — they're correctly exempt because Node always has \`process\``
|
|
13550
13637
|
},
|
|
13638
|
+
"lint/require-browser-smoke-test": {
|
|
13639
|
+
signature: "rule: pyreon/require-browser-smoke-test (architecture, error in recommended/strict/lib, off in app)",
|
|
13640
|
+
example: `// Per-package config (optional — defaults cover all known browser packages)
|
|
13641
|
+
{
|
|
13642
|
+
"rules": {
|
|
13643
|
+
"pyreon/require-browser-smoke-test": [
|
|
13644
|
+
"error",
|
|
13645
|
+
{
|
|
13646
|
+
"additionalPackages": ["@my-org/my-browser-pkg"],
|
|
13647
|
+
"exemptPaths": ["packages/experimental/"]
|
|
13648
|
+
}
|
|
13649
|
+
]
|
|
13650
|
+
}
|
|
13651
|
+
}`,
|
|
13652
|
+
notes: "Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). Every browser-categorized package MUST ship at least one `*.browser.test.{ts,tsx}` file under `src/`. Without this rule, new browser packages can quietly ship without smoke coverage and we drift back to the world before T1.1 — happy-dom silently masks environment-divergence bugs (PR #197 mock-vnode metadata drop, PR #200 `typeof process` dead code, multi-word event delegation bug). Default browser-package list mirrors `.claude/rules/test-environment-parity.md`. The rule fires once per package on its `src/index.ts`, walks the package directory looking for `*.browser.test.*`, and reports if none are found. Off in `app` preset because apps don't ship as packages with smoke obligations.",
|
|
13653
|
+
mistakes: `- Adding a new browser-running package without a browser test — the rule will fail your PR
|
|
13654
|
+
- Hardcoding the browser-package list in the rule — the list lives in \`.claude/rules/browser-packages.json\` (single source of truth), not in the rule source
|
|
13655
|
+
- Disabling the rule globally — use \`exemptPaths\` to exempt specific packages still under construction
|
|
13656
|
+
- Shipping a \`sanity.browser.test.ts\` with \`expect(1).toBe(1)\` just to satisfy the rule — it passes but provides zero signal. The rule is a GATE, not a quality check; review actual contents on PR`
|
|
13657
|
+
},
|
|
13658
|
+
"mcp/get_browser_smoke_status": {
|
|
13659
|
+
signature: "tool: get_browser_smoke_status — no args",
|
|
13660
|
+
example: `// Ask the MCP server:
|
|
13661
|
+
// "which Pyreon packages are missing browser smoke coverage?"
|
|
13662
|
+
// Tool walks packages/, matches against .claude/rules/browser-packages.json,
|
|
13663
|
+
// returns a coverage report.`,
|
|
13664
|
+
notes: "Companion to the `pyreon/require-browser-smoke-test` lint rule. Reports which browser-categorized Pyreon packages have at least one `*.browser.test.{ts,tsx}` file under `src/`. Uses the same `.claude/rules/browser-packages.json` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout).",
|
|
13665
|
+
mistakes: `- Using the tool's output as a substitute for running the CI script — this tool only checks file existence, not the self-expiring-exemption check that \`bun run lint:browser-smoke\` performs`
|
|
13666
|
+
},
|
|
13551
13667
|
"ui-core/PyreonUI": {
|
|
13552
13668
|
signature: "PyreonUI(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }): VNodeChild",
|
|
13553
13669
|
example: `import { PyreonUI } from "@pyreon/ui-core"
|
|
@@ -13798,12 +13914,13 @@ const tree = helper.getDocNode()`,
|
|
|
13798
13914
|
* to generate, validate, and migrate Pyreon code.
|
|
13799
13915
|
*
|
|
13800
13916
|
* Tools:
|
|
13801
|
-
* get_api
|
|
13802
|
-
* validate
|
|
13803
|
-
* migrate_react
|
|
13804
|
-
* diagnose
|
|
13805
|
-
* get_routes
|
|
13806
|
-
* get_components
|
|
13917
|
+
* get_api — Look up any Pyreon API: signature, usage, common mistakes
|
|
13918
|
+
* validate — Check a code snippet for Pyreon anti-patterns
|
|
13919
|
+
* migrate_react — Convert React code to idiomatic Pyreon
|
|
13920
|
+
* diagnose — Parse an error message into structured fix information
|
|
13921
|
+
* get_routes — List all routes in the current project
|
|
13922
|
+
* get_components — List all components with their props and signals
|
|
13923
|
+
* get_browser_smoke_status — Report which browser-categorized packages have smoke coverage
|
|
13807
13924
|
*
|
|
13808
13925
|
* Usage:
|
|
13809
13926
|
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
@@ -13888,6 +14005,112 @@ server.tool("get_components", {}, async () => {
|
|
|
13888
14005
|
}).join("\n");
|
|
13889
14006
|
return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`);
|
|
13890
14007
|
});
|
|
14008
|
+
server.tool("get_browser_smoke_status", {}, async () => {
|
|
14009
|
+
const fs = await import("node:fs");
|
|
14010
|
+
const path = await import("node:path");
|
|
14011
|
+
const cwd = process.cwd();
|
|
14012
|
+
let browserPackages = [];
|
|
14013
|
+
{
|
|
14014
|
+
let dir = cwd;
|
|
14015
|
+
for (let i = 0; i < 30; i++) {
|
|
14016
|
+
const candidate = path.join(dir, ".claude", "rules", "browser-packages.json");
|
|
14017
|
+
if (fs.existsSync(candidate)) {
|
|
14018
|
+
try {
|
|
14019
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
14020
|
+
if (Array.isArray(parsed.packages)) browserPackages = parsed.packages.filter((p) => typeof p === "string");
|
|
14021
|
+
} catch {}
|
|
14022
|
+
break;
|
|
14023
|
+
}
|
|
14024
|
+
const parent = path.dirname(dir);
|
|
14025
|
+
if (parent === dir) break;
|
|
14026
|
+
dir = parent;
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
14029
|
+
if (browserPackages.length === 0) return textResult("No `.claude/rules/browser-packages.json` found in the current project. This tool reports browser-smoke coverage for Pyreon monorepos that ship the single-source-of-truth list. Consumer apps can still opt in via the lint rule's `additionalPackages` option.");
|
|
14030
|
+
function hasBrowserTest(dir) {
|
|
14031
|
+
let entries;
|
|
14032
|
+
try {
|
|
14033
|
+
entries = fs.readdirSync(dir);
|
|
14034
|
+
} catch {
|
|
14035
|
+
return false;
|
|
14036
|
+
}
|
|
14037
|
+
for (const name of entries) {
|
|
14038
|
+
if (name.startsWith(".") || name === "node_modules" || name === "lib" || name === "dist") continue;
|
|
14039
|
+
const full = path.join(dir, name);
|
|
14040
|
+
let isDir = false;
|
|
14041
|
+
try {
|
|
14042
|
+
isDir = fs.statSync(full).isDirectory();
|
|
14043
|
+
} catch {
|
|
14044
|
+
continue;
|
|
14045
|
+
}
|
|
14046
|
+
if (isDir) {
|
|
14047
|
+
if (hasBrowserTest(full)) return true;
|
|
14048
|
+
continue;
|
|
14049
|
+
}
|
|
14050
|
+
if (/\.browser\.test\.(?:ts|tsx)$/.test(name)) return true;
|
|
14051
|
+
}
|
|
14052
|
+
return false;
|
|
14053
|
+
}
|
|
14054
|
+
const pkgDirs = /* @__PURE__ */ new Map();
|
|
14055
|
+
function walkPkgs(dir, depth = 0) {
|
|
14056
|
+
if (depth > 4) return;
|
|
14057
|
+
let entries;
|
|
14058
|
+
try {
|
|
14059
|
+
entries = fs.readdirSync(dir);
|
|
14060
|
+
} catch {
|
|
14061
|
+
return;
|
|
14062
|
+
}
|
|
14063
|
+
for (const name of entries) {
|
|
14064
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
14065
|
+
const full = path.join(dir, name);
|
|
14066
|
+
let isDir = false;
|
|
14067
|
+
try {
|
|
14068
|
+
isDir = fs.statSync(full).isDirectory();
|
|
14069
|
+
} catch {
|
|
14070
|
+
continue;
|
|
14071
|
+
}
|
|
14072
|
+
if (!isDir) continue;
|
|
14073
|
+
const pkgJsonPath = path.join(full, "package.json");
|
|
14074
|
+
if (fs.existsSync(pkgJsonPath)) try {
|
|
14075
|
+
const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
14076
|
+
if (typeof parsed.name === "string") pkgDirs.set(parsed.name, full);
|
|
14077
|
+
} catch {}
|
|
14078
|
+
else walkPkgs(full, depth + 1);
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
walkPkgs(path.join(cwd, "packages"));
|
|
14082
|
+
const covered = [];
|
|
14083
|
+
const missing = [];
|
|
14084
|
+
const unknown = [];
|
|
14085
|
+
for (const name of browserPackages) {
|
|
14086
|
+
const dir = pkgDirs.get(name);
|
|
14087
|
+
if (!dir) {
|
|
14088
|
+
unknown.push(name);
|
|
14089
|
+
continue;
|
|
14090
|
+
}
|
|
14091
|
+
if (hasBrowserTest(dir)) covered.push(name);
|
|
14092
|
+
else missing.push(name);
|
|
14093
|
+
}
|
|
14094
|
+
const parts = [];
|
|
14095
|
+
parts.push(`**Browser smoke coverage** (${covered.length} / ${browserPackages.length}):`);
|
|
14096
|
+
parts.push("");
|
|
14097
|
+
if (covered.length > 0) {
|
|
14098
|
+
parts.push(`✓ Covered (${covered.length}):`);
|
|
14099
|
+
for (const n of covered) parts.push(` - ${n}`);
|
|
14100
|
+
parts.push("");
|
|
14101
|
+
}
|
|
14102
|
+
if (missing.length > 0) {
|
|
14103
|
+
parts.push(`✗ Missing \`*.browser.test.*\` (${missing.length}):`);
|
|
14104
|
+
for (const n of missing) parts.push(` - ${n}`);
|
|
14105
|
+
parts.push("");
|
|
14106
|
+
parts.push("Add a `*.browser.test.{ts,tsx}` file under `src/` in each missing package. See `.claude/rules/test-environment-parity.md` for the setup recipe.");
|
|
14107
|
+
}
|
|
14108
|
+
if (unknown.length > 0) {
|
|
14109
|
+
parts.push(`? Listed in browser-packages.json but not found in this repo (${unknown.length}):`);
|
|
14110
|
+
for (const n of unknown) parts.push(` - ${n}`);
|
|
14111
|
+
}
|
|
14112
|
+
return textResult(parts.join("\n"));
|
|
14113
|
+
});
|
|
13891
14114
|
async function main() {
|
|
13892
14115
|
const transport = new StdioServerTransport();
|
|
13893
14116
|
await server.connect(transport);
|