@agent-scope/cli 1.18.1 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +907 -292
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +716 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +716 -110
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync,
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync, statSync, appendFileSync, readdirSync, rmSync, createReadStream } from 'fs';
|
|
2
2
|
import { resolve, join, extname, dirname } from 'path';
|
|
3
3
|
import { generateManifest } from '@agent-scope/manifest';
|
|
4
4
|
import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
@@ -167,13 +167,15 @@ function buildTable(headers, rows) {
|
|
|
167
167
|
}
|
|
168
168
|
function formatListTable(rows) {
|
|
169
169
|
if (rows.length === 0) return "No components found.";
|
|
170
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
170
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS", "COLLECTION", "INTERNAL"];
|
|
171
171
|
const tableRows = rows.map((r) => [
|
|
172
172
|
r.name,
|
|
173
173
|
r.file,
|
|
174
174
|
r.complexityClass,
|
|
175
175
|
String(r.hookCount),
|
|
176
|
-
String(r.contextCount)
|
|
176
|
+
String(r.contextCount),
|
|
177
|
+
r.collection ?? "\u2014",
|
|
178
|
+
r.internal ? "yes" : "no"
|
|
177
179
|
]);
|
|
178
180
|
return buildTable(headers, tableRows);
|
|
179
181
|
}
|
|
@@ -204,6 +206,8 @@ function formatGetTable(name, descriptor) {
|
|
|
204
206
|
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
205
207
|
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
206
208
|
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
209
|
+
` Collection: ${descriptor.collection ?? "\u2014"}`,
|
|
210
|
+
` Internal: ${descriptor.internal}`,
|
|
207
211
|
"",
|
|
208
212
|
` Props (${propNames.length}):`
|
|
209
213
|
];
|
|
@@ -226,8 +230,16 @@ function formatGetJson(name, descriptor) {
|
|
|
226
230
|
}
|
|
227
231
|
function formatQueryTable(rows, queryDesc) {
|
|
228
232
|
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
229
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
230
|
-
const tableRows = rows.map((r) => [
|
|
233
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS", "COLLECTION", "INTERNAL"];
|
|
234
|
+
const tableRows = rows.map((r) => [
|
|
235
|
+
r.name,
|
|
236
|
+
r.file,
|
|
237
|
+
r.complexityClass,
|
|
238
|
+
r.hooks,
|
|
239
|
+
r.contexts,
|
|
240
|
+
r.collection ?? "\u2014",
|
|
241
|
+
r.internal ? "yes" : "no"
|
|
242
|
+
]);
|
|
231
243
|
return `Query: ${queryDesc}
|
|
232
244
|
|
|
233
245
|
${buildTable(headers, tableRows)}`;
|
|
@@ -517,10 +529,10 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
517
529
|
return build3(deduped);
|
|
518
530
|
}
|
|
519
531
|
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
520
|
-
const { existsSync:
|
|
532
|
+
const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import('fs');
|
|
521
533
|
const { createRequire: createRequire3 } = await import('module');
|
|
522
|
-
if (!
|
|
523
|
-
const raw =
|
|
534
|
+
if (!existsSync16(cssFilePath)) return null;
|
|
535
|
+
const raw = readFileSync14(cssFilePath, "utf-8");
|
|
524
536
|
const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
|
|
525
537
|
if (!needsCompile) {
|
|
526
538
|
return raw;
|
|
@@ -946,7 +958,7 @@ function parseChecks(raw) {
|
|
|
946
958
|
}
|
|
947
959
|
function createCiCommand() {
|
|
948
960
|
return new Command("ci").description(
|
|
949
|
-
"Run
|
|
961
|
+
"Run the full Scope pipeline non-interactively and exit with a structured code.\n\nPIPELINE STEPS (in order):\n 1. manifest generate scan source, build .reactscope/manifest.json\n 2. render all screenshot every component\n 3. tokens compliance score on-token CSS coverage\n 4. visual regression pixel diff against --baseline (if provided)\n\nCHECKS (--checks flag, comma-separated):\n compliance token coverage below --threshold \u2192 exit 1\n a11y accessibility violations \u2192 exit 2\n console-errors console.error during render \u2192 exit 3\n visual-regression pixel diff against baseline \u2192 exit 4\n (render failures always \u2192 exit 5 regardless of --checks)\n\nEXIT CODES:\n 0 all checks passed\n 1 compliance below threshold\n 2 accessibility violations\n 3 console errors during render\n 4 visual regression detected\n 5 component render failures\n\nExamples:\n scope ci\n scope ci --baseline .reactscope/baseline --threshold 0.95\n scope ci --checks compliance,a11y --json -o ci-result.json\n scope ci --viewport 1280x720"
|
|
950
962
|
).option(
|
|
951
963
|
"-b, --baseline <dir>",
|
|
952
964
|
"Baseline directory for visual regression comparison (omit to skip)"
|
|
@@ -989,6 +1001,192 @@ function createCiCommand() {
|
|
|
989
1001
|
}
|
|
990
1002
|
);
|
|
991
1003
|
}
|
|
1004
|
+
function collectSourceFiles(dir) {
|
|
1005
|
+
if (!existsSync(dir)) return [];
|
|
1006
|
+
const results = [];
|
|
1007
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1008
|
+
const full = join(dir, entry.name);
|
|
1009
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
|
|
1010
|
+
results.push(...collectSourceFiles(full));
|
|
1011
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
1012
|
+
results.push(full);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return results;
|
|
1016
|
+
}
|
|
1017
|
+
function checkConfig(cwd) {
|
|
1018
|
+
const configPath = resolve(cwd, "reactscope.config.json");
|
|
1019
|
+
if (!existsSync(configPath)) {
|
|
1020
|
+
return {
|
|
1021
|
+
name: "config",
|
|
1022
|
+
status: "error",
|
|
1023
|
+
message: "reactscope.config.json not found \u2014 run `scope init`"
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1028
|
+
return { name: "config", status: "ok", message: "reactscope.config.json valid" };
|
|
1029
|
+
} catch {
|
|
1030
|
+
return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function checkTokens(cwd) {
|
|
1034
|
+
const configPath = resolve(cwd, "reactscope.config.json");
|
|
1035
|
+
let tokensPath = resolve(cwd, "reactscope.tokens.json");
|
|
1036
|
+
if (existsSync(configPath)) {
|
|
1037
|
+
try {
|
|
1038
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1039
|
+
if (cfg.tokens?.file) tokensPath = resolve(cwd, cfg.tokens.file);
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (!existsSync(tokensPath)) {
|
|
1044
|
+
return {
|
|
1045
|
+
name: "tokens",
|
|
1046
|
+
status: "warn",
|
|
1047
|
+
message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const raw = JSON.parse(readFileSync(tokensPath, "utf-8"));
|
|
1052
|
+
if (!raw.version) {
|
|
1053
|
+
return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
|
|
1054
|
+
}
|
|
1055
|
+
return { name: "tokens", status: "ok", message: "Token file valid" };
|
|
1056
|
+
} catch {
|
|
1057
|
+
return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function checkGlobalCss(cwd) {
|
|
1061
|
+
const configPath = resolve(cwd, "reactscope.config.json");
|
|
1062
|
+
let globalCss = [];
|
|
1063
|
+
if (existsSync(configPath)) {
|
|
1064
|
+
try {
|
|
1065
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1066
|
+
globalCss = cfg.components?.wrappers?.globalCSS ?? [];
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (globalCss.length === 0) {
|
|
1071
|
+
return {
|
|
1072
|
+
name: "globalCSS",
|
|
1073
|
+
status: "warn",
|
|
1074
|
+
message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
const missing = globalCss.filter((f) => !existsSync(resolve(cwd, f)));
|
|
1078
|
+
if (missing.length > 0) {
|
|
1079
|
+
return {
|
|
1080
|
+
name: "globalCSS",
|
|
1081
|
+
status: "error",
|
|
1082
|
+
message: `globalCSS file(s) not found: ${missing.join(", ")}`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
name: "globalCSS",
|
|
1087
|
+
status: "ok",
|
|
1088
|
+
message: `${globalCss.length} globalCSS file(s) present`
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function checkManifest(cwd) {
|
|
1092
|
+
const manifestPath = resolve(cwd, ".reactscope", "manifest.json");
|
|
1093
|
+
if (!existsSync(manifestPath)) {
|
|
1094
|
+
return {
|
|
1095
|
+
name: "manifest",
|
|
1096
|
+
status: "warn",
|
|
1097
|
+
message: "Manifest not found \u2014 run `scope manifest generate`"
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
const manifestMtime = statSync(manifestPath).mtimeMs;
|
|
1101
|
+
const sourceDir = resolve(cwd, "src");
|
|
1102
|
+
const sourceFiles = collectSourceFiles(sourceDir);
|
|
1103
|
+
const stale = sourceFiles.filter((f) => statSync(f).mtimeMs > manifestMtime);
|
|
1104
|
+
if (stale.length > 0) {
|
|
1105
|
+
return {
|
|
1106
|
+
name: "manifest",
|
|
1107
|
+
status: "warn",
|
|
1108
|
+
message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
|
|
1112
|
+
}
|
|
1113
|
+
var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
|
|
1114
|
+
function formatCheck(check) {
|
|
1115
|
+
return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
|
|
1116
|
+
}
|
|
1117
|
+
function createDoctorCommand() {
|
|
1118
|
+
return new Command("doctor").description(
|
|
1119
|
+
`Verify your Scope project setup before running other commands.
|
|
1120
|
+
|
|
1121
|
+
CHECKS PERFORMED:
|
|
1122
|
+
config reactscope.config.json exists and is valid JSON
|
|
1123
|
+
tokens reactscope.tokens.json exists and passes validation
|
|
1124
|
+
css globalCSS files referenced in config actually exist
|
|
1125
|
+
manifest .reactscope/manifest.json exists and is not stale
|
|
1126
|
+
(stale = source files modified after last generate)
|
|
1127
|
+
|
|
1128
|
+
STATUS LEVELS: ok | warn | error
|
|
1129
|
+
|
|
1130
|
+
Run this first whenever renders fail or produce unexpected output.
|
|
1131
|
+
|
|
1132
|
+
Examples:
|
|
1133
|
+
scope doctor
|
|
1134
|
+
scope doctor --json
|
|
1135
|
+
scope doctor --json | jq '.checks[] | select(.status == "error")'`
|
|
1136
|
+
).option("--json", "Emit structured JSON output", false).action((opts) => {
|
|
1137
|
+
const cwd = process.cwd();
|
|
1138
|
+
const checks = [
|
|
1139
|
+
checkConfig(cwd),
|
|
1140
|
+
checkTokens(cwd),
|
|
1141
|
+
checkGlobalCss(cwd),
|
|
1142
|
+
checkManifest(cwd)
|
|
1143
|
+
];
|
|
1144
|
+
const errors = checks.filter((c) => c.status === "error").length;
|
|
1145
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
1146
|
+
if (opts.json) {
|
|
1147
|
+
process.stdout.write(
|
|
1148
|
+
`${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
|
|
1149
|
+
`
|
|
1150
|
+
);
|
|
1151
|
+
if (errors > 0) process.exit(1);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
process.stdout.write("\nScope Doctor\n");
|
|
1155
|
+
process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1156
|
+
for (const check of checks) process.stdout.write(`${formatCheck(check)}
|
|
1157
|
+
`);
|
|
1158
|
+
process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1159
|
+
if (errors > 0) {
|
|
1160
|
+
process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
|
|
1161
|
+
|
|
1162
|
+
`);
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
} else if (warnings > 0) {
|
|
1165
|
+
process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
|
|
1166
|
+
|
|
1167
|
+
`);
|
|
1168
|
+
} else {
|
|
1169
|
+
process.stdout.write(" All checks passed!\n\n");
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/skill-content.ts
|
|
1175
|
+
var SKILL_CONTENT = '# Scope \u2014 Agent Skill\n\n## TLDR\nScope is a React codebase introspection toolkit. Use it to answer questions about component structure, props, context dependencies, side effects, and visual output \u2014 without running the app.\n\n**When to reach for it:** Any task requiring "which components use X", "what props does Y accept", "render Z for visual verification", "does this component depend on a provider", or "what design tokens are in use".\n\n**3-command workflow:**\n```\nscope init # scaffold config + auto-generate manifest\nscope manifest query --context ThemeContext # ask questions about the codebase\nscope render Button # produce a PNG of a component\n```\n\n---\n\n---\n\n## Mental Model\n\nUnderstanding how Scope\'s data flows is the key to using it effectively as an agent.\n\n```\nSource TypeScript files\n \u2193 (ts-morph AST parse)\n manifest.json \u2190 structural facts: props, hooks, contexts, complexity\n \u2193 (esbuild + Playwright)\n renders/*.json \u2190 visual facts: screenshot, computedStyles, dom, a11y\n \u2193 (token engine)\n compliance-styles.json \u2190 audit facts: which CSS values match tokens, which don\'t\n \u2193 (site generator)\n site/ \u2190 human-readable docs combining all of the above\n```\n\nEach layer depends on the previous. If you\'re getting unexpected results, check whether the earlier layers are stale (run `scope doctor` to diagnose).\n\n---\n\n## The Four Subsystems\n\n### 1. Manifest (`scope manifest *`)\nThe manifest is a static analysis snapshot of your TypeScript source. It tells you:\n- What components exist, where they live, and how they\'re exported\n- What props each component accepts (types, defaults, required/optional)\n- What React hooks they call (`detectedHooks`)\n- What contexts they consume (`requiredContexts`) \u2014 must be provided for a render to succeed\n- Whether they compose other components (`composes` / `composedBy`)\n- Their **complexity class** \u2014 `"simple"` or `"complex"` \u2014 which determines the render engine\n\nThe manifest never runs your code. It only reads TypeScript. This means it\'s fast and safe, but it can\'t know about runtime values.\n\n### 2. Render Engine (`scope render *`)\nThe render engine compiles components with esbuild and renders them in Chromium (Playwright). Two paths exist:\n\n| Path | When | Speed | Capability |\n|------|------|-------|------------|\n| **Satori** | `complexityClass: "simple"` | ~8ms | Flexbox only, no JS, no CSS-in-JS |\n| **BrowserPool** | `complexityClass: "complex"` | ~200\u2013800ms | Full DOM, CSS, Tailwind, animations |\n\nMost real-world components route through BrowserPool. Scope defaults to `"complex"` when uncertain (safe fallback).\n\nEach render produces:\n- `screenshot` \u2014 retina-quality PNG (2\xD7 `deviceScaleFactor`; display at CSS px dimensions)\n- `width` / `height` \u2014 CSS pixel dimensions of the component root\n- `computedStyles` \u2014 per-node computed CSS keyed by `#node-0`, `#node-1`, etc.\n- `dom` \u2014 full DOM tree with bounding boxes (BrowserPool only)\n- `accessibility` \u2014 role, aria-name, violation list (BrowserPool only)\n- `renderTimeMs` \u2014 wall-clock render duration\n\n### 3. Scope Files (`.scope.tsx`)\nScope files let you define **named rendering scenarios** for a component alongside it in the source tree. They are the primary way to ensure `render all` produces meaningful screenshots.\n\n```tsx\n// Button.scope.tsx\nimport type { ScopeFile } from \'@agent-scope/cli\';\nimport { Button } from \'./Button\';\n\nexport default {\n default: { variant: \'primary\', children: \'Click me\' },\n ghost: { variant: \'ghost\', children: \'Cancel\' },\n danger: { variant: \'danger\', children: \'Delete\' },\n disabled: { variant: \'primary\', children: \'Disabled\', disabled: true },\n} satisfies ScopeFile<typeof Button>;\n```\n\nKey rules:\n- The file must be named `<ComponentName>.scope.tsx` in the same directory\n- Export a default object where keys are scenario names and values are props\n- `render all` uses the `default` scenario (or first defined) as the primary screenshot\n- If 2+ scenarios exist, `render all` automatically runs a matrix and merges cells into the component JSON\n- Scenarios also feed the interactive Playground in the docs site\n\nWhen a component renders blank with `{}` props, **the fix is usually to create a `.scope.tsx` file** with real props.\n\n### 4. Token Compliance\nThe compliance pipeline:\n1. `scope render all` captures `computedStyles` for every element in every component\n2. These are written to `.reactscope/compliance-styles.json`\n3. The token engine compares each computed CSS value against your `reactscope.tokens.json`\n4. `scope tokens compliance` reports the aggregate on-system percentage\n5. `scope ci` fails if the percentage is below `complianceThreshold` (default 90%)\n\n**On-system** means the value exactly matches a resolved token value. Off-system means it\'s a hardcoded value with no token backing it.\n\n---\n\n## Complexity Classes \u2014 Practical Guide\n\nThe `complexityClass` field determines which render engine runs. Scope auto-detects it, but agents should understand it:\n\n**`"simple"` components:**\n- Pure presentational, flexbox layout only\n- No CSS grid, no absolute/fixed/sticky positioning\n- No CSS animations, transitions, or transforms\n- No `className` values Scope can\'t statically trace (e.g. dynamic Tailwind classes)\n- Renders in ~8ms via Satori (SVG-based, no browser needed)\n\n**`"complex"` components:**\n- Anything using Tailwind (CSS injection required)\n- CSS grid, positioned elements, overflow, z-index\n- Components that read from context at render time\n- Any component Scope isn\'t sure about (conservative default)\n- Renders in ~200\u2013800ms via Playwright BrowserPool\n\nWhen in doubt: complex is always safe. Simple is an optimization.\n\n---\n\n## Required Contexts \u2014 Why Renders Fail\n\nIf `requiredContexts` is non-empty, the component calls `useContext` on one or more contexts. Without a provider, it will either render broken or throw entirely.\n\nTwo ways to fix:\n1. **Provider presets in config** (recommended): add provider names to `reactscope.config.json \u2192 components.wrappers.providers`\n2. **Scope file with wrapper**: wrap the component in a provider in the scenario itself\n\nBuilt-in mocks (always provided): `ThemeContext \u2192 { theme: \'light\' }`, `LocaleContext \u2192 { locale: \'en-US\' }`.\n\n---\n\n## `scope doctor` \u2014 Always Run This First\n\nBefore debugging any render issue, run:\n```bash\nscope doctor\n```\n\nIt checks:\n- `reactscope.config.json` is valid JSON\n- Token file exists and has a `version` field\n- Every path in `globalCSS` resolves on disk\n- Manifest is present and up to date (not stale relative to source)\n\n**If `globalCSS` is empty or missing**: Tailwind styles won\'t apply to renders. Every component will look unstyled. This is the most common footgun. Fix: add your CSS entry file (the one with `@tailwind base; @tailwind components; @tailwind utilities;`) to `globalCSS` in config.\n\n---\n\n## Agent Decision Tree\n\n**"I want to know what props Component X accepts"**\n\u2192 `scope manifest get X --format json | jq \'.props\'`\n\n**"I want to know which components will break if I change a context"**\n\u2192 `scope manifest query --context MyContext --format json`\n\n**"I want to render a component to verify visual output"**\n\u2192 Create a `.scope.tsx` file with real props first, then `scope render X`\n\n**"I want to render all variants of a component"**\n\u2192 Define all variants in `.scope.tsx`, then `scope render all` (auto-matrix)\n\u2192 Or: `scope render matrix X --axes \'variant:primary,secondary,danger\'`\n\n**"I want to audit token compliance"**\n\u2192 `scope render all` first (populates computedStyles), then `scope tokens compliance`\n\n**"Renders look unstyled / blank"**\n\u2192 Run `scope doctor` \u2014 likely missing `globalCSS`\n\u2192 If props are the issue: create/update the `.scope.tsx` file\n\n**"I want to understand blast radius of a token change"**\n\u2192 `scope tokens impact color.primary.500 --new-value \'#0077dd\'`\n\u2192 `scope tokens preview color.primary.500 --new-value \'#0077dd\'` for visual diff\n\n**"I need to set up Scope in a new project"**\n\u2192 `scope init --yes` (auto-detects Tailwind + CSS, generates manifest automatically)\n\u2192 `scope doctor` to validate\n\u2192 Create `.scope.tsx` files for key components\n\u2192 `scope render all`\n\n**"I want to run Scope in CI"**\n\u2192 `scope ci --json --output ci-result.json`\n\u2192 Exit code 0 = pass, non-zero = specific failure type\n\n---\n\n\n## Installation\n\n```bash\nnpm install -g @agent-scope/cli # global\nnpm install --save-dev @agent-scope/cli # per-project\n```\n\nBinary: `scope`\n\n---\n\n## Core Workflow\n\n```\ninit \u2192 manifest generate \u2192 manifest query/get/list \u2192 render \u2192 (token audit) \u2192 ci\n```\n\n- **init**: Scaffold `reactscope.config.json` + token stub, auto-detect framework/globalCSS, **immediately runs `manifest generate`** so you see results right away.\n- **doctor**: Health-check command \u2014 validates config, token file, globalCSS presence, and manifest staleness.\n- **generate**: Parse TypeScript AST and emit `.reactscope/manifest.json`. Run once per codebase change (or automatically via `scope init`).\n- **query / get / list**: Ask structural questions. No network required. Works from manifest alone. Supports filtering by `--collection` and `--internal`.\n- **render**: Produce PNGs of components via esbuild + Playwright (BrowserPool). Requires manifest for file paths. Auto-injects required prop defaults and globalCSS.\n- **token audit**: Validate design tokens via `@scope/tokens` CLI commands (`tokens list`, `tokens compliance`, `tokens impact`, `tokens preview`, `tokens export`).\n- **ci**: Run compliance checks and exit with code 0/1 for CI pipelines. `report pr-comment` posts results to GitHub PRs.\n\n---\n\n## Full CLI Reference\n\n### `scope init`\nScaffold config, detect framework, extract Tailwind tokens, detect globalCSS files, and **automatically run `scope manifest generate`**.\n\n```bash\nscope init\nscope init --force # overwrite existing config\n```\n\nAfter init completes, the manifest is already written \u2014 no manual `scope manifest generate` step needed.\n\n**Tailwind token extraction**: reads `tailwind.config.js`, extracts colors (with nested scale support), spacing, fontFamily, borderRadius. Stored in `reactscope.tokens.json`.\n\n**globalCSS detection**: checks 9 common patterns (`src/styles.css`, `src/index.css`, `app/globals.css`, etc.). Stored in `components.wrappers.globalCSS` in config.\n\n---\n\n### `scope doctor`\nValidate the Scope setup. Exits non-zero on errors, zero on warnings-only.\n\n```bash\nscope doctor\nscope doctor --json\n```\n\nChecks:\n- `config` \u2014 `reactscope.config.json` is valid JSON with required fields\n- `tokens` \u2014 token file is present and has a valid `version` field\n- `globalCSS` \u2014 globalCSS files listed in config exist on disk\n- `manifest` \u2014 manifest exists and is not stale (compares source file mtimes)\n\n```\n$ scope doctor\nScope Doctor\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n [\u2713] config reactscope.config.json valid\n [\u2713] tokens Token file valid\n [\u2713] globalCSS 1 globalCSS file(s) present\n [!] manifest Manifest may be stale \u2014 5 source file(s) modified since last generate\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n 1 warning(s) \u2014 everything works but could be better\n```\n\n---\n\n### `scope capture <url>`\nCapture a live React component tree from a running app URL.\n\n```bash\nscope capture http://localhost:3000\nscope capture http://localhost:3000 --output report.json --pretty\nscope capture http://localhost:3000 --timeout 15000 --wait 2000\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `-o, --output <path>` | string | stdout | Write JSON to file |\n| `--pretty` | bool | false | Pretty-print JSON |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\nOutput (stdout): serialized PageReport JSON (or path when `--output` is set)\n\n---\n\n### `scope tree <url>`\nPrint the React component tree from a live URL.\n\n```bash\nscope tree http://localhost:3000\nscope tree http://localhost:3000 --depth 3 --show-props --show-hooks\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--depth <n>` | number | unlimited | Max depth to display |\n| `--show-props` | bool | false | Include prop names next to components |\n| `--show-hooks` | bool | false | Show hook counts per component |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report <url>`\nCapture and print a human-readable summary of a React app.\n\n```bash\nscope report http://localhost:3000\nscope report http://localhost:3000 --json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--json` | bool | false | Emit structured JSON instead of text |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report baseline`\nSave a baseline snapshot for future diff comparisons.\n\n```bash\nscope report baseline\nscope report baseline --output baselines/my-baseline.json\n```\n\n---\n\n### `scope report diff`\nDiff the current app state against a saved baseline.\n\n```bash\nscope report diff\nscope report diff --baseline baselines/my-baseline.json\nscope report diff --json\n```\n\n---\n\n### `scope report pr-comment`\nPost a Scope CI report as a GitHub PR comment. Used in CI pipelines via the reusable `scope-ci` workflow.\n\n```bash\nscope report pr-comment --report-path scope-ci-report.json\n```\n\nRequires `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_PR_NUMBER` in environment.\n\n---\n\n### `scope manifest generate`\nScan source files and write `.reactscope/manifest.json`.\n\n```bash\nscope manifest generate\nscope manifest generate --root ./packages/ui\nscope manifest generate --include "src/**/*.tsx" --exclude "**/*.test.tsx"\nscope manifest generate --output custom/manifest.json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--root <path>` | string | cwd | Project root directory |\n| `--output <path>` | string | `.reactscope/manifest.json` | Output path |\n| `--include <globs>` | string | `src/**/*.tsx,src/**/*.ts` | Comma-separated include globs |\n| `--exclude <globs>` | string | `**/node_modules/**,...` | Comma-separated exclude globs |\n\n---\n\n### `scope manifest list`\nList all components in the manifest.\n\n```bash\nscope manifest list\nscope manifest list --filter "Button*"\nscope manifest list --format json\nscope manifest list --collection Forms # filter to named collection\nscope manifest list --internal # only internal components\nscope manifest list --no-internal # hide internal components\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--format <fmt>` | `json\\|table` | auto (TTY\u2192table, pipe\u2192json) | Output format |\n| `--filter <glob>` | string | \u2014 | Filter component names by glob |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--internal` | bool | false | Show only internal components |\n| `--no-internal` | bool | false | Hide internal components |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\nTTY table output (includes COLLECTION and INTERNAL columns):\n```\nNAME FILE COMPLEXITY HOOKS CONTEXTS COLLECTION INTERNAL\n------------ --------------------------- ---------- ----- -------- ---------- --------\nButton src/components/Button.tsx simple 1 0 \u2014 no\nThemeToggle src/components/Toggle.tsx complex 3 1 Forms no\n```\n\n---\n\n### `scope manifest get <name>`\nGet full details of a single component.\n\n```bash\nscope manifest get Button\nscope manifest get Button --format json\n```\n\nJSON output includes `collection` and `internal` fields:\n```json\n{\n "name": "Button",\n "filePath": "src/components/Button.tsx",\n "collection": "Primitives",\n "internal": false,\n ...\n}\n```\n\n---\n\n### `scope manifest query`\nQuery components by attributes.\n\n```bash\nscope manifest query --context ThemeContext\nscope manifest query --hook useEffect\nscope manifest query --complexity complex\nscope manifest query --side-effects\nscope manifest query --has-fetch\nscope manifest query --has-prop <propName>\nscope manifest query --composed-by <ComponentName>\nscope manifest query --internal\nscope manifest query --collection Forms\nscope manifest query --context ThemeContext --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--context <name>` | string | \u2014 | Find components consuming a context by name |\n| `--hook <name>` | string | \u2014 | Find components using a specific hook |\n| `--complexity <class>` | `simple\\|complex` | \u2014 | Filter by complexity class |\n| `--side-effects` | bool | false | Any side effects detected |\n| `--has-fetch` | bool | false | Components with fetch calls specifically |\n| `--has-prop <name>` | string | \u2014 | Components that accept a specific prop |\n| `--composed-by <name>` | string | \u2014 | Components rendered inside a specific parent |\n| `--internal` | bool | false | Only internal components |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--format <fmt>` | `json\\|table` | auto | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render <component>`\nRender a single component to PNG (TTY) or JSON (pipe).\n\n**Auto prop defaults**: if `--props` is omitted, Scope injects sensible defaults so required props don\'t produce blank renders: strings/nodes \u2192 component name, unions \u2192 first value, booleans \u2192 `false`, numbers \u2192 `0`.\n\n**globalCSS auto-injection**: reads `components.wrappers.globalCSS` from config and compiles/injects CSS (supports Tailwind v3 via PostCSS) into the render harness. A warning is printed to stderr if no globalCSS is configured (common cause of unstyled renders).\n\n```bash\nscope render Button\nscope render Button --props \'{"variant":"primary","children":"Click me"}\'\nscope render Button --viewport 375x812\nscope render Button --output button.png\nscope render Button --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--props <json>` | string | `{}` | Inline props as JSON string |\n| `--viewport <WxH>` | string | `375x812` | Viewport size |\n| `--theme <name>` | string | \u2014 | Theme name from token system |\n| `-o, --output <path>` | string | \u2014 | Write PNG to specific path |\n| `--format <fmt>` | `png\\|json` | auto (TTY\u2192file, pipe\u2192json) | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render matrix <component>`\nRender across a Cartesian product of prop axes. Accepts both `key:v1,v2` and `{"key":["v1","v2"]}` JSON format for `--axes`.\n\n```bash\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\nscope render matrix Button --axes \'variant:primary,secondary size:sm,md,lg\'\nscope render matrix Button --sprite button-matrix.png --format json\n```\n\n---\n\n### `scope render all`\nRender every component in the manifest.\n\n```bash\nscope render all\nscope render all --concurrency 4 --output-dir renders/\n```\n\nHandles imports of CSS files in components (maps to empty loader so styles are injected at page level). SVG and font imports are handled via dataurl loaders.\n\n---\n\n### `scope instrument tree`\nCapture the live React component tree with instrumentation metadata.\n\n```bash\nscope instrument tree http://localhost:3000\nscope instrument tree http://localhost:3000 --depth 5 --show-props\n```\n\n**Implementation note**: uses a fresh `chromium.launch()` + `newContext()` + `newPage()` per call (not BrowserPool), with `addInitScript` called before `setContent` to ensure the Scope runtime is injected at document-start before React loads.\n\n---\n\n### `scope instrument hooks`\nProfile hook execution in live components.\n\n```bash\nscope instrument hooks http://localhost:3000\nscope instrument hooks http://localhost:3000 --component Button\n```\n\n**Implementation note**: requires `addInitScript({ content: getBrowserEntryScript() })` before `setContent` so `__REACT_DEVTOOLS_GLOBAL_HOOK__` is present when React loads its renderer.\n\n---\n\n### `scope instrument profile`\nProfile render performance of live components.\n\n```bash\nscope instrument profile http://localhost:3000\n```\n\n---\n\n### `scope instrument renders`\nRe-render causality analysis \u2014 what triggered each render.\n\n```bash\nscope instrument renders http://localhost:3000\n```\n\n---\n\n### `scope tokens get <name>`\nGet details of a single design token.\n\n### `scope tokens list`\nList all tokens. Token file must have a `version` field (written by `scope init`).\n\n```bash\nscope tokens list\nscope tokens list --type color\nscope tokens list --format json\n```\n\n### `scope tokens search <query>`\nFull-text search across token names/values.\n\n### `scope tokens resolve <value>`\nResolve a CSS value or alias back to its token name.\n\n### `scope tokens validate`\nValidate token file schema.\n\n### `scope tokens compliance`\nCheck rendered components for design token compliance.\n\n```bash\nscope tokens compliance\nscope tokens compliance --threshold 95\n```\n\n### `scope tokens impact <token>`\nAnalyze impact of changing a token \u2014 which components use it.\n\n```bash\nscope tokens impact --token color.primary.500\n```\n\n### `scope tokens preview <token>`\nPreview a token value change visually before committing.\n\n### `scope tokens export`\nExport tokens in multiple formats.\n\n```bash\nscope tokens export --format flat-json\nscope tokens export --format css\nscope tokens export --format scss\nscope tokens export --format ts\nscope tokens export --format tailwind\nscope tokens export --format style-dictionary\n```\n\n**Format aliases** (auto-corrected with "Did you mean?" hint):\n- `json` \u2192 `flat-json`\n- `js` \u2192 `ts`\n- `sass` \u2192 `scss`\n- `tw` \u2192 `tailwind`\n\n---\n\n### `scope ci`\nRun all CI checks (compliance, accessibility, console errors) and exit 0/1.\n\n```bash\nscope ci\nscope ci --json\nscope ci --threshold 90 # compliance threshold (default: 90)\n```\n\n```\n$ scope ci --json\n\u2192 CI passed in 3.2s\n\u2192 Compliance 100.0% >= threshold 90.0% \u2705\n\u2192 Accessibility audit not yet implemented \u2014 skipped \u2705\n\u2192 No console errors detected \u2705\n\u2192 Exit code 0\n```\n\nThe `scope-ci` **reusable GitHub Actions workflow** is available at `.github/workflows/scope-ci.yml` and can be included in any repo\'s CI to run `scope ci` and post results as a PR comment via `scope report pr-comment`.\n\n---\n\n### `scope site build`\nGenerate a static HTML component gallery site from the manifest.\n\n```bash\nscope site build\nscope site build --output ./dist/site\n```\n\n**Collections support**: components are grouped under named collection sections in the sidebar and index grid. Internal components are hidden from the sidebar and card grid but appear in composition detail sections with an `internal` badge.\n\n**Collection display rules**:\n- Sidebar: one section divider per collection + an "Ungrouped" section; internal components excluded\n- Index page: named sections with heading + optional description; internal components excluded\n- Component detail page: Composes/Composed By lists ALL components including internal ones (with subtle badge)\n- Falls back to flat list when no collections configured (backwards-compatible)\n\n### `scope site serve`\nServe the generated site locally.\n\n```bash\nscope site serve\nscope site serve --port 4000\n```\n\n---\n\n## Collections & Internal Components\n\nComponents can be organized into named **collections** and flagged as **internal** (library implementation details not shown in the public gallery).\n\n### Defining collections\n\n**1. TSDoc tag** (highest precedence):\n```tsx\n/**\n * @collection Forms\n */\nexport function Input() { ... }\n```\n\n**2. `.scope.ts` co-located file**:\n```ts\n// Input.scope.ts\nexport const collection = "Forms"\n```\n\n**3. Config-level glob patterns**:\n```json\n// reactscope.config.json\n{\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ]\n}\n```\n\nResolution precedence: TSDoc `@collection` > `.scope.ts` export > config pattern.\n\n### Flagging internal components\n\n**TSDoc tag**:\n```tsx\n/**\n * @internal\n */\nexport function InternalHelperButton() { ... }\n```\n\n**Config glob patterns**:\n```json\n{\n "internalPatterns": ["src/internal/**", "src/**/*Internal*"]\n}\n```\n\n---\n\n## Manifest Output Schema\n\nFile: `.reactscope/manifest.json`\n\n```typescript\n{\n version: "0.1",\n generatedAt: string, // ISO 8601\n collections: CollectionConfig[], // echoes config.collections, [] when not set\n components: Record<string, ComponentDescriptor>,\n tree: Record<string, { children: string[], parents: string[] }>\n}\n```\n\n### `ComponentDescriptor` fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `filePath` | `string` | Relative path from project root to source file |\n| `exportType` | `"named" \\| "default" \\| "none"` | How the component is exported |\n| `displayName` | `string` | `displayName` if set, else function/class name |\n| `collection` | `string?` | Resolved collection name (`undefined` = ungrouped) |\n| `internal` | `boolean` | `true` if flagged as internal (default: `false`) |\n| `props` | `Record<string, PropDescriptor>` | Extracted prop types keyed by prop name |\n| `composes` | `string[]` | Components this one renders in its JSX |\n| `composedBy` | `string[]` | Components that render this one in their JSX |\n| `complexityClass` | `"simple" \\| "complex"` | Render path: simple = Satori-safe, complex = requires BrowserPool |\n| `requiredContexts` | `string[]` | React context names consumed |\n| `detectedHooks` | `string[]` | All hooks called, sorted alphabetically |\n| `sideEffects` | `SideEffects` | Side effect categories detected |\n| `memoized` | `boolean` | Wrapped with `React.memo` |\n| `forwardedRef` | `boolean` | Wrapped with `React.forwardRef` |\n| `hocWrappers` | `string[]` | HOC wrapper names (excluding memo/forwardRef) |\n| `loc` | `{ start: number, end: number }` | Line numbers in source file |\n\n---\n\n## Common Agent Workflows\n\n### Structural queries\n\n```bash\n# Which components use ThemeContext?\nscope manifest query --context ThemeContext\n\n# What props does Button accept?\nscope manifest get Button --format json | jq \'.props\'\n\n# Which components are safe to render without a provider?\nscope manifest query --complexity simple # + check requiredContexts === []\n\n# Show all components with side effects\nscope manifest query --side-effects\n\n# Which components make fetch calls?\nscope manifest query --has-fetch\n\n# Which components use useEffect?\nscope manifest query --hook useEffect\n\n# Which components accept a disabled prop?\nscope manifest query --has-prop disabled\n\n# Which components are composed inside Modal?\nscope manifest query --composed-by Modal\n\n# All components in the Forms collection\nscope manifest list --collection Forms\n\n# All internal components (library implementation details)\nscope manifest list --internal\n\n# Public components only (hide internals)\nscope manifest list --no-internal\n```\n\n### Render workflows\n\n```bash\n# Render Button in all variants (auto-defaults props if not provided)\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\n\n# Render with JSON axes format\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\n\n# Render with explicit props\nscope render Button --props \'{"variant":"primary","disabled":true}\'\n\n# Render all components (handles CSS/SVG/font imports automatically)\nscope render all --concurrency 8\n\n# Get render as JSON\nscope render Button --format json | jq \'.screenshot\' | base64 -d > button.png\n```\n\n### Token workflows\n\n```bash\n# List all tokens\nscope tokens list\n\n# Check compliance\nscope tokens compliance --threshold 95\n\n# See what a token change impacts\nscope tokens impact --token color.primary.500\n\n# Export for Tailwind\nscope tokens export --format tailwind\n```\n\n### CI workflow\n\n```bash\n# Full compliance check\nscope ci --json\n\n# In GitHub Actions \u2014 use the reusable workflow\n# .github/workflows/ci.yml:\n# uses: FlatFilers/Scope/.github/workflows/scope-ci.yml@main\n```\n\n---\n\n## Error Patterns\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| `"React root not found"` | App not running, wrong URL, or Vite HMR interfering | Use `scope capture --wait 2000` |\n| `"Component not in manifest"` | Manifest is stale | Run `scope manifest generate` first |\n| `"Manifest not found"` | Missing manifest | Run `scope init` or `scope manifest generate` |\n| `"requiredContexts missing"` | Component needs a provider | Add provider presets to `reactscope.config.json` |\n| Blank PNG / 16\xD76px renders | No globalCSS injected (common with Tailwind) | Set `components.wrappers.globalCSS` in config; run `scope doctor` to verify |\n| `"Invalid props JSON"` | Malformed JSON in `--props` | Use single outer quotes: `--props \'{"key":"val"}\'` |\n| `"SCOPE_CAPTURE_JSON not available"` | Scope runtime not injected before React loaded | Fixed in PR #83 \u2014 update CLI |\n| `"No React DevTools hook found"` | Hook instrumentation init order bug | Fixed in PR #83 \u2014 update CLI |\n| `"ERR_MODULE_NOT_FOUND"` after tokens commands | Old Node shebang in CLI binary | Fixed in PR #90 \u2014 CLI now uses `#!/usr/bin/env bun` |\n| `"version" field missing in tokens` | Token stub written by old `scope init` | Re-run `scope init --force` or add `"version": "1"` to token file |\n| `"unknown option --has-prop"` | Old CLI version | Fixed in PR #90 \u2014 update CLI |\n| Format alias error (`json`, `js`, `sass`, `tw`) | Wrong format name for `tokens export` | Use `flat-json`, `ts`, `scss`, `tailwind`; CLI shows "Did you mean?" hint |\n\n---\n\n## `reactscope.config.json`\n\n```json\n{\n "components": {\n "wrappers": {\n "globalCSS": ["src/styles.css"]\n }\n },\n "tokens": {\n "file": "reactscope.tokens.json"\n },\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ],\n "internalPatterns": ["src/internal/**"],\n "providers": {\n "theme": { "component": "ThemeProvider", "props": { "theme": "light" } },\n "router": { "component": "MemoryRouter", "props": { "initialEntries": ["/"] } }\n }\n}\n```\n\n**Built-in mock providers** (always available, no config needed):\n- `ThemeContext` \u2192 `{ theme: \'light\' }` (or `--theme <name>`)\n- `LocaleContext` \u2192 `{ locale: \'en-US\' }`\n\n---\n\n## What Scope Cannot Do\n\n- **Runtime state**: `useState` values after user interaction\n- **Network requests**: `fetch`, `XHR`, `WebSocket`\n- **User interactions**: click, type, hover, drag\n- **Auth/session-gated components**: components that redirect or throw without a session\n- **Server components (RSC)**: React Server Components\n- **Dynamic CSS**: CSS-in-JS styles computed at runtime from props Scope can\'t infer\n\n---\n\n## Version History\n\n| Version | Date | Summary |\n|---------|------|---------|\n| v1.0 | 2026-03-11 | Initial SKILL.md (PR #36) \u2014 manifest, render, capture, tree, report, tokens, ci commands |\n| v1.1 | 2026-03-11 | Updated through PR #82 \u2014 Phase 2 CLI commands complete |\n| v1.2 | 2026-03-13 | PRs #83\u2013#95: runtime injection fix, dogfooding fixes (12 bugs), `scope doctor`, `scope init` auto-manifest, globalCSS render warning, collections & internal components feature |\n';
|
|
1176
|
+
|
|
1177
|
+
// src/get-skill-command.ts
|
|
1178
|
+
function createGetSkillCommand() {
|
|
1179
|
+
return new Command("get-skill").description(
|
|
1180
|
+
'Print the embedded Scope SKILL.md to stdout.\n\nAgents: pipe this command into your context loader to bootstrap Scope knowledge.\nThe skill covers: when to use each command, config requirements, output format,\nrender engine selection, and common failure modes.\n\nEMBEDDED AT BUILD TIME \u2014 works in any install context (global npm, npx, local).\n\nExamples:\n scope get-skill # raw markdown to stdout\n scope get-skill --json # { "skill": "..." } for structured ingestion\n scope get-skill | head -50 # preview the skill\n scope get-skill > /tmp/SKILL.md # save locally'
|
|
1181
|
+
).option("--json", "Wrap output in JSON { skill: string } instead of raw markdown").action((opts) => {
|
|
1182
|
+
if (opts.json) {
|
|
1183
|
+
process.stdout.write(`${JSON.stringify({ skill: SKILL_CONTENT }, null, 2)}
|
|
1184
|
+
`);
|
|
1185
|
+
} else {
|
|
1186
|
+
process.stdout.write(SKILL_CONTENT);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
992
1190
|
function hasConfigFile(dir, stem) {
|
|
993
1191
|
if (!existsSync(dir)) return false;
|
|
994
1192
|
try {
|
|
@@ -1199,9 +1397,9 @@ function createRL() {
|
|
|
1199
1397
|
});
|
|
1200
1398
|
}
|
|
1201
1399
|
async function ask(rl, question) {
|
|
1202
|
-
return new Promise((
|
|
1400
|
+
return new Promise((resolve19) => {
|
|
1203
1401
|
rl.question(question, (answer) => {
|
|
1204
|
-
|
|
1402
|
+
resolve19(answer.trim());
|
|
1205
1403
|
});
|
|
1206
1404
|
});
|
|
1207
1405
|
}
|
|
@@ -1276,7 +1474,7 @@ function extractTailwindTokens(tokenSources) {
|
|
|
1276
1474
|
}
|
|
1277
1475
|
}
|
|
1278
1476
|
if (Object.keys(colorTokens).length > 0) {
|
|
1279
|
-
tokens
|
|
1477
|
+
tokens.color = colorTokens;
|
|
1280
1478
|
}
|
|
1281
1479
|
}
|
|
1282
1480
|
}
|
|
@@ -1289,7 +1487,7 @@ function extractTailwindTokens(tokenSources) {
|
|
|
1289
1487
|
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1290
1488
|
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1291
1489
|
}
|
|
1292
|
-
tokens
|
|
1490
|
+
tokens.spacing = spacingTokens;
|
|
1293
1491
|
}
|
|
1294
1492
|
}
|
|
1295
1493
|
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
@@ -1302,7 +1500,7 @@ function extractTailwindTokens(tokenSources) {
|
|
|
1302
1500
|
}
|
|
1303
1501
|
}
|
|
1304
1502
|
if (Object.keys(fontTokens).length > 0) {
|
|
1305
|
-
tokens
|
|
1503
|
+
tokens.font = fontTokens;
|
|
1306
1504
|
}
|
|
1307
1505
|
}
|
|
1308
1506
|
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
@@ -1313,7 +1511,7 @@ function extractTailwindTokens(tokenSources) {
|
|
|
1313
1511
|
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1314
1512
|
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1315
1513
|
}
|
|
1316
|
-
tokens
|
|
1514
|
+
tokens.radius = radiusTokens;
|
|
1317
1515
|
}
|
|
1318
1516
|
}
|
|
1319
1517
|
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
@@ -1432,7 +1630,28 @@ async function runInit(options) {
|
|
|
1432
1630
|
process.stdout.write(` ${p}
|
|
1433
1631
|
`);
|
|
1434
1632
|
}
|
|
1435
|
-
process.stdout.write("\n
|
|
1633
|
+
process.stdout.write("\n Scanning components...\n");
|
|
1634
|
+
try {
|
|
1635
|
+
const manifestConfig = {
|
|
1636
|
+
include: config.components.include,
|
|
1637
|
+
rootDir
|
|
1638
|
+
};
|
|
1639
|
+
const manifest = await generateManifest(manifestConfig);
|
|
1640
|
+
const manifestCount = Object.keys(manifest.components).length;
|
|
1641
|
+
const manifestOutPath = join(rootDir, config.output.dir, "manifest.json");
|
|
1642
|
+
mkdirSync(join(rootDir, config.output.dir), { recursive: true });
|
|
1643
|
+
writeFileSync(manifestOutPath, `${JSON.stringify(manifest, null, 2)}
|
|
1644
|
+
`);
|
|
1645
|
+
process.stdout.write(
|
|
1646
|
+
` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
|
|
1647
|
+
`
|
|
1648
|
+
);
|
|
1649
|
+
} catch {
|
|
1650
|
+
process.stdout.write(
|
|
1651
|
+
" (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
process.stdout.write("\n");
|
|
1436
1655
|
return {
|
|
1437
1656
|
success: true,
|
|
1438
1657
|
message: "Project initialised successfully.",
|
|
@@ -1441,7 +1660,9 @@ async function runInit(options) {
|
|
|
1441
1660
|
};
|
|
1442
1661
|
}
|
|
1443
1662
|
function createInitCommand() {
|
|
1444
|
-
return new Command("init").description(
|
|
1663
|
+
return new Command("init").description(
|
|
1664
|
+
"Auto-detect your project layout and scaffold reactscope.config.json.\n\nWHAT IT DOES:\n - Detects component glob patterns from tsconfig / package.json / directory scan\n - Detects globalCSS files (Tailwind, PostCSS)\n - Writes reactscope.config.json with all detected values\n - Adds .reactscope/ to .gitignore\n - Creates .reactscope/ output directory\n\nCONFIG FIELDS GENERATED:\n components.include glob patterns for component discovery\n components.wrappers providers + globalCSS to inject on every render\n render.viewport default viewport (1280\xD7800)\n tokens.file path to reactscope.tokens.json\n output.dir .reactscope/ (all outputs go here)\n ci.complianceThreshold 0.90 (90% on-token required to pass CI)\n\nSafe to re-run \u2014 will not overwrite existing config unless --force.\n\nExamples:\n scope init\n scope init --yes # accept all detected defaults, no prompts\n scope init --force # overwrite existing config"
|
|
1665
|
+
).option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
1445
1666
|
try {
|
|
1446
1667
|
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1447
1668
|
if (!result.success && !result.skipped) {
|
|
@@ -1470,34 +1691,56 @@ function resolveFormat(formatFlag) {
|
|
|
1470
1691
|
return isTTY() ? "table" : "json";
|
|
1471
1692
|
}
|
|
1472
1693
|
function registerList(manifestCmd) {
|
|
1473
|
-
manifestCmd.command("list").description(
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1694
|
+
manifestCmd.command("list").description(
|
|
1695
|
+
`List all components in the manifest as a table (TTY) or JSON (piped).
|
|
1696
|
+
|
|
1697
|
+
Examples:
|
|
1698
|
+
scope manifest list
|
|
1699
|
+
scope manifest list --format json | jq '.[].name'
|
|
1700
|
+
scope manifest list --filter "Button*"`
|
|
1701
|
+
).option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--filter <glob>", "Filter components by name glob pattern").option("--collection <name>", "Filter to only components in the named collection").option("--internal", "Show only internal components").option("--no-internal", "Hide internal components from output").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
|
|
1702
|
+
(opts) => {
|
|
1703
|
+
try {
|
|
1704
|
+
const manifest = loadManifest(opts.manifest);
|
|
1705
|
+
const format = resolveFormat(opts.format);
|
|
1706
|
+
let entries = Object.entries(manifest.components);
|
|
1707
|
+
if (opts.filter !== void 0) {
|
|
1708
|
+
const filterPattern = opts.filter ?? "";
|
|
1709
|
+
entries = entries.filter(([name]) => matchGlob(filterPattern, name));
|
|
1710
|
+
}
|
|
1711
|
+
if (opts.collection !== void 0) {
|
|
1712
|
+
const col = opts.collection;
|
|
1713
|
+
entries = entries.filter(([, d]) => d.collection === col);
|
|
1714
|
+
}
|
|
1715
|
+
if (opts.internal === true) {
|
|
1716
|
+
entries = entries.filter(([, d]) => d.internal);
|
|
1717
|
+
} else if (opts.internal === false) {
|
|
1718
|
+
entries = entries.filter(([, d]) => !d.internal);
|
|
1719
|
+
}
|
|
1720
|
+
const rows = entries.map(([name, descriptor]) => ({
|
|
1721
|
+
name,
|
|
1722
|
+
file: descriptor.filePath,
|
|
1723
|
+
complexityClass: descriptor.complexityClass,
|
|
1724
|
+
hookCount: descriptor.detectedHooks.length,
|
|
1725
|
+
contextCount: descriptor.requiredContexts.length,
|
|
1726
|
+
collection: descriptor.collection,
|
|
1727
|
+
internal: descriptor.internal
|
|
1728
|
+
}));
|
|
1729
|
+
const output = format === "json" ? formatListJson(rows) : formatListTable(rows);
|
|
1730
|
+
process.stdout.write(`${output}
|
|
1491
1731
|
`);
|
|
1492
|
-
|
|
1493
|
-
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1494
1734
|
`);
|
|
1495
|
-
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1496
1737
|
}
|
|
1497
|
-
|
|
1738
|
+
);
|
|
1498
1739
|
}
|
|
1499
1740
|
function registerGet(manifestCmd) {
|
|
1500
|
-
manifestCmd.command("get <name>").description(
|
|
1741
|
+
manifestCmd.command("get <name>").description(
|
|
1742
|
+
"Get full details of a single component: props, hooks, complexity class, file path.\n\nExamples:\n scope manifest get Button\n scope manifest get Button --format json\n scope manifest get Button --format json | jq '.complexity'"
|
|
1743
|
+
).option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action((name, opts) => {
|
|
1501
1744
|
try {
|
|
1502
1745
|
const manifest = loadManifest(opts.manifest);
|
|
1503
1746
|
const format = resolveFormat(opts.format);
|
|
@@ -1521,10 +1764,12 @@ Available: ${available}${hint}`
|
|
|
1521
1764
|
});
|
|
1522
1765
|
}
|
|
1523
1766
|
function registerQuery(manifestCmd) {
|
|
1524
|
-
manifestCmd.command("query").description(
|
|
1767
|
+
manifestCmd.command("query").description(
|
|
1768
|
+
'Filter components by structural attributes. All flags are AND-combined.\n\nCOMPLEXITY CLASSES:\n simple \u2014 pure/presentational, no side effects, Satori-renderable\n complex \u2014 uses context/hooks/effects, requires BrowserPool to render\n\nExamples:\n scope manifest query --complexity simple\n scope manifest query --has-fetch\n scope manifest query --hook useContext --side-effects\n scope manifest query --has-prop "variant:union" --format json\n scope manifest query --composed-by Layout'
|
|
1769
|
+
).option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option(
|
|
1525
1770
|
"--has-prop <spec>",
|
|
1526
1771
|
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1527
|
-
).option("--composed-by <name>", "Find components that compose the named component").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
|
|
1772
|
+
).option("--composed-by <name>", "Find components that compose the named component").option("--internal", "Find only internal components", false).option("--collection <name>", "Filter to only components in the named collection").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
|
|
1528
1773
|
(opts) => {
|
|
1529
1774
|
try {
|
|
1530
1775
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1537,9 +1782,11 @@ function registerQuery(manifestCmd) {
|
|
|
1537
1782
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1538
1783
|
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1539
1784
|
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1785
|
+
if (opts.internal) queryParts.push("internal");
|
|
1786
|
+
if (opts.collection !== void 0) queryParts.push(`collection=${opts.collection}`);
|
|
1540
1787
|
if (queryParts.length === 0) {
|
|
1541
1788
|
process.stderr.write(
|
|
1542
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop,
|
|
1789
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, --composed-by, --internal, or --collection.\n"
|
|
1543
1790
|
);
|
|
1544
1791
|
process.exit(1);
|
|
1545
1792
|
}
|
|
@@ -1584,15 +1831,24 @@ function registerQuery(manifestCmd) {
|
|
|
1584
1831
|
const targetName = opts.composedBy;
|
|
1585
1832
|
entries = entries.filter(([, d]) => {
|
|
1586
1833
|
const composedBy = d.composedBy;
|
|
1587
|
-
return composedBy
|
|
1834
|
+
return composedBy?.includes(targetName);
|
|
1588
1835
|
});
|
|
1589
1836
|
}
|
|
1837
|
+
if (opts.internal) {
|
|
1838
|
+
entries = entries.filter(([, d]) => d.internal);
|
|
1839
|
+
}
|
|
1840
|
+
if (opts.collection !== void 0) {
|
|
1841
|
+
const col = opts.collection;
|
|
1842
|
+
entries = entries.filter(([, d]) => d.collection === col);
|
|
1843
|
+
}
|
|
1590
1844
|
const rows = entries.map(([name, d]) => ({
|
|
1591
1845
|
name,
|
|
1592
1846
|
file: d.filePath,
|
|
1593
1847
|
complexityClass: d.complexityClass,
|
|
1594
1848
|
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
1595
|
-
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
1849
|
+
contexts: d.requiredContexts.join(", ") || "\u2014",
|
|
1850
|
+
collection: d.collection,
|
|
1851
|
+
internal: d.internal
|
|
1596
1852
|
}));
|
|
1597
1853
|
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
1598
1854
|
process.stdout.write(`${output}
|
|
@@ -1607,7 +1863,7 @@ function registerQuery(manifestCmd) {
|
|
|
1607
1863
|
}
|
|
1608
1864
|
function registerGenerate(manifestCmd) {
|
|
1609
1865
|
manifestCmd.command("generate").description(
|
|
1610
|
-
|
|
1866
|
+
'Scan source files and generate .reactscope/manifest.json.\n\nUses Babel static analysis \u2014 no runtime or bundler required.\nRe-run whenever components are added, removed, or significantly changed.\n\nWHAT IT CAPTURES per component:\n - File path and export name\n - All props with types and default values\n - Hook usage (useState, useEffect, useContext, custom hooks)\n - Side effects (fetch, timers, subscriptions)\n - Complexity class: simple | complex\n - Context dependencies and composed child components\n\nExamples:\n scope manifest generate\n scope manifest generate --root ./packages/ui\n scope manifest generate --include "src/components/**/*.tsx" --exclude "**/*.stories.tsx"\n scope manifest generate --output ./custom-manifest.json'
|
|
1611
1867
|
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
1612
1868
|
try {
|
|
1613
1869
|
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
@@ -1642,7 +1898,7 @@ function registerGenerate(manifestCmd) {
|
|
|
1642
1898
|
}
|
|
1643
1899
|
function createManifestCommand() {
|
|
1644
1900
|
const manifestCmd = new Command("manifest").description(
|
|
1645
|
-
"Query and explore the component manifest"
|
|
1901
|
+
"Query and explore the component manifest (.reactscope/manifest.json).\n\nThe manifest is the source-of-truth registry of every React component\nin your codebase \u2014 generated by static analysis (no runtime needed).\n\nRun `scope manifest generate` first, then use list/get/query to explore.\n\nExamples:\n scope manifest generate\n scope manifest list\n scope manifest get Button\n scope manifest query --complexity complex --has-fetch"
|
|
1646
1902
|
);
|
|
1647
1903
|
registerList(manifestCmd);
|
|
1648
1904
|
registerGet(manifestCmd);
|
|
@@ -1958,7 +2214,19 @@ async function runHooksProfiling(componentName, filePath, props) {
|
|
|
1958
2214
|
}
|
|
1959
2215
|
function createInstrumentHooksCommand() {
|
|
1960
2216
|
const cmd = new Command("hooks").description(
|
|
1961
|
-
|
|
2217
|
+
`Profile per-hook-instance data for a component.
|
|
2218
|
+
|
|
2219
|
+
METRICS CAPTURED per hook instance:
|
|
2220
|
+
useState update count, current value
|
|
2221
|
+
useCallback cache hit rate (stable reference %)
|
|
2222
|
+
useMemo cache hit rate (recompute %)
|
|
2223
|
+
useEffect execution count
|
|
2224
|
+
useRef current value snapshot
|
|
2225
|
+
|
|
2226
|
+
Examples:
|
|
2227
|
+
scope instrument hooks SearchInput
|
|
2228
|
+
scope instrument hooks SearchInput --props '{"value":"hello"}' --json
|
|
2229
|
+
scope instrument hooks Dropdown --json | jq '.hooks[] | select(.type == "useMemo")' `
|
|
1962
2230
|
).argument("<component>", "Component name (must exist in the manifest)").option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|text (default: auto)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
|
|
1963
2231
|
async (componentName, opts) => {
|
|
1964
2232
|
try {
|
|
@@ -2242,7 +2510,19 @@ async function runInteractionProfile(componentName, filePath, props, interaction
|
|
|
2242
2510
|
}
|
|
2243
2511
|
function createInstrumentProfileCommand() {
|
|
2244
2512
|
const cmd = new Command("profile").description(
|
|
2245
|
-
|
|
2513
|
+
`Capture a full performance profile for an interaction sequence.
|
|
2514
|
+
|
|
2515
|
+
PROFILE INCLUDES:
|
|
2516
|
+
renders total re-renders triggered by the interaction
|
|
2517
|
+
timing interaction start \u2192 paint time (ms)
|
|
2518
|
+
layoutShifts cumulative layout shift (CLS) score
|
|
2519
|
+
scriptTime JS execution time (ms)
|
|
2520
|
+
longTasks count of tasks >50ms
|
|
2521
|
+
|
|
2522
|
+
Examples:
|
|
2523
|
+
scope instrument profile Button --interaction '[{"action":"click","target":"button"}]'
|
|
2524
|
+
scope instrument profile Modal --interaction '[{"action":"click","target":".open-btn"}]' --json
|
|
2525
|
+
scope instrument profile Form --json | jq '.summary.renderCount'`
|
|
2246
2526
|
).argument("<component>", "Component name (must exist in the manifest)").option(
|
|
2247
2527
|
"--interaction <json>",
|
|
2248
2528
|
`Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
|
|
@@ -2578,7 +2858,21 @@ async function runInstrumentTree(options) {
|
|
|
2578
2858
|
}
|
|
2579
2859
|
}
|
|
2580
2860
|
function createInstrumentTreeCommand() {
|
|
2581
|
-
return new Command("tree").description(
|
|
2861
|
+
return new Command("tree").description(
|
|
2862
|
+
`Render a component and output the full instrumentation tree:
|
|
2863
|
+
DOM structure, computed styles per node, a11y roles, and React fibers.
|
|
2864
|
+
|
|
2865
|
+
OUTPUT STRUCTURE per node:
|
|
2866
|
+
tag / id / className DOM identity
|
|
2867
|
+
computedStyles resolved CSS properties
|
|
2868
|
+
a11y role, name, focusable
|
|
2869
|
+
children nested child nodes
|
|
2870
|
+
|
|
2871
|
+
Examples:
|
|
2872
|
+
scope instrument tree Card
|
|
2873
|
+
scope instrument tree Button --props '{"variant":"primary"}' --json
|
|
2874
|
+
scope instrument tree Input --json | jq '.tree.computedStyles'`
|
|
2875
|
+
).argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
2582
2876
|
"--wasted-renders",
|
|
2583
2877
|
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
2584
2878
|
false
|
|
@@ -2972,7 +3266,8 @@ Available: ${available}`
|
|
|
2972
3266
|
}
|
|
2973
3267
|
const rootDir = process.cwd();
|
|
2974
3268
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
2975
|
-
const preScript = getBrowserEntryScript()
|
|
3269
|
+
const preScript = `${getBrowserEntryScript()}
|
|
3270
|
+
${buildInstrumentationScript()}`;
|
|
2976
3271
|
const htmlHarness = await buildComponentHarness(
|
|
2977
3272
|
filePath,
|
|
2978
3273
|
options.componentName,
|
|
@@ -3061,7 +3356,24 @@ function formatRendersTable(result) {
|
|
|
3061
3356
|
return lines.join("\n");
|
|
3062
3357
|
}
|
|
3063
3358
|
function createInstrumentRendersCommand() {
|
|
3064
|
-
return new Command("renders").description(
|
|
3359
|
+
return new Command("renders").description(
|
|
3360
|
+
`Trace every re-render triggered by an interaction and identify root causes.
|
|
3361
|
+
|
|
3362
|
+
OUTPUT INCLUDES per render event:
|
|
3363
|
+
component which component re-rendered
|
|
3364
|
+
trigger why it re-rendered: state_change | props_change | context_change |
|
|
3365
|
+
parent_rerender | force_update | hook_dependency
|
|
3366
|
+
wasted true if re-rendered with no changed inputs and not memoized
|
|
3367
|
+
chain full causality chain from root cause to this render
|
|
3368
|
+
|
|
3369
|
+
WASTED RENDERS: propsChanged=false AND stateChanged=false AND contextChanged=false
|
|
3370
|
+
AND memoized=false \u2014 these are optimisation opportunities.
|
|
3371
|
+
|
|
3372
|
+
Examples:
|
|
3373
|
+
scope instrument renders SearchPage --interaction '[{"action":"type","target":"input","text":"hello"}]'
|
|
3374
|
+
scope instrument renders Button --interaction '[{"action":"click","target":"button"}]' --json
|
|
3375
|
+
scope instrument renders Form --json | jq '.events[] | select(.wasted == true)'`
|
|
3376
|
+
).argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
3065
3377
|
"--interaction <json>",
|
|
3066
3378
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
3067
3379
|
"[]"
|
|
@@ -3107,7 +3419,28 @@ function createInstrumentRendersCommand() {
|
|
|
3107
3419
|
}
|
|
3108
3420
|
function createInstrumentCommand() {
|
|
3109
3421
|
const instrumentCmd = new Command("instrument").description(
|
|
3110
|
-
|
|
3422
|
+
`Runtime instrumentation for React component behaviour analysis.
|
|
3423
|
+
|
|
3424
|
+
All instrument commands:
|
|
3425
|
+
1. Build an esbuild harness for the component
|
|
3426
|
+
2. Load it in a Playwright browser
|
|
3427
|
+
3. Inject instrumentation hooks into React DevTools fiber
|
|
3428
|
+
4. Execute interactions and collect events
|
|
3429
|
+
|
|
3430
|
+
PREREQUISITES:
|
|
3431
|
+
scope manifest generate (component must be in manifest)
|
|
3432
|
+
reactscope.config.json (for wrappers/globalCSS)
|
|
3433
|
+
|
|
3434
|
+
INTERACTION FORMAT:
|
|
3435
|
+
JSON array of step objects: [{action, target, text?}]
|
|
3436
|
+
Actions: click | type | focus | blur | hover | key
|
|
3437
|
+
Target: CSS selector for the element to interact with
|
|
3438
|
+
|
|
3439
|
+
Examples:
|
|
3440
|
+
scope instrument renders Button --interaction '[{"action":"click","target":"button"}]'
|
|
3441
|
+
scope instrument hooks SearchInput --props '{"value":"hello"}'
|
|
3442
|
+
scope instrument profile Modal --interaction '[{"action":"click","target":".open-btn"}]'
|
|
3443
|
+
scope instrument tree Card`
|
|
3111
3444
|
);
|
|
3112
3445
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
3113
3446
|
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
@@ -3346,7 +3679,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
|
|
|
3346
3679
|
}
|
|
3347
3680
|
});
|
|
3348
3681
|
return [...set];
|
|
3349
|
-
});
|
|
3682
|
+
}) ?? [];
|
|
3350
3683
|
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3351
3684
|
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3352
3685
|
await page.addStyleTag({ content: projectCss2 });
|
|
@@ -3359,49 +3692,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
|
|
|
3359
3692
|
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3360
3693
|
);
|
|
3361
3694
|
}
|
|
3362
|
-
const PAD =
|
|
3363
|
-
const MIN_W = 320;
|
|
3364
|
-
const MIN_H = 200;
|
|
3695
|
+
const PAD = 8;
|
|
3365
3696
|
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3366
3697
|
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3367
3698
|
const rawW = boundingBox.width + PAD * 2;
|
|
3368
3699
|
const rawH = boundingBox.height + PAD * 2;
|
|
3369
|
-
const
|
|
3370
|
-
const
|
|
3371
|
-
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3372
|
-
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3700
|
+
const safeW = Math.min(rawW, viewportWidth - clipX);
|
|
3701
|
+
const safeH = Math.min(rawH, viewportHeight - clipY);
|
|
3373
3702
|
const screenshot = await page.screenshot({
|
|
3374
3703
|
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3375
3704
|
type: "png"
|
|
3376
3705
|
});
|
|
3706
|
+
const STYLE_PROPS = [
|
|
3707
|
+
"display",
|
|
3708
|
+
"width",
|
|
3709
|
+
"height",
|
|
3710
|
+
"color",
|
|
3711
|
+
"backgroundColor",
|
|
3712
|
+
"fontSize",
|
|
3713
|
+
"fontFamily",
|
|
3714
|
+
"fontWeight",
|
|
3715
|
+
"lineHeight",
|
|
3716
|
+
"padding",
|
|
3717
|
+
"paddingTop",
|
|
3718
|
+
"paddingRight",
|
|
3719
|
+
"paddingBottom",
|
|
3720
|
+
"paddingLeft",
|
|
3721
|
+
"margin",
|
|
3722
|
+
"marginTop",
|
|
3723
|
+
"marginRight",
|
|
3724
|
+
"marginBottom",
|
|
3725
|
+
"marginLeft",
|
|
3726
|
+
"gap",
|
|
3727
|
+
"borderRadius",
|
|
3728
|
+
"borderWidth",
|
|
3729
|
+
"borderColor",
|
|
3730
|
+
"borderStyle",
|
|
3731
|
+
"boxShadow",
|
|
3732
|
+
"opacity",
|
|
3733
|
+
"position",
|
|
3734
|
+
"flexDirection",
|
|
3735
|
+
"alignItems",
|
|
3736
|
+
"justifyContent",
|
|
3737
|
+
"overflow"
|
|
3738
|
+
];
|
|
3739
|
+
const _domResult = await page.evaluate(
|
|
3740
|
+
(args) => {
|
|
3741
|
+
let count = 0;
|
|
3742
|
+
const styles = {};
|
|
3743
|
+
function captureStyles(el, id, propList) {
|
|
3744
|
+
const computed = window.getComputedStyle(el);
|
|
3745
|
+
const out = {};
|
|
3746
|
+
for (const prop of propList) {
|
|
3747
|
+
const val = computed[prop] ?? "";
|
|
3748
|
+
if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
|
|
3749
|
+
}
|
|
3750
|
+
styles[id] = out;
|
|
3751
|
+
}
|
|
3752
|
+
function walk(node) {
|
|
3753
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3754
|
+
return {
|
|
3755
|
+
tag: "#text",
|
|
3756
|
+
attrs: {},
|
|
3757
|
+
text: node.textContent?.trim() ?? "",
|
|
3758
|
+
children: []
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
const el = node;
|
|
3762
|
+
const id = count++;
|
|
3763
|
+
captureStyles(el, id, args.props);
|
|
3764
|
+
const attrs = {};
|
|
3765
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3766
|
+
attrs[attr.name] = attr.value;
|
|
3767
|
+
}
|
|
3768
|
+
const children = Array.from(el.childNodes).filter(
|
|
3769
|
+
(n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
|
|
3770
|
+
).map(walk);
|
|
3771
|
+
return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
|
|
3772
|
+
}
|
|
3773
|
+
const root = document.querySelector(args.sel);
|
|
3774
|
+
if (!root)
|
|
3775
|
+
return {
|
|
3776
|
+
tree: { tag: "div", attrs: {}, children: [] },
|
|
3777
|
+
elementCount: 0,
|
|
3778
|
+
nodeStyles: {}
|
|
3779
|
+
};
|
|
3780
|
+
return { tree: walk(root), elementCount: count, nodeStyles: styles };
|
|
3781
|
+
},
|
|
3782
|
+
{ sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
|
|
3783
|
+
);
|
|
3784
|
+
const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
|
|
3785
|
+
const elementCount = _domResult?.elementCount ?? 0;
|
|
3786
|
+
const nodeStyles = _domResult?.nodeStyles ?? {};
|
|
3377
3787
|
const computedStyles = {};
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
"fontFamily",
|
|
3391
|
-
"padding",
|
|
3392
|
-
"margin"
|
|
3393
|
-
]) {
|
|
3394
|
-
out[prop] = computed.getPropertyValue(prop);
|
|
3788
|
+
if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
|
|
3789
|
+
for (const [nodeId, styles] of Object.entries(nodeStyles)) {
|
|
3790
|
+
computedStyles[`#node-${nodeId}`] = styles;
|
|
3791
|
+
}
|
|
3792
|
+
const dom = {
|
|
3793
|
+
tree: domTree,
|
|
3794
|
+
elementCount,
|
|
3795
|
+
boundingBox: {
|
|
3796
|
+
x: boundingBox.x,
|
|
3797
|
+
y: boundingBox.y,
|
|
3798
|
+
width: boundingBox.width,
|
|
3799
|
+
height: boundingBox.height
|
|
3395
3800
|
}
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3801
|
+
};
|
|
3802
|
+
const a11yInfo = await page.evaluate((sel) => {
|
|
3803
|
+
const wrapper = document.querySelector(sel);
|
|
3804
|
+
const el = wrapper?.firstElementChild ?? wrapper;
|
|
3805
|
+
if (!el) return { role: "generic", name: "" };
|
|
3806
|
+
return {
|
|
3807
|
+
role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
|
|
3808
|
+
name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
|
|
3809
|
+
};
|
|
3810
|
+
}, "[data-reactscope-root]") ?? {
|
|
3811
|
+
role: "generic",
|
|
3812
|
+
name: ""
|
|
3813
|
+
};
|
|
3814
|
+
const imgViolations = await page.evaluate((sel) => {
|
|
3815
|
+
const container = document.querySelector(sel);
|
|
3816
|
+
if (!container) return [];
|
|
3817
|
+
const issues = [];
|
|
3818
|
+
container.querySelectorAll("img").forEach((img) => {
|
|
3819
|
+
if (!img.alt) issues.push("Image missing accessible name");
|
|
3820
|
+
});
|
|
3821
|
+
return issues;
|
|
3822
|
+
}, "[data-reactscope-root]") ?? [];
|
|
3823
|
+
const accessibility = {
|
|
3824
|
+
role: a11yInfo.role,
|
|
3825
|
+
name: a11yInfo.name,
|
|
3826
|
+
violations: imgViolations
|
|
3827
|
+
};
|
|
3399
3828
|
return {
|
|
3400
3829
|
screenshot,
|
|
3401
3830
|
width: Math.round(safeW),
|
|
3402
3831
|
height: Math.round(safeH),
|
|
3403
3832
|
renderTimeMs,
|
|
3404
|
-
computedStyles
|
|
3833
|
+
computedStyles,
|
|
3834
|
+
dom,
|
|
3835
|
+
accessibility
|
|
3405
3836
|
};
|
|
3406
3837
|
} finally {
|
|
3407
3838
|
pool.release(slot);
|
|
@@ -3439,7 +3870,27 @@ Available: ${available}`
|
|
|
3439
3870
|
return { __default__: {} };
|
|
3440
3871
|
}
|
|
3441
3872
|
function registerRenderSingle(renderCmd) {
|
|
3442
|
-
renderCmd.command("component <component>", { isDefault: true }).description(
|
|
3873
|
+
renderCmd.command("component <component>", { isDefault: true }).description(
|
|
3874
|
+
`Render one component to a PNG screenshot or JSON data object.
|
|
3875
|
+
|
|
3876
|
+
PROP SOURCES (in priority order):
|
|
3877
|
+
--scenario <name> named scenario from <ComponentName>.scope file
|
|
3878
|
+
--props <json> inline props JSON string
|
|
3879
|
+
(no flag) component rendered with all-default props
|
|
3880
|
+
|
|
3881
|
+
FORMAT DETECTION:
|
|
3882
|
+
--format png always write PNG
|
|
3883
|
+
--format json always write JSON render data
|
|
3884
|
+
auto (default) PNG when -o has .png extension or stdout is file;
|
|
3885
|
+
JSON when stdout is a pipe
|
|
3886
|
+
|
|
3887
|
+
Examples:
|
|
3888
|
+
scope render component Button
|
|
3889
|
+
scope render component Button --props '{"variant":"primary","size":"lg"}'
|
|
3890
|
+
scope render component Button --scenario hover-state -o button-hover.png
|
|
3891
|
+
scope render component Card --viewport 375x812 --theme dark
|
|
3892
|
+
scope render component Badge --format json | jq '.a11y'`
|
|
3893
|
+
).option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--scenario <name>", "Run a named scenario from the component's .scope file").option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
3443
3894
|
async (componentName, opts) => {
|
|
3444
3895
|
try {
|
|
3445
3896
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3482,6 +3933,11 @@ Available: ${available}`
|
|
|
3482
3933
|
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3483
3934
|
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3484
3935
|
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3936
|
+
if (globalCssFiles.length === 0) {
|
|
3937
|
+
process.stderr.write(
|
|
3938
|
+
"warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3485
3941
|
const renderer = buildRenderer(
|
|
3486
3942
|
filePath,
|
|
3487
3943
|
componentName,
|
|
@@ -3559,7 +4015,9 @@ Available: ${available}`
|
|
|
3559
4015
|
);
|
|
3560
4016
|
}
|
|
3561
4017
|
function registerRenderMatrix(renderCmd) {
|
|
3562
|
-
renderCmd.command("matrix <component>").description(
|
|
4018
|
+
renderCmd.command("matrix <component>").description(
|
|
4019
|
+
'Render every combination of values across one or more prop axes.\nProduces a matrix of screenshots \u2014 one cell per combination.\n\nAXES FORMAT (two equivalent forms):\n Short: --axes "variant:primary,ghost size:sm,md,lg"\n JSON: --axes {"variant":["primary","ghost"],"size":["sm","md","lg"]}\n\nCOMPOSITION CONTEXTS (--contexts):\n Test component in different layout environments.\n Available IDs: centered, rtl, sidebar, dark-bg, light-bg\n (Define custom contexts in reactscope.config.json)\n\nSTRESS PRESETS (--stress):\n Inject adversarial content to test edge cases.\n Available IDs: text.long, text.unicode, text.empty\n\nExamples:\n scope render matrix Button --axes "variant:primary,ghost,destructive"\n scope render matrix Button --axes "variant:primary,ghost size:sm,lg" --sprite matrix.png\n scope render matrix Badge --axes "type:info,warn,error" --contexts centered,rtl\n scope render matrix Input --stress text.long,text.unicode --format json'
|
|
4020
|
+
).option(
|
|
3563
4021
|
"--axes <spec>",
|
|
3564
4022
|
`Axis definitions: key:v1,v2 space-separated OR JSON object e.g. 'variant:primary,ghost size:sm,lg' or '{"variant":["primary","ghost"],"size":["sm","lg"]}'`
|
|
3565
4023
|
).option(
|
|
@@ -3718,7 +4176,9 @@ Available: ${available}`
|
|
|
3718
4176
|
);
|
|
3719
4177
|
}
|
|
3720
4178
|
function registerRenderAll(renderCmd) {
|
|
3721
|
-
renderCmd.command("all").description(
|
|
4179
|
+
renderCmd.command("all").description(
|
|
4180
|
+
"Render every component in the manifest and write to .reactscope/renders/.\n\nAlso emits .reactscope/compliance-styles.json (computed CSS class\u2192value map)\nwhich is required by `scope tokens compliance`.\n\nSCENARIO SELECTION:\n Each component is rendered using its default scenario from its .scope file\n if one exists, otherwise with all-default props.\n\nMATRIX AUTO-DETECTION:\n If a component has a .scope file with a matrix block, render all\n will render the matrix cells in addition to the default screenshot.\n\nExamples:\n scope render all\n scope render all --concurrency 8\n scope render all --format json --output-dir .reactscope/renders\n scope render all --manifest ./custom/manifest.json"
|
|
4181
|
+
).option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
3722
4182
|
async (opts) => {
|
|
3723
4183
|
try {
|
|
3724
4184
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3735,17 +4195,31 @@ function registerRenderAll(renderCmd) {
|
|
|
3735
4195
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
3736
4196
|
`);
|
|
3737
4197
|
const results = [];
|
|
4198
|
+
const complianceStylesMap = {};
|
|
3738
4199
|
let completed = 0;
|
|
3739
4200
|
const renderOne = async (name) => {
|
|
3740
4201
|
const descriptor = manifest.components[name];
|
|
3741
4202
|
if (descriptor === void 0) return;
|
|
3742
4203
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3743
4204
|
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
3744
|
-
const
|
|
4205
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
4206
|
+
const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
|
|
4207
|
+
const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
|
|
4208
|
+
const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
|
|
4209
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
4210
|
+
const renderer = buildRenderer(
|
|
4211
|
+
filePath,
|
|
4212
|
+
name,
|
|
4213
|
+
375,
|
|
4214
|
+
812,
|
|
4215
|
+
allCssFiles,
|
|
4216
|
+
process.cwd(),
|
|
4217
|
+
wrapperScript
|
|
4218
|
+
);
|
|
3745
4219
|
const outcome = await safeRender(
|
|
3746
|
-
() => renderer.renderCell(
|
|
4220
|
+
() => renderer.renderCell(renderProps, descriptor.complexityClass),
|
|
3747
4221
|
{
|
|
3748
|
-
props:
|
|
4222
|
+
props: renderProps,
|
|
3749
4223
|
sourceLocation: {
|
|
3750
4224
|
file: descriptor.filePath,
|
|
3751
4225
|
line: descriptor.loc.start,
|
|
@@ -3785,6 +4259,77 @@ function registerRenderAll(renderCmd) {
|
|
|
3785
4259
|
writeFileSync(pngPath, result.screenshot);
|
|
3786
4260
|
const jsonPath = resolve(outputDir, `${name}.json`);
|
|
3787
4261
|
writeFileSync(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
4262
|
+
const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
|
|
4263
|
+
const compStyles = {
|
|
4264
|
+
colors: {},
|
|
4265
|
+
spacing: {},
|
|
4266
|
+
typography: {},
|
|
4267
|
+
borders: {},
|
|
4268
|
+
shadows: {}
|
|
4269
|
+
};
|
|
4270
|
+
for (const [prop, val] of Object.entries(rawStyles)) {
|
|
4271
|
+
if (!val || val === "none" || val === "") continue;
|
|
4272
|
+
const lower = prop.toLowerCase();
|
|
4273
|
+
if (lower.includes("color") || lower.includes("background")) {
|
|
4274
|
+
compStyles.colors[prop] = val;
|
|
4275
|
+
} else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
|
|
4276
|
+
compStyles.spacing[prop] = val;
|
|
4277
|
+
} else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
|
|
4278
|
+
compStyles.typography[prop] = val;
|
|
4279
|
+
} else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
|
|
4280
|
+
compStyles.borders[prop] = val;
|
|
4281
|
+
} else if (lower.includes("shadow")) {
|
|
4282
|
+
compStyles.shadows[prop] = val;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
complianceStylesMap[name] = compStyles;
|
|
4286
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
|
|
4287
|
+
try {
|
|
4288
|
+
const scenarioEntries2 = Object.entries(scopeData.scenarios);
|
|
4289
|
+
const scenarioAxis = {
|
|
4290
|
+
name: "scenario",
|
|
4291
|
+
values: scenarioEntries2.map(([k]) => k)
|
|
4292
|
+
};
|
|
4293
|
+
const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
|
|
4294
|
+
const matrixRenderer = buildRenderer(
|
|
4295
|
+
filePath,
|
|
4296
|
+
name,
|
|
4297
|
+
375,
|
|
4298
|
+
812,
|
|
4299
|
+
allCssFiles,
|
|
4300
|
+
process.cwd(),
|
|
4301
|
+
wrapperScript
|
|
4302
|
+
);
|
|
4303
|
+
const wrappedRenderer = {
|
|
4304
|
+
_satori: matrixRenderer._satori,
|
|
4305
|
+
async renderCell(props, cc) {
|
|
4306
|
+
const scenarioName = props.scenario;
|
|
4307
|
+
const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
|
|
4308
|
+
return matrixRenderer.renderCell(realProps, cc ?? "simple");
|
|
4309
|
+
}
|
|
4310
|
+
};
|
|
4311
|
+
const matrix = new RenderMatrix(wrappedRenderer, [scenarioAxis], {
|
|
4312
|
+
concurrency: 2
|
|
4313
|
+
});
|
|
4314
|
+
const matrixResult = await matrix.render();
|
|
4315
|
+
const matrixCells = matrixResult.cells.map((cell) => ({
|
|
4316
|
+
axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
|
|
4317
|
+
screenshot: cell.result.screenshot.toString("base64"),
|
|
4318
|
+
width: cell.result.width,
|
|
4319
|
+
height: cell.result.height,
|
|
4320
|
+
renderTimeMs: cell.result.renderTimeMs
|
|
4321
|
+
}));
|
|
4322
|
+
const existingJson = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
4323
|
+
existingJson.cells = matrixCells;
|
|
4324
|
+
existingJson.axisLabels = [scenarioAxis.values];
|
|
4325
|
+
writeFileSync(jsonPath, JSON.stringify(existingJson, null, 2));
|
|
4326
|
+
} catch (matrixErr) {
|
|
4327
|
+
process.stderr.write(
|
|
4328
|
+
` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
|
|
4329
|
+
`
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
3788
4333
|
if (isTTY()) {
|
|
3789
4334
|
process.stdout.write(
|
|
3790
4335
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -3808,6 +4353,14 @@ function registerRenderAll(renderCmd) {
|
|
|
3808
4353
|
}
|
|
3809
4354
|
await Promise.all(workers);
|
|
3810
4355
|
await shutdownPool3();
|
|
4356
|
+
const compStylesPath = resolve(
|
|
4357
|
+
resolve(process.cwd(), opts.outputDir),
|
|
4358
|
+
"..",
|
|
4359
|
+
"compliance-styles.json"
|
|
4360
|
+
);
|
|
4361
|
+
writeFileSync(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
|
|
4362
|
+
process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
|
|
4363
|
+
`);
|
|
3811
4364
|
process.stderr.write("\n");
|
|
3812
4365
|
const summary = formatSummaryText(results, outputDir);
|
|
3813
4366
|
process.stderr.write(`${summary}
|
|
@@ -3847,7 +4400,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
3847
4400
|
}
|
|
3848
4401
|
function createRenderCommand() {
|
|
3849
4402
|
const renderCmd = new Command("render").description(
|
|
3850
|
-
|
|
4403
|
+
'Render React components to PNG screenshots or JSON render data.\n\nScope uses two render engines depending on component complexity:\n Satori \u2014 fast SVG\u2192PNG renderer for simple/presentational components\n BrowserPool \u2014 Playwright headless browser for complex components\n (context providers, hooks, async, global CSS)\n\nPREREQUISITES:\n 1. reactscope.config.json exists (scope init)\n 2. manifest.json is up to date (scope manifest generate)\n 3. If using globalCSS: Tailwind/PostCSS is configured in the project\n\nOUTPUTS (written to .reactscope/renders/<ComponentName>/):\n screenshot.png retina-quality PNG (2\xD7 physical pixels, displayed at 1\xD7)\n render.json props, dimensions, DOM snapshot, a11y, computed styles\n compliance-styles.json (render all only) \u2014 token matching input\n\nExamples:\n scope render component Button\n scope render matrix Button --axes "variant:primary,ghost size:sm,md,lg"\n scope render all\n scope render all --format json --output-dir ./out'
|
|
3851
4404
|
);
|
|
3852
4405
|
registerRenderSingle(renderCmd);
|
|
3853
4406
|
registerRenderMatrix(renderCmd);
|
|
@@ -4007,12 +4560,12 @@ async function runBaseline(options = {}) {
|
|
|
4007
4560
|
mkdirSync(rendersDir, { recursive: true });
|
|
4008
4561
|
let manifest;
|
|
4009
4562
|
if (manifestPath !== void 0) {
|
|
4010
|
-
const { readFileSync:
|
|
4563
|
+
const { readFileSync: readFileSync14 } = await import('fs');
|
|
4011
4564
|
const absPath = resolve(rootDir, manifestPath);
|
|
4012
4565
|
if (!existsSync(absPath)) {
|
|
4013
4566
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
4014
4567
|
}
|
|
4015
|
-
manifest = JSON.parse(
|
|
4568
|
+
manifest = JSON.parse(readFileSync14(absPath, "utf-8"));
|
|
4016
4569
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
4017
4570
|
`);
|
|
4018
4571
|
} else {
|
|
@@ -5065,7 +5618,9 @@ var MIME_TYPES = {
|
|
|
5065
5618
|
".ico": "image/x-icon"
|
|
5066
5619
|
};
|
|
5067
5620
|
function registerBuild(siteCmd) {
|
|
5068
|
-
siteCmd.command("build").description(
|
|
5621
|
+
siteCmd.command("build").description(
|
|
5622
|
+
'Build the static HTML site from manifest + render outputs.\n\nINPUT DIRECTORY (.reactscope/ by default) must contain:\n manifest.json component registry\n renders/ screenshots and render.json files from `scope render all`\n\nOPTIONAL:\n --compliance <path> include token compliance scores on detail pages\n --base-path <path> set if deploying to a subdirectory (e.g. /ui-docs)\n\nExamples:\n scope site build\n scope site build --title "Design System" -o .reactscope/site\n scope site build --compliance .reactscope/compliance-styles.json\n scope site build --base-path /ui'
|
|
5623
|
+
).option("-i, --input <path>", "Path to .reactscope input directory", ".reactscope").option("-o, --output <path>", "Output directory for generated site", ".reactscope/site").option("--base-path <path>", "Base URL path prefix for subdirectory deployment", "/").option("--compliance <path>", "Path to compliance batch report JSON").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
|
|
5069
5624
|
async (opts) => {
|
|
5070
5625
|
try {
|
|
5071
5626
|
const inputDir = resolve(process.cwd(), opts.input);
|
|
@@ -5107,7 +5662,9 @@ Run \`scope manifest generate\` first.`
|
|
|
5107
5662
|
);
|
|
5108
5663
|
}
|
|
5109
5664
|
function registerServe(siteCmd) {
|
|
5110
|
-
siteCmd.command("serve").description(
|
|
5665
|
+
siteCmd.command("serve").description(
|
|
5666
|
+
"Start a local HTTP server for the built site directory.\n\nRun `scope site build` first.\nCtrl+C to stop.\n\nExamples:\n scope site serve\n scope site serve --port 8080\n scope site serve --dir ./my-site-output"
|
|
5667
|
+
).option("-p, --port <number>", "Port to listen on", "3000").option("-d, --dir <path>", "Directory to serve", ".reactscope/site").action((opts) => {
|
|
5111
5668
|
try {
|
|
5112
5669
|
const port = Number.parseInt(opts.port, 10);
|
|
5113
5670
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
@@ -5171,7 +5728,7 @@ Run \`scope site build\` first.`
|
|
|
5171
5728
|
}
|
|
5172
5729
|
function createSiteCommand() {
|
|
5173
5730
|
const siteCmd = new Command("site").description(
|
|
5174
|
-
|
|
5731
|
+
'Build and serve the static HTML component gallery site.\n\nPREREQUISITES:\n scope manifest generate (manifest.json)\n scope render all (renders/ + compliance-styles.json)\n\nSITE CONTENTS:\n /index.html component gallery with screenshots + metadata\n /<component>/index.html detail page: props, renders, matrix, X-Ray, compliance\n\nExamples:\n scope site build && scope site serve\n scope site build --title "Acme UI" --compliance .reactscope/compliance-styles.json\n scope site serve --port 8080'
|
|
5175
5732
|
);
|
|
5176
5733
|
registerBuild(siteCmd);
|
|
5177
5734
|
registerServe(siteCmd);
|
|
@@ -5309,7 +5866,9 @@ function formatComplianceReport(batch, threshold) {
|
|
|
5309
5866
|
return lines.join("\n");
|
|
5310
5867
|
}
|
|
5311
5868
|
function registerCompliance(tokensCmd) {
|
|
5312
|
-
tokensCmd.command("compliance").description(
|
|
5869
|
+
tokensCmd.command("compliance").description(
|
|
5870
|
+
"Compute a token compliance score across all rendered components.\n\nCompares computed CSS values from .reactscope/compliance-styles.json\nagainst the token file \u2014 reports what % of style values are on-token.\n\nPREREQUISITES:\n scope render all must have run first (produces compliance-styles.json)\n\nSCORING:\n compliant value exactly matches a token\n near-match value is within tolerance of a token (e.g. close color)\n off-token value not found in token file\n\nEXIT CODES:\n 0 compliance >= threshold (or no --threshold set)\n 1 compliance < threshold\n\nExamples:\n scope tokens compliance\n scope tokens compliance --threshold 90\n scope tokens compliance --format json | jq '.summary'\n scope tokens compliance --styles ./custom/compliance-styles.json"
|
|
5871
|
+
).option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
5313
5872
|
try {
|
|
5314
5873
|
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
5315
5874
|
const { tokens } = loadTokens(tokenFilePath);
|
|
@@ -5367,7 +5926,9 @@ function resolveTokenFilePath2(fileFlag) {
|
|
|
5367
5926
|
return resolve(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
5368
5927
|
}
|
|
5369
5928
|
function createTokensExportCommand() {
|
|
5370
|
-
return new Command("export").description(
|
|
5929
|
+
return new Command("export").description(
|
|
5930
|
+
'Export design tokens to CSS variables, TypeScript, SCSS, Tailwind config, Figma, or flat JSON.\n\nFORMATS:\n css CSS custom properties (:root { --color-primary-500: #3b82f6; })\n scss SCSS variables ($color-primary-500: #3b82f6;)\n ts TypeScript const export (export const tokens = {...})\n tailwind Tailwind theme.extend block (paste into tailwind.config.js)\n flat-json Flat { path: value } map (useful for tooling integration)\n figma Figma Tokens plugin format\n\nExamples:\n scope tokens export --format css --out src/tokens.css\n scope tokens export --format css --prefix brand --selector ":root, [data-theme]"\n scope tokens export --format tailwind --out tailwind-tokens.js\n scope tokens export --format ts --out src/tokens.ts\n scope tokens export --format css --theme dark --out dark-tokens.css'
|
|
5931
|
+
).requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
5371
5932
|
"--theme <name>",
|
|
5372
5933
|
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
5373
5934
|
).action(
|
|
@@ -5507,7 +6068,9 @@ function formatImpactSummary(report) {
|
|
|
5507
6068
|
return `\u2192 ${parts.join(", ")}`;
|
|
5508
6069
|
}
|
|
5509
6070
|
function registerImpact(tokensCmd) {
|
|
5510
|
-
tokensCmd.command("impact <path>").description(
|
|
6071
|
+
tokensCmd.command("impact <path>").description(
|
|
6072
|
+
"List every component and CSS element that uses a given token.\nUse this to understand the blast radius before changing a token value.\n\nPREREQUISITE: scope render all (populates compliance-styles.json)\n\nExamples:\n scope tokens impact color.primary.500\n scope tokens impact spacing.4 --format json\n scope tokens impact font.size.base | grep Button"
|
|
6073
|
+
).option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH2})`).option("--new-value <value>", "Proposed new value \u2014 report visual severity of the change").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action(
|
|
5511
6074
|
(tokenPath, opts) => {
|
|
5512
6075
|
try {
|
|
5513
6076
|
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
@@ -5596,7 +6159,9 @@ async function renderComponentWithCssOverride(filePath, componentName, cssOverri
|
|
|
5596
6159
|
}
|
|
5597
6160
|
}
|
|
5598
6161
|
function registerPreview(tokensCmd) {
|
|
5599
|
-
tokensCmd.command("preview <path>").description(
|
|
6162
|
+
tokensCmd.command("preview <path>").description(
|
|
6163
|
+
'Render before/after screenshots of all components affected by a token change.\nUseful for visual review before committing a token value update.\n\nPREREQUISITE: scope render all (provides baseline renders)\n\nExamples:\n scope tokens preview color.primary.500\n scope tokens preview color.primary.500 --new-value "#2563eb" -o preview.png'
|
|
6164
|
+
).requiredOption("--new-value <value>", "The proposed new resolved value for the token").option("--sprite", "Output a PNG sprite sheet (default when TTY)", false).option("-o, --output <path>", "Output PNG path (default: .reactscope/previews/<token>.png)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH3})`).option("--manifest <path>", "Path to manifest.json", DEFAULT_MANIFEST_PATH).option("--format <fmt>", "Output format: json or text (default: auto-detect)").option("--timeout <ms>", "Browser timeout per render (ms)", "10000").option("--viewport-width <px>", "Viewport width in pixels", "1280").option("--viewport-height <px>", "Viewport height in pixels", "720").action(
|
|
5600
6165
|
async (tokenPath, opts) => {
|
|
5601
6166
|
try {
|
|
5602
6167
|
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
@@ -5843,7 +6408,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
5843
6408
|
return chain;
|
|
5844
6409
|
}
|
|
5845
6410
|
function registerGet2(tokensCmd) {
|
|
5846
|
-
tokensCmd.command("get <path>").description(
|
|
6411
|
+
tokensCmd.command("get <path>").description(
|
|
6412
|
+
"Resolve a token path and print its final computed value.\nFollows all {ref} chains to the raw value.\n\nExamples:\n scope tokens get color.primary.500\n scope tokens get spacing.4 --format json\n scope tokens get font.size.base --file ./tokens/brand.json"
|
|
6413
|
+
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
5847
6414
|
try {
|
|
5848
6415
|
const filePath = resolveTokenFilePath(opts.file);
|
|
5849
6416
|
const { tokens } = loadTokens(filePath);
|
|
@@ -5868,7 +6435,18 @@ function registerGet2(tokensCmd) {
|
|
|
5868
6435
|
});
|
|
5869
6436
|
}
|
|
5870
6437
|
function registerList2(tokensCmd) {
|
|
5871
|
-
tokensCmd.command("list [category]").description(
|
|
6438
|
+
tokensCmd.command("list [category]").description(
|
|
6439
|
+
`List all tokens, optionally filtered by category prefix or type.
|
|
6440
|
+
|
|
6441
|
+
CATEGORY: top-level token namespace (e.g. "color", "spacing", "typography")
|
|
6442
|
+
TYPE: token value type \u2014 color | spacing | typography | shadow | radius | opacity
|
|
6443
|
+
|
|
6444
|
+
Examples:
|
|
6445
|
+
scope tokens list
|
|
6446
|
+
scope tokens list color
|
|
6447
|
+
scope tokens list --type spacing
|
|
6448
|
+
scope tokens list color --format json | jq '.[].path'`
|
|
6449
|
+
).option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
5872
6450
|
(category, opts) => {
|
|
5873
6451
|
try {
|
|
5874
6452
|
const filePath = resolveTokenFilePath(opts.file);
|
|
@@ -5898,7 +6476,9 @@ function registerList2(tokensCmd) {
|
|
|
5898
6476
|
);
|
|
5899
6477
|
}
|
|
5900
6478
|
function registerSearch(tokensCmd) {
|
|
5901
|
-
tokensCmd.command("search <value>").description(
|
|
6479
|
+
tokensCmd.command("search <value>").description(
|
|
6480
|
+
'Find the token(s) whose computed value matches the given raw value.\nSupports fuzzy color matching (hex \u2194 rgb \u2194 hsl equivalence).\n\nExamples:\n scope tokens search "#3b82f6"\n scope tokens search "16px"\n scope tokens search "rgb(59, 130, 246)" # fuzzy-matches #3b82f6'
|
|
6481
|
+
).option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
5902
6482
|
(value, opts) => {
|
|
5903
6483
|
try {
|
|
5904
6484
|
const filePath = resolveTokenFilePath(opts.file);
|
|
@@ -5981,7 +6561,9 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
5981
6561
|
);
|
|
5982
6562
|
}
|
|
5983
6563
|
function registerResolve(tokensCmd) {
|
|
5984
|
-
tokensCmd.command("resolve <path>").description(
|
|
6564
|
+
tokensCmd.command("resolve <path>").description(
|
|
6565
|
+
"Print the full reference chain from a token path down to its raw value.\nUseful for debugging circular references or understanding token inheritance.\n\nExamples:\n scope tokens resolve color.primary.500\n scope tokens resolve button.background --format json"
|
|
6566
|
+
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
5985
6567
|
try {
|
|
5986
6568
|
const filePath = resolveTokenFilePath(opts.file);
|
|
5987
6569
|
const absFilePath = filePath;
|
|
@@ -6017,7 +6599,19 @@ function registerResolve(tokensCmd) {
|
|
|
6017
6599
|
}
|
|
6018
6600
|
function registerValidate(tokensCmd) {
|
|
6019
6601
|
tokensCmd.command("validate").description(
|
|
6020
|
-
|
|
6602
|
+
`Validate the token file and report errors.
|
|
6603
|
+
|
|
6604
|
+
CHECKS:
|
|
6605
|
+
- Circular reference chains (A \u2192 B \u2192 A)
|
|
6606
|
+
- Broken references ({path.that.does.not.exist})
|
|
6607
|
+
- Type mismatches (token declared as "color" but value is a number)
|
|
6608
|
+
- Duplicate paths
|
|
6609
|
+
|
|
6610
|
+
Exits 1 if any errors are found (suitable for CI).
|
|
6611
|
+
|
|
6612
|
+
Examples:
|
|
6613
|
+
scope tokens validate
|
|
6614
|
+
scope tokens validate --format json | jq '.errors'`
|
|
6021
6615
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
6022
6616
|
try {
|
|
6023
6617
|
const filePath = resolveTokenFilePath(opts.file);
|
|
@@ -6096,7 +6690,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
6096
6690
|
}
|
|
6097
6691
|
function createTokensCommand() {
|
|
6098
6692
|
const tokensCmd = new Command("tokens").description(
|
|
6099
|
-
|
|
6693
|
+
'Query, validate, and export design tokens from reactscope.tokens.json.\n\nTOKEN FILE RESOLUTION (in priority order):\n 1. --file <path> explicit override\n 2. tokens.file in reactscope.config.json\n 3. reactscope.tokens.json default (project root)\n\nTOKEN FILE FORMAT (reactscope.tokens.json):\n Nested JSON. Each leaf is a token with { value, type } or just a raw value.\n Paths use dot notation: color.primary.500, spacing.4, font.size.base\n References use {path.to.other.token} syntax.\n Themes: top-level "themes" key with named override maps.\n\nTOKEN TYPES: color | spacing | typography | shadow | radius | opacity | other\n\nExamples:\n scope tokens validate\n scope tokens list color\n scope tokens get color.primary.500\n scope tokens compliance\n scope tokens export --format css --out tokens.css'
|
|
6100
6694
|
);
|
|
6101
6695
|
registerGet2(tokensCmd);
|
|
6102
6696
|
registerList2(tokensCmd);
|
|
@@ -6112,8 +6706,12 @@ function createTokensCommand() {
|
|
|
6112
6706
|
|
|
6113
6707
|
// src/program.ts
|
|
6114
6708
|
function createProgram(options = {}) {
|
|
6115
|
-
const program = new Command("scope").version(options.version ?? "0.1.0").description(
|
|
6116
|
-
|
|
6709
|
+
const program = new Command("scope").version(options.version ?? "0.1.0").description(
|
|
6710
|
+
'Scope \u2014 static analysis + visual rendering toolkit for React component libraries.\n\nScope answers questions about React codebases \u2014 structure, props, visual output,\ndesign token compliance \u2014 without running the full application.\n\nQUICKSTART (new project):\n scope init # detect config, scaffold reactscope.config.json\n scope doctor # verify setup before doing anything else\n scope manifest generate # scan source and build component manifest\n scope render all # screenshot every component\n scope site build # build HTML gallery\n scope site serve # open at http://localhost:3000\n\nQUICKSTART (existing project / CI):\n scope ci # manifest \u2192 render \u2192 compliance \u2192 regression in one step\n\nAGENT BOOTSTRAP:\n scope get-skill # print SKILL.md to stdout \u2014 pipe into agent context\n\nCONFIG FILE: reactscope.config.json (created by `scope init`)\n components.include glob patterns for component files (e.g. "src/**/*.tsx")\n components.wrappers providers and globalCSS to wrap every render\n render.viewport default viewport width\xD7height in px\n tokens.file path to reactscope.tokens.json (default)\n output.dir output root (default: .reactscope/)\n ci.complianceThreshold fail threshold for `scope ci` (default: 0.90)\n\nOUTPUT DIRECTORY: .reactscope/\n manifest.json component registry \u2014 updated by `scope manifest generate`\n renders/<Name>/ PNGs + render.json per component\n compliance-styles.json computed-style map for token matching\n site/ static HTML gallery (built by `scope site build`)\n\nRun `scope <command> --help` for detailed flags and examples.'
|
|
6711
|
+
);
|
|
6712
|
+
program.command("capture <url>").description(
|
|
6713
|
+
"Capture the live React component tree from a running app and emit it as JSON.\nRequires a running dev server at the given URL (e.g. http://localhost:5173).\n\nExamples:\n scope capture http://localhost:5173\n scope capture http://localhost:5173 -o report.json --pretty\n scope capture http://localhost:5173 --timeout 15000 --wait 500"
|
|
6714
|
+
).option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
6117
6715
|
async (url, opts) => {
|
|
6118
6716
|
try {
|
|
6119
6717
|
const { report } = await browserCapture({
|
|
@@ -6137,7 +6735,9 @@ function createProgram(options = {}) {
|
|
|
6137
6735
|
}
|
|
6138
6736
|
}
|
|
6139
6737
|
);
|
|
6140
|
-
program.command("tree <url>").description(
|
|
6738
|
+
program.command("tree <url>").description(
|
|
6739
|
+
"Print a formatted React component tree from a running app.\nUseful for quickly understanding component hierarchy without full capture.\n\nExamples:\n scope tree http://localhost:5173\n scope tree http://localhost:5173 --show-props --show-hooks\n scope tree http://localhost:5173 --depth 4"
|
|
6740
|
+
).option("--depth <n>", "Max depth to display (default: unlimited)").option("--show-props", "Include prop names next to components", false).option("--show-hooks", "Show hook counts per component", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
6141
6741
|
async (url, opts) => {
|
|
6142
6742
|
try {
|
|
6143
6743
|
const { report } = await browserCapture({
|
|
@@ -6160,7 +6760,9 @@ function createProgram(options = {}) {
|
|
|
6160
6760
|
}
|
|
6161
6761
|
}
|
|
6162
6762
|
);
|
|
6163
|
-
program.command("report <url>").description(
|
|
6763
|
+
program.command("report <url>").description(
|
|
6764
|
+
"Capture a React app and print a human-readable analysis summary.\nIncludes component count, hook usage, side-effect summary, and more.\n\nExamples:\n scope report http://localhost:5173\n scope report http://localhost:5173 --json\n scope report http://localhost:5173 --json -o report.json"
|
|
6765
|
+
).option("--json", "Output as structured JSON instead of human-readable text", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
6164
6766
|
async (url, opts) => {
|
|
6165
6767
|
try {
|
|
6166
6768
|
const { report } = await browserCapture({
|
|
@@ -6184,7 +6786,9 @@ function createProgram(options = {}) {
|
|
|
6184
6786
|
}
|
|
6185
6787
|
}
|
|
6186
6788
|
);
|
|
6187
|
-
program.command("generate").description(
|
|
6789
|
+
program.command("generate").description(
|
|
6790
|
+
'Generate a Playwright test file from a Scope trace (.json).\nTraces are produced by scope instrument renders or scope capture.\n\nExamples:\n scope generate trace.json\n scope generate trace.json -o tests/scope.spec.ts -d "User login flow"'
|
|
6791
|
+
).argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
6188
6792
|
const raw = readFileSync(tracePath, "utf-8");
|
|
6189
6793
|
const trace = loadTrace(raw);
|
|
6190
6794
|
const source = generateTest(trace, {
|
|
@@ -6200,6 +6804,8 @@ function createProgram(options = {}) {
|
|
|
6200
6804
|
program.addCommand(createInstrumentCommand());
|
|
6201
6805
|
program.addCommand(createInitCommand());
|
|
6202
6806
|
program.addCommand(createCiCommand());
|
|
6807
|
+
program.addCommand(createDoctorCommand());
|
|
6808
|
+
program.addCommand(createGetSkillCommand());
|
|
6203
6809
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
6204
6810
|
if (existingReportCmd !== void 0) {
|
|
6205
6811
|
registerBaselineSubCommand(existingReportCmd);
|
|
@@ -6210,6 +6816,6 @@ function createProgram(options = {}) {
|
|
|
6210
6816
|
return program;
|
|
6211
6817
|
}
|
|
6212
6818
|
|
|
6213
|
-
export { CI_EXIT, createCiCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
|
|
6819
|
+
export { CI_EXIT, createCiCommand, createDoctorCommand, createGetSkillCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
|
|
6214
6820
|
//# sourceMappingURL=index.js.map
|
|
6215
6821
|
//# sourceMappingURL=index.js.map
|