@lariocpt/flawless-react 0.0.0-stage → 0.3.1
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/.agents/skills/flawless-a11y/SKILL.md +113 -0
- package/.agents/skills/flawless-api/SKILL.md +159 -0
- package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
- package/.agents/skills/flawless-e2e/SKILL.md +155 -0
- package/.agents/skills/flawless-expo/SKILL.md +283 -0
- package/.agents/skills/flawless-findings/SKILL.md +116 -0
- package/.agents/skills/flawless-load/SKILL.md +112 -0
- package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
- package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
- package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
- package/.agents/skills/flawless-react/SKILL.md +403 -0
- package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
- package/.agents/skills/flawless-regen/SKILL.md +135 -0
- package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
- package/LICENSE +21 -0
- package/README.md +611 -2
- package/bin/flawless-react +4 -0
- package/package.json +67 -4
- package/scripts/postinstall.d.mts +46 -0
- package/scripts/postinstall.mjs +127 -0
- package/src/analyse/authShims.ts +220 -0
- package/src/analyse/bootability.ts +257 -0
- package/src/analyse/childBridge.ts +302 -0
- package/src/analyse/childrenIntoSlot.ts +66 -0
- package/src/analyse/compoundFamily.ts +100 -0
- package/src/analyse/dataExports.ts +200 -0
- package/src/analyse/extractChildAttrs.ts +112 -0
- package/src/analyse/extractDataDeps.ts +857 -0
- package/src/analyse/extractDisclosures.ts +186 -0
- package/src/analyse/extractFetchCalls.ts +81 -0
- package/src/analyse/extractFlows.ts +239 -0
- package/src/analyse/extractGuards.ts +270 -0
- package/src/analyse/extractHooks.ts +237 -0
- package/src/analyse/extractInteractions.ts +1421 -0
- package/src/analyse/extractLogging.ts +45 -0
- package/src/analyse/extractNativeA11y.ts +138 -0
- package/src/analyse/extractPredicates.ts +784 -0
- package/src/analyse/extractPropUsage.ts +162 -0
- package/src/analyse/extractProps.ts +1212 -0
- package/src/analyse/extractRenderMode.ts +165 -0
- package/src/analyse/extractRootTag.ts +117 -0
- package/src/analyse/extractState.ts +361 -0
- package/src/analyse/extractStaticText.ts +212 -0
- package/src/analyse/extractVariants.ts +186 -0
- package/src/analyse/harvestApiEndpoints.ts +284 -0
- package/src/analyse/hostProps.ts +185 -0
- package/src/analyse/hostTag.ts +72 -0
- package/src/analyse/htmlContentModel.ts +94 -0
- package/src/analyse/index.ts +744 -0
- package/src/analyse/mountPathCalls.ts +442 -0
- package/src/analyse/nativeContentModel.ts +129 -0
- package/src/analyse/nativeDialect.ts +83 -0
- package/src/analyse/nativeProps.ts +339 -0
- package/src/analyse/ownReturns.ts +31 -0
- package/src/analyse/parseFile.ts +919 -0
- package/src/analyse/pos.ts +8 -0
- package/src/analyse/providerWraps.ts +1977 -0
- package/src/analyse/pureModule.ts +88 -0
- package/src/analyse/reexportTable.ts +148 -0
- package/src/analyse/refs.ts +195 -0
- package/src/analyse/renderPath.ts +441 -0
- package/src/analyse/resolveDepReturns.ts +370 -0
- package/src/analyse/resolveIndexKeys.ts +98 -0
- package/src/analyse/resolveModule.ts +431 -0
- package/src/analyse/resolveTagRootTags.ts +103 -0
- package/src/analyse/serverActionReturns.ts +213 -0
- package/src/analyse/sourceCache.ts +263 -0
- package/src/analyse/spreadSiblings.ts +98 -0
- package/src/analyse/walkChildren.ts +98 -0
- package/src/analyse/zodSchema.ts +424 -0
- package/src/bin.ts +23 -0
- package/src/changed.ts +221 -0
- package/src/cli.ts +560 -0
- package/src/cliFlags.ts +281 -0
- package/src/core.ts +1229 -0
- package/src/docLine.ts +122 -0
- package/src/emitPaths.ts +51 -0
- package/src/expo/detect.ts +184 -0
- package/src/expo/refusal.ts +62 -0
- package/src/expo/routes.ts +145 -0
- package/src/index.ts +95 -0
- package/src/mcp/analysisTools.ts +720 -0
- package/src/mcp/infoTools.ts +135 -0
- package/src/mcp/protocol.ts +130 -0
- package/src/mcp/run.ts +269 -0
- package/src/mcp/runTools.ts +362 -0
- package/src/mcp/server.ts +279 -0
- package/src/mcp/spec.ts +86 -0
- package/src/mcp/support.ts +268 -0
- package/src/mcp/tools.ts +75 -0
- package/src/nextjs/apiRoutes.ts +116 -0
- package/src/nextjs/detect.ts +397 -0
- package/src/nextjs/emit.ts +106 -0
- package/src/nextjs/liveSpec.ts +79 -0
- package/src/nextjs/mswHandlers.ts +55 -0
- package/src/nextjs/providerWrapper.ts +603 -0
- package/src/nextjs/routes.ts +86 -0
- package/src/nextjs/swagger.ts +32 -0
- package/src/packageJson.ts +414 -0
- package/src/platform.ts +18 -0
- package/src/prompt.ts +132 -0
- package/src/reactCompiler.ts +165 -0
- package/src/reporters/findings.ts +156 -0
- package/src/reporters/report.ts +463 -0
- package/src/runner/runTests.ts +402 -0
- package/src/scaffold/adoptStyle.ts +181 -0
- package/src/scaffold/index.ts +253 -0
- package/src/scaffold/openapi.ts +183 -0
- package/src/scaffold/templates.ts +2379 -0
- package/src/scaffold-tests/index.ts +463 -0
- package/src/scaffold-tests/mockValues.ts +208 -0
- package/src/scaffold-tests/mocks.ts +794 -0
- package/src/scaffold-tests/nativeIndex.ts +52 -0
- package/src/scaffold-tests/nativeTemplates.ts +1528 -0
- package/src/scaffold-tests/postDispatch.ts +152 -0
- package/src/scaffold-tests/registry.ts +443 -0
- package/src/scaffold-tests/selectors.ts +493 -0
- package/src/scaffold-tests/selectorsNative.ts +273 -0
- package/src/scaffold-tests/stamp.ts +79 -0
- package/src/scaffold-tests/templates.ts +3578 -0
- package/src/skills/cli.ts +248 -0
- package/src/skills/compat.ts +166 -0
- package/src/skills/data.ts +192 -0
- package/src/skills/md.d.ts +7 -0
- package/src/skills/menu.ts +55 -0
- package/src/skills/spec.ts +227 -0
- package/src/suiteScripts.ts +110 -0
- package/src/tasks/a11yNative.ts +281 -0
- package/src/tasks/a11yScan.ts +67 -0
- package/src/tasks/biomeRunner.ts +301 -0
- package/src/tasks/biomeSweep.ts +166 -0
- package/src/tasks/e2eExpo.ts +436 -0
- package/src/tasks/e2eRoutes.ts +756 -0
- package/src/tasks/eslintReactRules.ts +67 -0
- package/src/tasks/index.ts +24 -0
- package/src/tasks/loadExpo.ts +273 -0
- package/src/tasks/loadPipes.ts +750 -0
- package/src/tasks/perfHarness.ts +145 -0
- package/src/tasks/staticPerfAnalysis.ts +510 -0
- package/src/tasks/types.ts +1 -0
- package/src/tasks/unitAndBrowser.ts +462 -0
- package/src/types.ts +1419 -0
- package/src/version.ts +12 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// The RUN plane of the MCP surface: the tools that change a project, and so run
|
|
2
|
+
// as a flawless-react child process (see run.ts for why that boundary exists).
|
|
3
|
+
//
|
|
4
|
+
// Both tools here map argument → argv rather than reaching into core.ts, and
|
|
5
|
+
// that is deliberate: the CLI is the contract agents are told about, the exit
|
|
6
|
+
// codes and the `--ci` blob are already its documented shape, and a second
|
|
7
|
+
// in-process path would be a second implementation to keep in agreement — the
|
|
8
|
+
// defect class this repo keeps finding in its own docs.
|
|
9
|
+
|
|
10
|
+
import { parseSkillsCommand } from "../skills/spec.ts";
|
|
11
|
+
import { ALL_TASK_IDS } from "../tasks/index.ts";
|
|
12
|
+
import type { HeapMode, RootMode } from "../types.ts";
|
|
13
|
+
import type { JsonObject } from "./protocol.ts";
|
|
14
|
+
import { callOpts, DEFAULT_TIMEOUT_MS, renderCiRun, renderRun, runFlawlessChild } from "./run.ts";
|
|
15
|
+
import {
|
|
16
|
+
optBool,
|
|
17
|
+
optEnum,
|
|
18
|
+
optEnumArray,
|
|
19
|
+
optJsonText,
|
|
20
|
+
optStr,
|
|
21
|
+
optStrArray,
|
|
22
|
+
optStrOrTrue,
|
|
23
|
+
requireKeys,
|
|
24
|
+
type ToolDef,
|
|
25
|
+
ToolError,
|
|
26
|
+
} from "./support.ts";
|
|
27
|
+
|
|
28
|
+
/** `--heaps` closed set, from HeapMode — the flag's own doc line is stale about
|
|
29
|
+
* this and parseArgs is the authority. */
|
|
30
|
+
const HEAP_MODES: readonly HeapMode[] = ["off", "stress", "all"];
|
|
31
|
+
const ROOT_MODES: readonly RootMode[] = ["primary", "exported"];
|
|
32
|
+
|
|
33
|
+
// ------------------------------------------------------------ flawless_run ----
|
|
34
|
+
|
|
35
|
+
const RUN_ARGS = [
|
|
36
|
+
"entry",
|
|
37
|
+
"tasks",
|
|
38
|
+
"nextjs",
|
|
39
|
+
"expo",
|
|
40
|
+
"out",
|
|
41
|
+
"colocate",
|
|
42
|
+
"force",
|
|
43
|
+
"report",
|
|
44
|
+
"run_tests",
|
|
45
|
+
"changed",
|
|
46
|
+
"roots",
|
|
47
|
+
"exclude",
|
|
48
|
+
"no_default_excludes",
|
|
49
|
+
"depth",
|
|
50
|
+
"variants",
|
|
51
|
+
"include_children",
|
|
52
|
+
"heaps",
|
|
53
|
+
"props",
|
|
54
|
+
"provider",
|
|
55
|
+
"app_resolver",
|
|
56
|
+
"api",
|
|
57
|
+
"swagger_url",
|
|
58
|
+
"inline_summary",
|
|
59
|
+
"cwd",
|
|
60
|
+
"env",
|
|
61
|
+
"timeout_ms",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tool arguments → CLI argv, in the order the CLI reads them.
|
|
66
|
+
*
|
|
67
|
+
* `--ci` is always first: it is the non-interactive contract (no prompts, JSON on
|
|
68
|
+
* stdout) and without it a run would wait forever on a stdin the protocol owns.
|
|
69
|
+
* Boolean options are only emitted when they change the default, so the argv a
|
|
70
|
+
* user reproduces by hand matches what the tool ran.
|
|
71
|
+
*/
|
|
72
|
+
function runArgv(a: JsonObject): string[] {
|
|
73
|
+
const entry = optStr(a, "entry");
|
|
74
|
+
const tasks = optEnumArray(a, "tasks", ALL_TASK_IDS);
|
|
75
|
+
const nextjs = optStrOrTrue(a, "nextjs");
|
|
76
|
+
const expo = optStrOrTrue(a, "expo");
|
|
77
|
+
if (nextjs !== undefined && expo !== undefined) {
|
|
78
|
+
// The CLI refuses this too; refusing here gives the agent the explanation
|
|
79
|
+
// before a child pays for the module graph to say it.
|
|
80
|
+
throw new ToolError(
|
|
81
|
+
"nextjs and expo are mutually exclusive — one project, one framework mode per run",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const argv = ["--ci"];
|
|
85
|
+
if (entry) argv.push(entry);
|
|
86
|
+
if (tasks?.length) argv.push("--tasks", tasks.join(","));
|
|
87
|
+
if (nextjs === true) argv.push("--nextjs");
|
|
88
|
+
else if (typeof nextjs === "string") argv.push("--nextjs", nextjs);
|
|
89
|
+
if (expo === true) argv.push("--expo");
|
|
90
|
+
else if (typeof expo === "string") argv.push("--expo", expo);
|
|
91
|
+
const out = optStr(a, "out");
|
|
92
|
+
if (out) argv.push("--out", out);
|
|
93
|
+
if (optBool(a, "colocate")) argv.push("--colocate");
|
|
94
|
+
if (optBool(a, "force")) argv.push("--force");
|
|
95
|
+
if (optBool(a, "report")) argv.push("--report");
|
|
96
|
+
if (optBool(a, "run_tests")) argv.push("--run-tests");
|
|
97
|
+
const changed = optStrOrTrue(a, "changed");
|
|
98
|
+
if (changed === true) argv.push("--changed");
|
|
99
|
+
else if (typeof changed === "string") argv.push("--changed", changed);
|
|
100
|
+
const roots = optEnum(a, "roots", ROOT_MODES);
|
|
101
|
+
if (roots) argv.push("--roots", roots);
|
|
102
|
+
for (const g of optStrArray(a, "exclude") ?? []) argv.push("--exclude", g);
|
|
103
|
+
if (optBool(a, "no_default_excludes")) argv.push("--no-default-excludes");
|
|
104
|
+
const depth = a.depth;
|
|
105
|
+
if (typeof depth === "number") argv.push("--depth", String(depth));
|
|
106
|
+
const variants = a.variants;
|
|
107
|
+
if (typeof variants === "number") argv.push("--variants", String(variants));
|
|
108
|
+
const includeChildren = optBool(a, "include_children");
|
|
109
|
+
if (includeChildren === false) argv.push("--include-children=false");
|
|
110
|
+
const heaps = optEnum(a, "heaps", HEAP_MODES);
|
|
111
|
+
if (heaps) argv.push("--heaps", heaps);
|
|
112
|
+
const props = optJsonText(a, "props");
|
|
113
|
+
if (props) argv.push("--props", props);
|
|
114
|
+
const provider = optStr(a, "provider");
|
|
115
|
+
if (provider) argv.push("--provider", provider);
|
|
116
|
+
const appResolver = optStr(a, "app_resolver");
|
|
117
|
+
if (appResolver) argv.push("--app-resolver", appResolver);
|
|
118
|
+
const api = optStr(a, "api");
|
|
119
|
+
if (api) argv.push("--api", api);
|
|
120
|
+
const swaggerUrl = optStr(a, "swagger_url");
|
|
121
|
+
if (swaggerUrl) argv.push("--swagger-url", swaggerUrl);
|
|
122
|
+
if (optBool(a, "inline_summary") === false) argv.push("--no-inline-summary");
|
|
123
|
+
return argv;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const flawlessRun: ToolDef = {
|
|
127
|
+
name: "flawless_run",
|
|
128
|
+
description:
|
|
129
|
+
"Generate or refresh a flawless-react suite. THIS WRITES FILES: the suite directory " +
|
|
130
|
+
"(.flawless, or flawless/ in framework mode), regenerated specs and harness, REPORT.md, and " +
|
|
131
|
+
"in Next.js/Expo mode the target project's package.json scripts and its config files. Every " +
|
|
132
|
+
"CLI option is available; omit `tasks` to run perf-harness alone, the documented " +
|
|
133
|
+
"non-interactive default. Runs a flawless-react child with --ci and returns its JSON blob " +
|
|
134
|
+
"(exitCode, outDir, results, keptScripts, testRun, errors) plus stderr diagnostics — a " +
|
|
135
|
+
"nonzero exit is an error result, never a green run. `run_tests` executes the generated " +
|
|
136
|
+
"suites, which is minutes and needs their prerequisites: a dev server for e2e " +
|
|
137
|
+
"(FLAWLESS_E2E_BASEURL, FLAWLESS_E2E_ID) and a seeded database for load pipes (LOAD_BASE, " +
|
|
138
|
+
"LOAD_AUTH). Call flawless_project_status first: an existing suite is regenerated wholesale, " +
|
|
139
|
+
"and hand-written assertions in it are lost unless they live in generated files.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
entry: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description:
|
|
146
|
+
"Component file or glob. Omit in Next.js/Expo mode to sweep what the framework detection " +
|
|
147
|
+
"finds. Relative paths resolve against `cwd`.",
|
|
148
|
+
},
|
|
149
|
+
tasks: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: { type: "string", enum: [...ALL_TASK_IDS] },
|
|
152
|
+
description: "Which tasks to run. Default: perf-harness only.",
|
|
153
|
+
},
|
|
154
|
+
nextjs: {
|
|
155
|
+
type: ["string", "boolean"],
|
|
156
|
+
description: "Next.js mode: project root, or true for the current directory.",
|
|
157
|
+
},
|
|
158
|
+
expo: {
|
|
159
|
+
type: ["string", "boolean"],
|
|
160
|
+
description:
|
|
161
|
+
"Expo / React Native mode: project root, or true for the current directory. Mutually " +
|
|
162
|
+
"exclusive with nextjs.",
|
|
163
|
+
},
|
|
164
|
+
out: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description:
|
|
167
|
+
"Suite output directory, relative to cwd. Default .flawless, flawless/ in framework mode.",
|
|
168
|
+
},
|
|
169
|
+
colocate: {
|
|
170
|
+
type: "boolean",
|
|
171
|
+
description: "Write unit specs beside each source file instead of into the suite.",
|
|
172
|
+
},
|
|
173
|
+
force: {
|
|
174
|
+
type: "boolean",
|
|
175
|
+
description:
|
|
176
|
+
"Overwrite a suite that exists. Without it an existing suite is left as-is (status kept).",
|
|
177
|
+
},
|
|
178
|
+
report: { type: "boolean", description: "Write REPORT.md." },
|
|
179
|
+
run_tests: {
|
|
180
|
+
type: "boolean",
|
|
181
|
+
description:
|
|
182
|
+
"Actually execute the generated suites and fold their exit code into the result. Slow, and " +
|
|
183
|
+
"each suite fails with its own named prerequisite when its environment is missing.",
|
|
184
|
+
},
|
|
185
|
+
changed: {
|
|
186
|
+
type: ["string", "boolean"],
|
|
187
|
+
description:
|
|
188
|
+
"Scoped mode: only components affected since a git ref (true = HEAD). A repo with no commits " +
|
|
189
|
+
"refuses rather than treating everything as deleted.",
|
|
190
|
+
},
|
|
191
|
+
roots: {
|
|
192
|
+
type: "string",
|
|
193
|
+
enum: [...ROOT_MODES],
|
|
194
|
+
description: "exported emits a tree per exported component — what a whole-app sweep needs.",
|
|
195
|
+
},
|
|
196
|
+
exclude: {
|
|
197
|
+
type: "array",
|
|
198
|
+
items: { type: "string" },
|
|
199
|
+
description: "Extra glob patterns to skip, added to the defaults.",
|
|
200
|
+
},
|
|
201
|
+
no_default_excludes: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
description: "Drop the built-in excludes (fixtures, shadcn/ui kit, copy, stories).",
|
|
204
|
+
},
|
|
205
|
+
depth: {
|
|
206
|
+
type: "integer",
|
|
207
|
+
minimum: 1,
|
|
208
|
+
maximum: 20,
|
|
209
|
+
description: "Component-tree depth to follow.",
|
|
210
|
+
},
|
|
211
|
+
variants: {
|
|
212
|
+
type: "integer",
|
|
213
|
+
minimum: 1,
|
|
214
|
+
maximum: 20,
|
|
215
|
+
description: "Prop variants per component.",
|
|
216
|
+
},
|
|
217
|
+
include_children: {
|
|
218
|
+
type: "boolean",
|
|
219
|
+
description:
|
|
220
|
+
"Follow JSX children through to leaf content. Default true; false is a speed control.",
|
|
221
|
+
},
|
|
222
|
+
heaps: {
|
|
223
|
+
type: "string",
|
|
224
|
+
enum: [...HEAP_MODES],
|
|
225
|
+
description: "How much of the V8 heap the profiling suite keeps. Default stress.",
|
|
226
|
+
},
|
|
227
|
+
props: {
|
|
228
|
+
type: "object",
|
|
229
|
+
description:
|
|
230
|
+
'Props override when the analyser cannot resolve a type, e.g. {"data": [{"id": "1"}]}.',
|
|
231
|
+
},
|
|
232
|
+
provider: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Provider wrapper file evaluated by the browser host.",
|
|
235
|
+
},
|
|
236
|
+
app_resolver: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Module resolver for the real-app browser host.",
|
|
239
|
+
},
|
|
240
|
+
api: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "OpenAPI spec for the MSW layer, or off to disable route discovery.",
|
|
243
|
+
},
|
|
244
|
+
swagger_url: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "Fetch the API spec from a URL, or off to disable it.",
|
|
247
|
+
},
|
|
248
|
+
inline_summary: {
|
|
249
|
+
type: "boolean",
|
|
250
|
+
description:
|
|
251
|
+
"Drop <Component>.flawless.json beside each source file. Default true, so false to opt out.",
|
|
252
|
+
},
|
|
253
|
+
cwd: { type: "string", description: "Working directory for the run. Default: the server's." },
|
|
254
|
+
env: {
|
|
255
|
+
type: "object",
|
|
256
|
+
additionalProperties: { type: "string" },
|
|
257
|
+
description:
|
|
258
|
+
"Extra environment variables, e.g. FLAWLESS_E2E_BASEURL for a run with run_tests.",
|
|
259
|
+
},
|
|
260
|
+
timeout_ms: {
|
|
261
|
+
type: "integer",
|
|
262
|
+
minimum: 1000,
|
|
263
|
+
maximum: 14400000,
|
|
264
|
+
description: `Watchdog; the process group is killed at the deadline. Default ${DEFAULT_TIMEOUT_MS}.`,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
required: [],
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
},
|
|
270
|
+
async run(args) {
|
|
271
|
+
requireKeys(args, RUN_ARGS);
|
|
272
|
+
const opts = callOpts(args);
|
|
273
|
+
const run = await runFlawlessChild(runArgv(args), opts);
|
|
274
|
+
return renderCiRun(run, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------- flawless_skills ----
|
|
279
|
+
|
|
280
|
+
const SKILLS_ARGS = [
|
|
281
|
+
"action",
|
|
282
|
+
"all",
|
|
283
|
+
"select",
|
|
284
|
+
"global",
|
|
285
|
+
"force",
|
|
286
|
+
"copy",
|
|
287
|
+
"cwd",
|
|
288
|
+
"timeout_ms",
|
|
289
|
+
] as const;
|
|
290
|
+
|
|
291
|
+
const flawlessSkills: ToolDef = {
|
|
292
|
+
name: "flawless_skills",
|
|
293
|
+
description:
|
|
294
|
+
"The fourteen agent skills flawless-react embeds, in the .agents/skills and .claude/skills " +
|
|
295
|
+
"layout Codex, Cursor, OpenCode and Claude Code read. action=status reads both scopes and " +
|
|
296
|
+
"writes nothing; action=install runs the CLI's install and WRITES those files (links by " +
|
|
297
|
+
"default, copies with `copy` for Claude Code). Without `all` or `select`, install installs " +
|
|
298
|
+
"every skill — the piped/CI behaviour, since this call has no terminal to answer a menu. " +
|
|
299
|
+
"A skill whose file a user edited is reported as edited and is only overwritten with `force`.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
action: {
|
|
304
|
+
type: "string",
|
|
305
|
+
enum: ["status", "install"],
|
|
306
|
+
description: "status reads; install writes.",
|
|
307
|
+
},
|
|
308
|
+
all: { type: "boolean", description: "install: every skill, no menu." },
|
|
309
|
+
select: {
|
|
310
|
+
type: "array",
|
|
311
|
+
items: { type: "string" },
|
|
312
|
+
description: "install: only these skill names.",
|
|
313
|
+
},
|
|
314
|
+
global: {
|
|
315
|
+
type: "boolean",
|
|
316
|
+
description: "install: target ~/.agents/skills instead of ./.agents/skills.",
|
|
317
|
+
},
|
|
318
|
+
force: {
|
|
319
|
+
type: "boolean",
|
|
320
|
+
description: "install: overwrite a SKILL.md the user edited locally.",
|
|
321
|
+
},
|
|
322
|
+
copy: {
|
|
323
|
+
type: "boolean",
|
|
324
|
+
description: "install: copy into .claude/skills instead of linking it.",
|
|
325
|
+
},
|
|
326
|
+
cwd: { type: "string", description: "Project directory for a project-scoped install." },
|
|
327
|
+
timeout_ms: { type: "integer", minimum: 1000, maximum: 300000 },
|
|
328
|
+
},
|
|
329
|
+
required: ["action"],
|
|
330
|
+
additionalProperties: false,
|
|
331
|
+
},
|
|
332
|
+
async run(args) {
|
|
333
|
+
requireKeys(args, SKILLS_ARGS, ["action"]);
|
|
334
|
+
const action = args.action;
|
|
335
|
+
if (action !== "status" && action !== "install") {
|
|
336
|
+
throw new ToolError('invalid argument "action": expected status or install');
|
|
337
|
+
}
|
|
338
|
+
const opts = callOpts(args);
|
|
339
|
+
const argv = ["skills", action === "install" ? "install" : "list"];
|
|
340
|
+
// Validated through the CLI's own parser, so an argument the verb does not
|
|
341
|
+
// take is refused with the CLI's message instead of being silently dropped.
|
|
342
|
+
parseSkillsCommand(argv.slice(1).concat(buildSkillFlags(args)));
|
|
343
|
+
argv.push(...buildSkillFlags(args));
|
|
344
|
+
const run = await runFlawlessChild(argv, opts);
|
|
345
|
+
return renderRun(run, opts.timeoutMs ?? 30_000);
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** install-only switches, in CLI spelling. `status` ignores them, and the parser
|
|
350
|
+
* above is what refuses them on a status call. */
|
|
351
|
+
function buildSkillFlags(a: JsonObject): string[] {
|
|
352
|
+
const out: string[] = [];
|
|
353
|
+
const select = optStrArray(a, "select");
|
|
354
|
+
if (optBool(a, "all")) out.push("--all");
|
|
355
|
+
if (select?.length) out.push("--select", select.join(","));
|
|
356
|
+
if (optBool(a, "global")) out.push("--global");
|
|
357
|
+
if (optBool(a, "force")) out.push("--force");
|
|
358
|
+
if (optBool(a, "copy")) out.push("--copy");
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const RUN_TOOLS: ToolDef[] = [flawlessRun, flawlessSkills];
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// `flawless-react mcp` — the Model Context Protocol server over stdio.
|
|
2
|
+
//
|
|
3
|
+
// Transport contract: one JSON-RPC 2.0 message per line on stdin and stdout.
|
|
4
|
+
// stdout belongs to the protocol EXCLUSIVELY, so before anything is served
|
|
5
|
+
// process.stdout.write is redirected to stderr and protocol frames go out on the
|
|
6
|
+
// saved real writer with drain backpressure. Two things make that more than
|
|
7
|
+
// hygiene here: the analyser and the scaffolder are libraries that narrate to
|
|
8
|
+
// stdout, and the read plane runs IN this process — without the redirect a single
|
|
9
|
+
// progress line would corrupt the frame the host is framing its own reads on.
|
|
10
|
+
// (Generation is different: it runs in a child, see run.ts, because the suites it
|
|
11
|
+
// executes inherit stdout.)
|
|
12
|
+
//
|
|
13
|
+
// Lifetime contract: the host owns the lifecycle. A signal (SIGINT, SIGTERM) kills
|
|
14
|
+
// any live child's process group and then drains the in-flight call, so a server
|
|
15
|
+
// that is taken down never leaves a generation — or under `--run-tests` a dev
|
|
16
|
+
// server and a browser — holding ports in someone's project. stdin EOF drains
|
|
17
|
+
// instead: `printf … | flawless-react mcp` closes the pipe as it writes the batch,
|
|
18
|
+
// and the work that batch asked for still finishes (its watchdog still bounds it).
|
|
19
|
+
// Closing the pipe is not a cancel.
|
|
20
|
+
//
|
|
21
|
+
// Calls are SERIALIZED, one in flight, and this is load-bearing rather than
|
|
22
|
+
// polite: the read plane runs with the process working directory set to the
|
|
23
|
+
// call's `root` (so globs, default excludes and tsconfig resolution behave exactly
|
|
24
|
+
// as they do when the same command is typed in that project). Two concurrent
|
|
25
|
+
// reads would each chdir out from under the other's glob and return findings for
|
|
26
|
+
// the wrong tree. One at a time makes that impossible by construction.
|
|
27
|
+
|
|
28
|
+
import { VERSION } from "../version.ts";
|
|
29
|
+
import {
|
|
30
|
+
INVALID_PARAMS,
|
|
31
|
+
type JsonObject,
|
|
32
|
+
type JsonValue,
|
|
33
|
+
METHOD_NOT_FOUND,
|
|
34
|
+
parseLine,
|
|
35
|
+
type RpcRequest,
|
|
36
|
+
type RpcResponse,
|
|
37
|
+
fail as rpcFail,
|
|
38
|
+
reply as rpcReply,
|
|
39
|
+
SERVER_NOT_INITIALIZED,
|
|
40
|
+
serialize,
|
|
41
|
+
} from "./protocol.ts";
|
|
42
|
+
import { killedChildCount, killLiveChildren } from "./run.ts";
|
|
43
|
+
import { parseMcpCommand, renderMcpHelp } from "./spec.ts";
|
|
44
|
+
import { ToolError } from "./support.ts";
|
|
45
|
+
import { ALL_TOOLS, invokeTool, toolByName, toolsList } from "./tools.ts";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Protocol versions this server answers, newest first.
|
|
49
|
+
*
|
|
50
|
+
* A client asking for one not listed here gets the newest version this dispatch
|
|
51
|
+
* actually implements rather than an echo of what it asked for: agreeing to a
|
|
52
|
+
* revision whose behaviour this file does not implement would be a promise
|
|
53
|
+
* flawless-react cannot keep.
|
|
54
|
+
*/
|
|
55
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The `flawless-react mcp` entry point. `argv` is everything AFTER the
|
|
59
|
+
* subcommand word (cli.ts strips it, the same way it does for `skills`).
|
|
60
|
+
*
|
|
61
|
+
* Help is answered here, on a real stdout, before the redirect below — after the
|
|
62
|
+
* redirect `--help` would print itself onto stderr and the host would see an empty
|
|
63
|
+
* protocol stream.
|
|
64
|
+
*/
|
|
65
|
+
export async function runMcpCli(argv: readonly string[]): Promise<number> {
|
|
66
|
+
let help = false;
|
|
67
|
+
try {
|
|
68
|
+
help = parseMcpCommand(argv).verb === "help";
|
|
69
|
+
} catch (err) {
|
|
70
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
if (help) {
|
|
74
|
+
process.stdout.write(renderMcpHelp());
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
return runMcp();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runMcp(): Promise<number> {
|
|
81
|
+
const realStdout = process.stdout.write.bind(process.stdout);
|
|
82
|
+
process.stdout.write = ((chunk: unknown): boolean => {
|
|
83
|
+
const text =
|
|
84
|
+
typeof chunk === "string"
|
|
85
|
+
? chunk
|
|
86
|
+
: chunk instanceof Uint8Array
|
|
87
|
+
? Buffer.from(chunk).toString("utf8")
|
|
88
|
+
: String(chunk);
|
|
89
|
+
return process.stderr.write(text);
|
|
90
|
+
}) as typeof process.stdout.write;
|
|
91
|
+
|
|
92
|
+
let initialized = false;
|
|
93
|
+
let exiting = false;
|
|
94
|
+
|
|
95
|
+
// One frame out, backpressure honoured: a frame the OS buffer cannot take yet
|
|
96
|
+
// is waited for, not dropped. Exiting on a non-empty write queue truncates the
|
|
97
|
+
// stream, and a truncated frame is a host that hangs waiting for its answer.
|
|
98
|
+
const writeLine = async (message: RpcResponse): Promise<void> => {
|
|
99
|
+
if (realStdout(serialize(message))) return;
|
|
100
|
+
await new Promise<void>((res) => process.stdout.once("drain", () => res()));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let chain: Promise<void> = Promise.resolve();
|
|
104
|
+
const enqueue = (task: () => Promise<void>): void => {
|
|
105
|
+
chain = chain.then(task, task);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleRequest = async (req: RpcRequest): Promise<RpcResponse | null> => {
|
|
109
|
+
switch (req.method) {
|
|
110
|
+
case "initialize": {
|
|
111
|
+
const requested =
|
|
112
|
+
typeof req.params?.protocolVersion === "string" ? req.params.protocolVersion : "";
|
|
113
|
+
const version = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
114
|
+
? requested
|
|
115
|
+
: (SUPPORTED_PROTOCOL_VERSIONS[0] as string);
|
|
116
|
+
initialized = true;
|
|
117
|
+
return rpcReply(req.id, {
|
|
118
|
+
protocolVersion: version,
|
|
119
|
+
capabilities: { tools: {} },
|
|
120
|
+
serverInfo: { name: "flawless-react", title: "flawless-react", version: VERSION },
|
|
121
|
+
instructions:
|
|
122
|
+
"flawless-react suite generation over MCP, in two planes. Run-plane tools " +
|
|
123
|
+
"(flawless_run, flawless_skills) change a project and execute a flawless-react " +
|
|
124
|
+
"child with --ci, so their exit codes and output are the CLI's own; read-plane " +
|
|
125
|
+
"tools open files and write nothing. Per-call cwd/env never persist — pass them " +
|
|
126
|
+
"on every call. A nonzero exit, a killed timeout and an unparseable example are " +
|
|
127
|
+
"isError results carrying the verbatim output, and every output cap announces " +
|
|
128
|
+
"itself: this server does not report a false pass.",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
case "ping":
|
|
132
|
+
return rpcReply(req.id, {});
|
|
133
|
+
case "tools/list":
|
|
134
|
+
if (!initialized) {
|
|
135
|
+
return rpcFail(req.id, {
|
|
136
|
+
code: SERVER_NOT_INITIALIZED,
|
|
137
|
+
message: `flawless-react mcp: send initialize before ${req.method}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return rpcReply(req.id, toolsList());
|
|
141
|
+
case "tools/call": {
|
|
142
|
+
if (!initialized) {
|
|
143
|
+
return rpcFail(req.id, {
|
|
144
|
+
code: SERVER_NOT_INITIALIZED,
|
|
145
|
+
message: "flawless-react mcp: send initialize before tools/call",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const name = req.params?.name;
|
|
149
|
+
if (typeof name !== "string") {
|
|
150
|
+
return rpcFail(req.id, { code: INVALID_PARAMS, message: 'tools/call: "name" required' });
|
|
151
|
+
}
|
|
152
|
+
const tool = toolByName(name);
|
|
153
|
+
if (!tool) {
|
|
154
|
+
// Unknown tool is a protocol-level error, per the spec: the tool never
|
|
155
|
+
// existed, so it cannot be the one answering. The list rides along — a
|
|
156
|
+
// host that guessed a name is exactly the caller that needs the eight.
|
|
157
|
+
return rpcFail(req.id, {
|
|
158
|
+
code: INVALID_PARAMS,
|
|
159
|
+
message: `unknown tool "${name}" — one of: ${ALL_TOOLS.map((t) => t.name).join(", ")}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const rawArgs = req.params?.arguments;
|
|
163
|
+
if (rawArgs !== undefined && (typeof rawArgs !== "object" || rawArgs === null)) {
|
|
164
|
+
return rpcFail(req.id, {
|
|
165
|
+
code: INVALID_PARAMS,
|
|
166
|
+
message: "tools/call: arguments must be an object",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const result = await invokeTool(tool, (rawArgs ?? {}) as JsonObject);
|
|
171
|
+
// ToolResult is plain data ({content, isError}); the cast is the
|
|
172
|
+
// JsonValue boundary, not a hole — it carries no methods.
|
|
173
|
+
return rpcReply(req.id, result as unknown as JsonValue);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Argument misuse and tool failure are a RESULT, not a transport error:
|
|
176
|
+
// the call was understood, and the answer is "no, with the reason".
|
|
177
|
+
// This is the repo's never-a-false-pass rule read at the protocol layer —
|
|
178
|
+
// burying the reason in an error frame the host may only log is how a
|
|
179
|
+
// failed generation starts reading as an inconclusive one.
|
|
180
|
+
const kind = err instanceof ToolError ? "invalid arguments" : "error";
|
|
181
|
+
return rpcReply(req.id, {
|
|
182
|
+
content: [{ type: "text", text: `flawless-react ${kind}: ${(err as Error).message}` }],
|
|
183
|
+
isError: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
return rpcFail(req.id, {
|
|
189
|
+
code: METHOD_NOT_FOUND,
|
|
190
|
+
message: `flawless-react mcp does not implement "${req.method}"`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// `kill` splits the two ways a server stops:
|
|
196
|
+
//
|
|
197
|
+
// - A SIGNAL means nobody is left to read and the process is being taken down by
|
|
198
|
+
// force. Kill FIRST, then drain: the other order spends minutes serving a host
|
|
199
|
+
// that is already gone, and the child spending them is the one still writing
|
|
200
|
+
// into the project. The kill makes the drain a formality — the call's own
|
|
201
|
+
// `proc.exited` is the promise that lands next.
|
|
202
|
+
// - EOF is different, because `printf … | flawless-react mcp` closes stdin the
|
|
203
|
+
// instant the batch is written. Those in-flight calls ARE the requests that
|
|
204
|
+
// were made; draining them lets a piped batch finish (a generation killed
|
|
205
|
+
// halfway would leave half-written specs in the project for no one's
|
|
206
|
+
// convenience). Closing the pipe is not a cancel — SIGTERM is.
|
|
207
|
+
const shutdown = async (reason: string, opts: { kill: boolean }): Promise<void> => {
|
|
208
|
+
if (exiting) return;
|
|
209
|
+
exiting = true;
|
|
210
|
+
if (opts.kill) killLiveChildren();
|
|
211
|
+
await chain.catch(() => {});
|
|
212
|
+
const killed = killedChildCount();
|
|
213
|
+
process.stderr.write(
|
|
214
|
+
`flawless-react mcp: ${reason}, shutting down${killed ? ` (${killed} child killed)` : ""}\n`,
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
// The signal name is passed as the reason rather than taken from the handler
|
|
218
|
+
// argument: this repo's `process.on` overloads resolve to a signal union that
|
|
219
|
+
// a `NodeJS.Signals`-typed parameter does not satisfy. Exiting here, after the
|
|
220
|
+
// drain, is what keeps a killed server from truncating a frame mid-write.
|
|
221
|
+
const onSignal = (signal: string): void => {
|
|
222
|
+
void shutdown(signal, { kill: true }).then(() => process.exit(0));
|
|
223
|
+
};
|
|
224
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
225
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
226
|
+
|
|
227
|
+
const reader = Bun.stdin.stream().getReader();
|
|
228
|
+
const decoder = new TextDecoder();
|
|
229
|
+
let buf = "";
|
|
230
|
+
try {
|
|
231
|
+
while (!exiting) {
|
|
232
|
+
const { done, value } = await reader.read();
|
|
233
|
+
if (done) break;
|
|
234
|
+
buf += decoder.decode(value, { stream: true });
|
|
235
|
+
let nl = buf.indexOf("\n");
|
|
236
|
+
while (nl !== -1) {
|
|
237
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
238
|
+
buf = buf.slice(nl + 1);
|
|
239
|
+
nl = buf.indexOf("\n");
|
|
240
|
+
if (!line.trim()) continue;
|
|
241
|
+
const parsed = parseLine(line);
|
|
242
|
+
if (parsed.kind === "invalid") {
|
|
243
|
+
const response = parsed.response;
|
|
244
|
+
enqueue(async () => void (await writeLine(response)));
|
|
245
|
+
} else if (parsed.kind === "request") {
|
|
246
|
+
const req = parsed.request;
|
|
247
|
+
enqueue(async () => {
|
|
248
|
+
const response = await handleRequest(req);
|
|
249
|
+
if (response) await writeLine(response);
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
const note = parsed.notification;
|
|
253
|
+
// The handshake's second half and the cancel notice: no frame back.
|
|
254
|
+
// Anything else is SAID to, on stderr, never swallowed silently — a
|
|
255
|
+
// host sending a method this server does not handle should know.
|
|
256
|
+
if (
|
|
257
|
+
note.method !== "notifications/initialized" &&
|
|
258
|
+
note.method !== "notifications/cancelled"
|
|
259
|
+
) {
|
|
260
|
+
process.stderr.write(`flawless-react mcp: ignoring notification "${note.method}"\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
process.stderr.write(`flawless-react mcp: stdin error: ${(err as Error).message}\n`);
|
|
267
|
+
// A broken transport is not a closed one: nothing more can be read or answered,
|
|
268
|
+
// so an in-flight child is killed rather than left working for a server that
|
|
269
|
+
// cannot report on it.
|
|
270
|
+
await shutdown("stdin error", { kill: true });
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
// stdin closed cleanly. Drain rather than kill: a piped batch closes the moment
|
|
274
|
+
// it is written, and the work it asked for is not less real for the pipe being
|
|
275
|
+
// gone. The drain is bounded by each call's own watchdog, so this is not
|
|
276
|
+
// unbounded — and no child outlives it.
|
|
277
|
+
await shutdown("stdin closed", { kill: false });
|
|
278
|
+
return 0;
|
|
279
|
+
}
|