@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.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +611 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
package/src/cli.ts ADDED
@@ -0,0 +1,560 @@
1
+ // core.ts (via the analyser) imports the TypeScript compiler — ~110ms of
2
+ // startup nobody running --help/--version should pay. Only type-only imports
3
+ // of that graph may appear here; the value imports of runCore and TASKS happen
4
+ // inside main(), after parseArgs and its fast paths have had their chance to
5
+ // exit. UsageError comes from types.ts, a leaf, precisely so this file never
6
+ // needs core.ts at load time.
7
+ import { FLAG_NAMES, renderHelp } from "./cliFlags.ts";
8
+ import type { CoreResult } from "./core.ts";
9
+ import { flawlessArgv, stripTrailingComment } from "./docLine.ts";
10
+ import { isMcpArgv, parseMcpCommand } from "./mcp/spec.ts";
11
+ import { confirmRunTests, outro, promptInteractive } from "./prompt.ts";
12
+ import { printSummaryTable } from "./reporters/findings.ts";
13
+ import {
14
+ isSkillsArgv,
15
+ parseSkillsCommand,
16
+ renderSkillsHelp,
17
+ type SkillsCommand,
18
+ } from "./skills/spec.ts";
19
+ import {
20
+ type CliArgs,
21
+ type HeapMode,
22
+ type PropsShape,
23
+ type RootMode,
24
+ type TaskId,
25
+ UsageError,
26
+ } from "./types.ts";
27
+ import { VERSION } from "./version.ts";
28
+
29
+ const VALID_TASK_IDS: ReadonlySet<TaskId> = new Set<TaskId>([
30
+ "perf-harness",
31
+ "unit-browser",
32
+ "perf-static",
33
+ "a11y",
34
+ "react-rules",
35
+ "e2e",
36
+ "load",
37
+ ]);
38
+
39
+ /** A closed set, so a typo fails at the CLI rather than silently profiling everything. */
40
+ function readHeapMode(raw: string | undefined): HeapMode {
41
+ if (raw === "off" || raw === "stress" || raw === "all") return raw;
42
+ throw new Error(`--heaps must be off, stress or all (got ${JSON.stringify(raw ?? "")})`);
43
+ }
44
+
45
+ export function parseArgs(argv: string[]): CliArgs {
46
+ let entry: string | undefined;
47
+ let out: string | undefined;
48
+ let depth = 3;
49
+ let variants = 5;
50
+ let heaps: HeapMode = "stress";
51
+ let changed: string | true | undefined;
52
+ let includeChildren = true;
53
+ let force = false;
54
+ let report = false;
55
+ let propsOverride: PropsShape | undefined;
56
+ let provider: string | undefined;
57
+ let apiSpec: string | undefined;
58
+ let tasks: TaskId[] | undefined;
59
+ let noInteractive = false;
60
+ let json = false;
61
+ let runTests = false;
62
+ let inlineSummary = true;
63
+ let nextjs: string | undefined;
64
+ let expo: string | undefined;
65
+ let appResolver: string | undefined;
66
+ let swaggerUrl: string | undefined;
67
+ let colocate = false;
68
+ let ci = false;
69
+ let roots: RootMode = "primary";
70
+ const exclude: string[] = [];
71
+ let useDefaultExcludes = true;
72
+ let help = false;
73
+ let version = false;
74
+
75
+ for (let i = 0; i < argv.length; i++) {
76
+ const a = argv[i];
77
+ if (a === undefined) continue;
78
+ if (a === "--out") {
79
+ const v = argv[++i];
80
+ if (v === undefined) throw new Error("--out requires a value");
81
+ out = v;
82
+ } else if (a.startsWith("--out=")) {
83
+ out = a.slice("--out=".length);
84
+ } else if (a === "--depth") {
85
+ const v = argv[++i];
86
+ if (v === undefined) throw new Error("--depth requires a value");
87
+ depth = Number(v);
88
+ } else if (a.startsWith("--depth=")) {
89
+ depth = Number(a.slice("--depth=".length));
90
+ } else if (a === "--changed") {
91
+ // Optional value: `--changed` alone means HEAD, `--changed main` a ref.
92
+ const next = argv[i + 1];
93
+ changed = next !== undefined && !next.startsWith("-") ? (argv[++i] as string) : true;
94
+ } else if (a.startsWith("--changed=")) {
95
+ changed = a.slice("--changed=".length) || true;
96
+ } else if (a === "--heaps") {
97
+ heaps = readHeapMode(argv[++i]);
98
+ } else if (a.startsWith("--heaps=")) {
99
+ heaps = readHeapMode(a.slice("--heaps=".length));
100
+ } else if (a === "--variants") {
101
+ const v = argv[++i];
102
+ if (v === undefined) throw new Error("--variants requires a value");
103
+ variants = Number(v);
104
+ } else if (a.startsWith("--variants=")) {
105
+ variants = Number(a.slice("--variants=".length));
106
+ } else if (a === "--include-children") {
107
+ includeChildren = true;
108
+ } else if (a.startsWith("--include-children=")) {
109
+ const v = a.slice("--include-children=".length).toLowerCase();
110
+ includeChildren = !(v === "false" || v === "0" || v === "no");
111
+ } else if (a === "--props") {
112
+ const v = argv[++i];
113
+ if (v === undefined) throw new Error("--props requires a JSON value");
114
+ propsOverride = parsePropsJson(v);
115
+ } else if (a.startsWith("--props=")) {
116
+ propsOverride = parsePropsJson(a.slice("--props=".length));
117
+ } else if (a === "--provider") {
118
+ const v = argv[++i];
119
+ if (v === undefined) throw new Error("--provider requires a file path");
120
+ provider = v;
121
+ } else if (a.startsWith("--provider=")) {
122
+ provider = a.slice("--provider=".length);
123
+ } else if (a === "--api") {
124
+ const v = argv[++i];
125
+ if (v === undefined) throw new Error("--api requires a file path");
126
+ apiSpec = v;
127
+ } else if (a.startsWith("--api=")) {
128
+ apiSpec = a.slice("--api=".length);
129
+ } else if (a === "--tasks") {
130
+ const v = argv[++i];
131
+ if (v === undefined) throw new Error("--tasks requires a csv value");
132
+ tasks = parseTaskList(v);
133
+ } else if (a.startsWith("--tasks=")) {
134
+ tasks = parseTaskList(a.slice("--tasks=".length));
135
+ } else if (a === "--nextjs") {
136
+ const peek = argv[i + 1];
137
+ if (peek !== undefined && !peek.startsWith("-")) {
138
+ nextjs = argv[++i] as string;
139
+ } else {
140
+ nextjs = ".";
141
+ }
142
+ } else if (a.startsWith("--nextjs=")) {
143
+ nextjs = a.slice("--nextjs=".length) || ".";
144
+ } else if (a === "--expo") {
145
+ const peek = argv[i + 1];
146
+ if (peek !== undefined && !peek.startsWith("-")) {
147
+ expo = argv[++i] as string;
148
+ } else {
149
+ expo = ".";
150
+ }
151
+ } else if (a.startsWith("--expo=")) {
152
+ expo = a.slice("--expo=".length) || ".";
153
+ } else if (a === "--app-resolver") {
154
+ const v = argv[++i];
155
+ if (v === undefined) throw new Error("--app-resolver requires a file path");
156
+ appResolver = v;
157
+ } else if (a.startsWith("--app-resolver=")) {
158
+ appResolver = a.slice("--app-resolver=".length);
159
+ } else if (a === "--swagger-url") {
160
+ const v = argv[++i];
161
+ if (v === undefined) throw new Error("--swagger-url requires a value");
162
+ swaggerUrl = v;
163
+ } else if (a.startsWith("--swagger-url=")) {
164
+ swaggerUrl = a.slice("--swagger-url=".length);
165
+ } else if (a === "--roots" || a.startsWith("--roots=")) {
166
+ const v = a.startsWith("--roots=") ? a.slice("--roots=".length) : argv[++i];
167
+ if (v !== "primary" && v !== "exported") {
168
+ throw new Error(`--roots must be "primary" or "exported", got: ${v ?? "<missing>"}`);
169
+ }
170
+ roots = v;
171
+ } else if (a === "--exclude" || a.startsWith("--exclude=")) {
172
+ const v = a.startsWith("--exclude=") ? a.slice("--exclude=".length) : argv[++i];
173
+ if (v === undefined) throw new Error("--exclude requires a glob");
174
+ exclude.push(v);
175
+ } else if (a === "--no-default-excludes") {
176
+ useDefaultExcludes = false;
177
+ } else if (a === "--colocate") {
178
+ colocate = true;
179
+ } else if (a === "--ci") {
180
+ ci = true;
181
+ noInteractive = true;
182
+ json = true;
183
+ } else if (a === "--no-interactive") {
184
+ noInteractive = true;
185
+ } else if (a === "--run-tests") {
186
+ runTests = true;
187
+ } else if (a === "--no-inline-summary") {
188
+ inlineSummary = false;
189
+ } else if (a === "--json") {
190
+ json = true;
191
+ } else if (a === "--force") {
192
+ force = true;
193
+ } else if (a === "--report") {
194
+ report = true;
195
+ } else if (a === "-h" || a === "--help") {
196
+ // `break`, not a return: --help used to process.exit(0) right here, so
197
+ // everything after it went unread and the post-loop validation never ran.
198
+ // Both are preserved deliberately — `--depth=-1 --help` printed help then,
199
+ // and prints help now, rather than becoming a usage error.
200
+ help = true;
201
+ break;
202
+ } else if (a === "-V" || a === "--version") {
203
+ version = true;
204
+ break;
205
+ } else if (a.startsWith("--")) {
206
+ throw new Error(`unknown flag: ${a}`);
207
+ } else if (entry === undefined) {
208
+ entry = a;
209
+ } else {
210
+ throw new Error(`unexpected positional: ${a}`);
211
+ }
212
+ }
213
+
214
+ if (!help && !version) {
215
+ // A project is one or the other. Checked here rather than in either detector
216
+ // so the message names the conflict instead of whichever ran first, and so
217
+ // it costs nothing on the --help path.
218
+ if (nextjs !== undefined && expo !== undefined) {
219
+ throw new Error(
220
+ "--nextjs and --expo are mutually exclusive: a project is a Next.js app or an Expo app, not both.",
221
+ );
222
+ }
223
+ if (!Number.isFinite(depth) || depth < 0) throw new Error(`--depth must be >= 0`);
224
+ if (!Number.isFinite(variants) || variants < 0) throw new Error(`--variants must be >= 0`);
225
+ }
226
+
227
+ return {
228
+ entry,
229
+ out,
230
+ depth,
231
+ variants,
232
+ heaps,
233
+ changed,
234
+ includeChildren,
235
+ force,
236
+ report,
237
+ propsOverride,
238
+ provider,
239
+ apiSpec,
240
+ tasks,
241
+ noInteractive,
242
+ json,
243
+ runTests,
244
+ inlineSummary,
245
+ nextjs,
246
+ expo,
247
+ appResolver,
248
+ swaggerUrl,
249
+ colocate,
250
+ ci,
251
+ roots,
252
+ exclude,
253
+ useDefaultExcludes,
254
+ help,
255
+ version,
256
+ };
257
+ }
258
+
259
+ /** A raw JSON.parse error ("JSON Parse error: …") names no flag; keep the
260
+ * diagnostic but say which flag carried the bad value. */
261
+ function parsePropsJson(raw: string): PropsShape {
262
+ try {
263
+ return JSON.parse(raw) as PropsShape;
264
+ } catch (err) {
265
+ throw new Error(`--props is not valid JSON: ${(err as Error).message}`);
266
+ }
267
+ }
268
+
269
+ function parseTaskList(s: string): TaskId[] {
270
+ const parts = s
271
+ .split(",")
272
+ .map((p) => p.trim())
273
+ .filter(Boolean);
274
+ const ids: TaskId[] = [];
275
+ for (const p of parts) {
276
+ if (!VALID_TASK_IDS.has(p as TaskId)) {
277
+ throw new Error(`unknown task id: ${p}. Available: ${[...VALID_TASK_IDS].join(", ")}`);
278
+ }
279
+ ids.push(p as TaskId);
280
+ }
281
+ return ids;
282
+ }
283
+
284
+ export interface LineProblem {
285
+ line: string;
286
+ problem: string;
287
+ }
288
+
289
+ export interface CheckReport {
290
+ /** flawless-react invocations examined, whether or not they parsed. */
291
+ checked: number;
292
+ /** Lines that invoke something else. Not a failure, but see the note below. */
293
+ skipped: number;
294
+ problems: LineProblem[];
295
+ }
296
+
297
+ /**
298
+ * Parse every documented flawless-react invocation in `text` without running it.
299
+ *
300
+ * The lint for shipped examples. Parsing is pure — parseArgs opens no file and
301
+ * spawns nothing — so an example naming ./src/Dashboard.tsx or a seeded database
302
+ * checks clean on a machine that has neither.
303
+ *
304
+ * `checked === 0` is reported, never smoothed over. A run that examined nothing
305
+ * and said "ok" is the exact false pass this tool exists to eliminate, and it is
306
+ * the likeliest failure of a doc lint: the extractor breaks, no line survives to
307
+ * be checked, and the gate stays green while the docs rot behind it.
308
+ */
309
+ export function checkLines(text: string): CheckReport {
310
+ const report: CheckReport = { checked: 0, skipped: 0, problems: [] };
311
+ for (const raw of text.split("\n")) {
312
+ const line = stripTrailingComment(raw).trim();
313
+ if (line === "" || line.startsWith("#")) continue;
314
+ let argv: string[] | null;
315
+ try {
316
+ argv = flawlessArgv(line);
317
+ } catch (err) {
318
+ report.checked++;
319
+ report.problems.push({ line, problem: (err as Error).message });
320
+ continue;
321
+ }
322
+ if (argv === null) {
323
+ report.skipped++;
324
+ continue;
325
+ }
326
+ report.checked++;
327
+ try {
328
+ // Two other grammars, neither of which parseArgs speaks: `skills …` is a
329
+ // subcommand and `--flags` a bare switch. Left to parseArgs, a working
330
+ // `flawless-react skills install --all` would be reported broken (its verb
331
+ // read as an entry glob) and a stale flag in prose would pass uncaught —
332
+ // both directions of the same silence. `mcp …` is a third: without it a
333
+ // documented `flawless-react mcp` line passes as an entry glob named "mcp",
334
+ // which is the wrong-reason pass skills/spec.ts exists to prevent.
335
+ if (isSkillsArgv(argv)) parseSkillsCommand(argv.slice(1));
336
+ else if (isMcpArgv(argv)) parseMcpCommand(argv.slice(1));
337
+ else if (argv[0] === "--flags") {
338
+ if (argv.length > 1) throw new Error("--flags takes no arguments");
339
+ } else parseArgs(argv);
340
+ } catch (err) {
341
+ report.problems.push({ line, problem: (err as Error).message });
342
+ }
343
+ }
344
+ return report;
345
+ }
346
+
347
+ export async function main(argv: string[]): Promise<void> {
348
+ // The skills subcommand, ahead of parseArgs for the same reason as the two
349
+ // surfaces below it: `skills` is not an entry glob, and falling through would
350
+ // mean parsing it as one. The bytes stay behind a dynamic import — the grammar
351
+ // here costs a few hundred bytes, the ~70 KB of embedded SKILL.md comes in only
352
+ // once a verb is actually being run.
353
+ if (isSkillsArgv(argv)) {
354
+ let cmd: SkillsCommand;
355
+ try {
356
+ cmd = parseSkillsCommand(argv.slice(1));
357
+ } catch (err) {
358
+ process.stderr.write(`${(err as Error).message}\n`);
359
+ process.exit(2);
360
+ }
361
+ if (cmd.verb === "help") {
362
+ process.stdout.write(renderSkillsHelp());
363
+ process.exit(0);
364
+ }
365
+ const { runSkillsCommand } = await import("./skills/cli.ts");
366
+ process.exit(await runSkillsCommand(cmd));
367
+ }
368
+
369
+ // The MCP server, dispatched for the same reason as `skills` above: `mcp` is a
370
+ // subcommand, not an entry glob. Falling through would start a generation run
371
+ // over a project called "mcp" — and the docs lint would still call that line
372
+ // clean. The whole server (the analyser, the rules, the child plane) stays
373
+ // behind a dynamic import so `--help` and `--check` never pay for the TypeScript
374
+ // compiler it reaches through (FR-031); only a host that actually speaks the
375
+ // protocol loads it.
376
+ if (isMcpArgv(argv)) {
377
+ const { runMcpCli } = await import("./mcp/server.ts");
378
+ process.exit(await runMcpCli(argv.slice(1)));
379
+ }
380
+
381
+ // Two parse-only surfaces, ahead of parseArgs and well ahead of the core
382
+ // graph. They exist for consumers that CANNOT import this module: the website
383
+ // repo keeps hand-copied examples and a "every flag flawless-react accepts"
384
+ // table, and its CI can run the published binary even though it can have no
385
+ // opinion about this source tree. Both must stay clear of core.ts, or --help
386
+ // and --check start paying for the TypeScript compiler (FR-031).
387
+ if (argv[0] === "--flags") {
388
+ if (argv.length > 1) {
389
+ process.stderr.write("--flags takes no arguments\n");
390
+ process.exit(2);
391
+ }
392
+ process.stdout.write(`${FLAG_NAMES.join("\n")}\n`);
393
+ process.exit(0);
394
+ }
395
+ if (argv[0] === "--check") {
396
+ if (argv.length < 2) {
397
+ process.stderr.write("--check requires a line to check\n");
398
+ process.exit(2);
399
+ }
400
+ // Several operands are joined rather than rejected: a caller feeding a whole
401
+ // doc page one block per argument is the normal shape, and an unquoted line
402
+ // would otherwise be silently truncated to its first word.
403
+ const report = checkLines(argv.slice(1).join("\n"));
404
+ for (const p of report.problems) {
405
+ process.stderr.write(`${p.problem}\n in: ${p.line}\n`);
406
+ }
407
+ if (report.problems.length > 0) process.exit(1);
408
+ if (report.checked === 0) {
409
+ process.stderr.write(
410
+ `--check found no flawless-react invocation in ${report.skipped} line(s)\n`,
411
+ );
412
+ process.exit(1);
413
+ }
414
+ process.stdout.write(
415
+ `ok: ${report.checked} invocation(s) parse (${report.skipped} line(s) skipped)\n`,
416
+ );
417
+ process.exit(0);
418
+ }
419
+
420
+ let args: CliArgs;
421
+ try {
422
+ args = parseArgs(argv);
423
+ } catch (err) {
424
+ process.stderr.write(`${(err as Error).message}\n`);
425
+ process.exit(2);
426
+ }
427
+
428
+ // Printed here rather than inside parseArgs, so that parsing a documented
429
+ // line stays a pure function call. It used to process.exit(0) mid-loop, which
430
+ // meant no test — and no lint — could parse `flawless-react --help` in-process.
431
+ if (args.help) {
432
+ process.stdout.write(renderHelp());
433
+ process.exit(0);
434
+ }
435
+ if (args.version) {
436
+ process.stdout.write(`flawless-react ${VERSION}\n`);
437
+ process.exit(0);
438
+ }
439
+
440
+ let pattern = args.entry;
441
+ let outDir = args.out;
442
+ let taskIds = args.tasks;
443
+
444
+ const wantInteractive = !args.noInteractive && pattern === undefined && process.stdin.isTTY;
445
+ if (wantInteractive) {
446
+ // Deferred with core.ts: tasks/index.ts pulls every task module, and only
447
+ // the interactive menu needs the registry before runCore does.
448
+ const { TASKS } = await import("./tasks/index.ts");
449
+ const picked = await promptInteractive({
450
+ taskChoices: TASKS.map((t) => ({ id: t.id, label: t.label, description: t.description })),
451
+ defaultTaskIds: ["perf-harness", "perf-static"],
452
+ });
453
+ if (!picked) {
454
+ process.exit(0);
455
+ }
456
+ pattern = picked.pattern;
457
+ outDir = picked.outDir;
458
+ taskIds = picked.tasks;
459
+ }
460
+
461
+ if (pattern === undefined && args.nextjs === undefined && args.expo === undefined) {
462
+ process.stderr.write(
463
+ "missing entry path or glob. Pass a .tsx path, a glob, or run in a TTY for the interactive menu. See --help.\n",
464
+ );
465
+ process.exit(2);
466
+ }
467
+
468
+ // First value-touch of the core graph (and with it the TypeScript compiler):
469
+ // every exit path above must stay reachable without paying for it.
470
+ const { runCore } = await import("./core.ts");
471
+
472
+ let result: CoreResult;
473
+ try {
474
+ result = await runCore({
475
+ pattern: pattern as string,
476
+ outDir,
477
+ taskIds,
478
+ depth: args.depth,
479
+ variants: args.variants,
480
+ heaps: args.heaps,
481
+ ...(args.changed === undefined ? {} : { changed: args.changed }),
482
+ includeChildren: args.includeChildren,
483
+ force: args.force,
484
+ report: args.report,
485
+ propsOverride: args.propsOverride,
486
+ provider: args.provider ?? args.appResolver,
487
+ apiSpec: args.apiSpec,
488
+ inlineSummary: args.inlineSummary,
489
+ nextjs: args.nextjs,
490
+ expo: args.expo,
491
+ swaggerUrl: args.swaggerUrl,
492
+ colocate: args.colocate,
493
+ roots: args.roots,
494
+ exclude: args.exclude,
495
+ useDefaultExcludes: args.useDefaultExcludes,
496
+ // --json (and --ci, which implies it) asks for a machine-readable run, so
497
+ // the progress and finding lines have to stay off stdout or the blob is
498
+ // preceded by prose and the whole thing fails to parse.
499
+ log: !args.json,
500
+ runTests: args.runTests || (wantInteractive && (await confirmRunTests())),
501
+ });
502
+ } catch (err) {
503
+ process.stderr.write(`${(err as Error).message}\n`);
504
+ process.exit(err instanceof UsageError ? 2 : 1);
505
+ }
506
+
507
+ if (!args.json) printSummaryTable(result.results);
508
+ if (args.json) {
509
+ // exitCode rides inside the blob: suite failures under --run-tests fold
510
+ // into it, and a --ci consumer parsing stdout alone must see them without
511
+ // shell plumbing around $?.
512
+ //
513
+ // testRun and errors are what make the exit code EXPLICABLE rather than
514
+ // merely correct. Without testRun, exit 0 conflates "the suites passed"
515
+ // with "no suite ran" (B-14); without errors, a failure belonging to no
516
+ // task gives exit 1 with every result ok:true and no reason anywhere
517
+ // (FR-041). Both are omitted from the JSON when empty/absent — `testRun`
518
+ // because absent genuinely means "no run was asked for".
519
+ process.stdout.write(
520
+ `${JSON.stringify(
521
+ {
522
+ exitCode: result.exitCode,
523
+ outDir: result.outDir,
524
+ results: result.results,
525
+ keptScripts: result.keptScripts,
526
+ testRun: result.testRun,
527
+ ...(result.errors.length > 0 ? { errors: result.errors } : {}),
528
+ },
529
+ null,
530
+ 2,
531
+ )}\n`,
532
+ );
533
+ }
534
+ if (wantInteractive) {
535
+ // The skills question, asked here rather than never: an interactive run is a
536
+ // person who has just watched the tool work, with a terminal in front of them.
537
+ // It goes BEFORE the outro because clack owns the session, and the gate is the
538
+ // project having no `flawless-*` skill yet — so answering it is what stops it
539
+ // being asked, with no marker file trailing the user. npm's postinstall asks
540
+ // the same question at install time; `FLAWLESS_NO_SKILLS` silences both.
541
+ const { defaultSkillsIO, offerSkills } = await import("./skills/cli.ts");
542
+ await offerSkills(defaultSkillsIO());
543
+ await outro(result.exitCode !== 0 ? "done (with failures)" : "done");
544
+ }
545
+ process.exit(result.exitCode);
546
+ }
547
+
548
+ // Harmless when imported (bin.ts, the compiled binary, tests) and the
549
+ // difference between a run and a silent exit-0 no-op when someone invokes
550
+ // `bun src/cli.ts …` directly — the trap bin.ts's header documents.
551
+ if (import.meta.main) {
552
+ try {
553
+ await main(Bun.argv.slice(2));
554
+ } catch (err) {
555
+ // A rejection escaping main() must land on the documented exit-1 contract,
556
+ // not surface as the runtime's uncaught-error dump.
557
+ process.stderr.write(`${String((err as Error | undefined)?.stack ?? err)}\n`);
558
+ process.exit(1);
559
+ }
560
+ }