uilint 0.2.6 → 0.2.8
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/index.js +87 -764
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -237,8 +237,8 @@ async function initializeLangfuseIfEnabled() {
|
|
|
237
237
|
},
|
|
238
238
|
{ asType: "generation" }
|
|
239
239
|
);
|
|
240
|
-
await new Promise((
|
|
241
|
-
resolveTrace =
|
|
240
|
+
await new Promise((resolve7) => {
|
|
241
|
+
resolveTrace = resolve7;
|
|
242
242
|
});
|
|
243
243
|
if (endData && generationRef) {
|
|
244
244
|
const usageDetails = endData.usage ? Object.fromEntries(
|
|
@@ -1249,14 +1249,14 @@ import {
|
|
|
1249
1249
|
} from "uilint-core";
|
|
1250
1250
|
import { ensureOllamaReady as ensureOllamaReady3 } from "uilint-core/node";
|
|
1251
1251
|
async function readStdin2() {
|
|
1252
|
-
return new Promise((
|
|
1252
|
+
return new Promise((resolve7) => {
|
|
1253
1253
|
let data = "";
|
|
1254
1254
|
const rl = createInterface({ input: process.stdin });
|
|
1255
1255
|
rl.on("line", (line) => {
|
|
1256
1256
|
data += line;
|
|
1257
1257
|
});
|
|
1258
1258
|
rl.on("close", () => {
|
|
1259
|
-
|
|
1259
|
+
resolve7(data);
|
|
1260
1260
|
});
|
|
1261
1261
|
});
|
|
1262
1262
|
}
|
|
@@ -1837,7 +1837,7 @@ function detectPackageManager(projectPath) {
|
|
|
1837
1837
|
return "npm";
|
|
1838
1838
|
}
|
|
1839
1839
|
function spawnAsync(command, args, cwd) {
|
|
1840
|
-
return new Promise((
|
|
1840
|
+
return new Promise((resolve7, reject) => {
|
|
1841
1841
|
const child = spawn(command, args, {
|
|
1842
1842
|
cwd,
|
|
1843
1843
|
stdio: "inherit",
|
|
@@ -1845,7 +1845,7 @@ function spawnAsync(command, args, cwd) {
|
|
|
1845
1845
|
});
|
|
1846
1846
|
child.on("error", reject);
|
|
1847
1847
|
child.on("close", (code) => {
|
|
1848
|
-
if (code === 0)
|
|
1848
|
+
if (code === 0) resolve7();
|
|
1849
1849
|
else
|
|
1850
1850
|
reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
|
|
1851
1851
|
});
|
|
@@ -2427,39 +2427,15 @@ async function installEslintPlugin(opts) {
|
|
|
2427
2427
|
}
|
|
2428
2428
|
|
|
2429
2429
|
// src/commands/install/analyze.ts
|
|
2430
|
-
var LEGACY_HOOK_FILES = ["uilint-validate.sh", "uilint-validate.js"];
|
|
2431
|
-
function safeParseJson(filePath) {
|
|
2432
|
-
try {
|
|
2433
|
-
const content = readFileSync5(filePath, "utf-8");
|
|
2434
|
-
return JSON.parse(content);
|
|
2435
|
-
} catch {
|
|
2436
|
-
return void 0;
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
2430
|
async function analyze2(projectPath = process.cwd()) {
|
|
2440
2431
|
const workspaceRoot = findWorkspaceRoot5(projectPath);
|
|
2441
2432
|
const packageManager = detectPackageManager(projectPath);
|
|
2442
2433
|
const cursorDir = join8(projectPath, ".cursor");
|
|
2443
2434
|
const cursorDirExists = existsSync9(cursorDir);
|
|
2444
|
-
const mcpPath = join8(cursorDir, "mcp.json");
|
|
2445
|
-
const mcpExists = existsSync9(mcpPath);
|
|
2446
|
-
const mcpConfig = mcpExists ? safeParseJson(mcpPath) : void 0;
|
|
2447
|
-
const hooksPath = join8(cursorDir, "hooks.json");
|
|
2448
|
-
const hooksExists = existsSync9(hooksPath);
|
|
2449
|
-
const hooksConfig = hooksExists ? safeParseJson(hooksPath) : void 0;
|
|
2450
|
-
const hooksDir = join8(cursorDir, "hooks");
|
|
2451
|
-
const legacyPaths = [];
|
|
2452
|
-
for (const legacyFile of LEGACY_HOOK_FILES) {
|
|
2453
|
-
const legacyPath = join8(hooksDir, legacyFile);
|
|
2454
|
-
if (existsSync9(legacyPath)) {
|
|
2455
|
-
legacyPaths.push(legacyPath);
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
2435
|
const styleguidePath = join8(projectPath, ".uilint", "styleguide.md");
|
|
2459
2436
|
const styleguideExists = existsSync9(styleguidePath);
|
|
2460
2437
|
const commandsDir = join8(cursorDir, "commands");
|
|
2461
2438
|
const genstyleguideExists = existsSync9(join8(commandsDir, "genstyleguide.md"));
|
|
2462
|
-
const genrulesExists = existsSync9(join8(commandsDir, "genrules.md"));
|
|
2463
2439
|
const nextApps = [];
|
|
2464
2440
|
const directDetection = detectNextAppRouter(projectPath);
|
|
2465
2441
|
if (directDetection) {
|
|
@@ -2518,25 +2494,12 @@ async function analyze2(projectPath = process.cwd()) {
|
|
|
2518
2494
|
exists: cursorDirExists,
|
|
2519
2495
|
path: cursorDir
|
|
2520
2496
|
},
|
|
2521
|
-
mcp: {
|
|
2522
|
-
exists: mcpExists,
|
|
2523
|
-
path: mcpPath,
|
|
2524
|
-
config: mcpConfig
|
|
2525
|
-
},
|
|
2526
|
-
hooks: {
|
|
2527
|
-
exists: hooksExists,
|
|
2528
|
-
path: hooksPath,
|
|
2529
|
-
config: hooksConfig,
|
|
2530
|
-
hasLegacy: legacyPaths.length > 0,
|
|
2531
|
-
legacyPaths
|
|
2532
|
-
},
|
|
2533
2497
|
styleguide: {
|
|
2534
2498
|
exists: styleguideExists,
|
|
2535
2499
|
path: styleguidePath
|
|
2536
2500
|
},
|
|
2537
2501
|
commands: {
|
|
2538
|
-
genstyleguide: genstyleguideExists
|
|
2539
|
-
genrules: genrulesExists
|
|
2502
|
+
genstyleguide: genstyleguideExists
|
|
2540
2503
|
},
|
|
2541
2504
|
nextApps,
|
|
2542
2505
|
viteApps,
|
|
@@ -2549,171 +2512,6 @@ import { join as join11 } from "path";
|
|
|
2549
2512
|
import { createRequire as createRequire2 } from "module";
|
|
2550
2513
|
|
|
2551
2514
|
// src/commands/install/constants.ts
|
|
2552
|
-
var HOOKS_CONFIG = {
|
|
2553
|
-
version: 1,
|
|
2554
|
-
hooks: {
|
|
2555
|
-
beforeSubmitPrompt: [{ command: ".cursor/hooks/uilint-session-start.sh" }],
|
|
2556
|
-
afterFileEdit: [{ command: ".cursor/hooks/uilint-track.sh" }],
|
|
2557
|
-
stop: [{ command: ".cursor/hooks/uilint-session-end.sh" }]
|
|
2558
|
-
}
|
|
2559
|
-
};
|
|
2560
|
-
var MCP_CONFIG = {
|
|
2561
|
-
mcpServers: {
|
|
2562
|
-
uilint: {
|
|
2563
|
-
command: "npx",
|
|
2564
|
-
args: ["uilint-mcp"]
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
};
|
|
2568
|
-
var LEGACY_HOOK_COMMANDS = [
|
|
2569
|
-
".cursor/hooks/uilint-validate.sh",
|
|
2570
|
-
".cursor/hooks/uilint-validate.js"
|
|
2571
|
-
];
|
|
2572
|
-
var SESSION_START_SCRIPT = `#!/bin/bash
|
|
2573
|
-
# UILint session start hook
|
|
2574
|
-
# Clears tracked files at the start of each agent turn
|
|
2575
|
-
#
|
|
2576
|
-
# IMPORTANT: Cursor hooks communicate over stdio using JSON.
|
|
2577
|
-
# - stdout must be JSON (Cursor will parse it)
|
|
2578
|
-
# - stderr is for logs
|
|
2579
|
-
|
|
2580
|
-
echo "[UILint] Session start - clearing tracked files" >&2
|
|
2581
|
-
|
|
2582
|
-
# Prefer local monorepo build when developing UILint itself.
|
|
2583
|
-
# Fall back to npx for normal consumers.
|
|
2584
|
-
uilint() {
|
|
2585
|
-
if [ -f "packages/uilint/dist/index.js" ]; then
|
|
2586
|
-
node "packages/uilint/dist/index.js" "$@"
|
|
2587
|
-
else
|
|
2588
|
-
npx uilint "$@"
|
|
2589
|
-
fi
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
# Read JSON input from stdin (required by hook protocol)
|
|
2593
|
-
cat > /dev/null
|
|
2594
|
-
|
|
2595
|
-
# Clear session state
|
|
2596
|
-
result=$(uilint session clear)
|
|
2597
|
-
status=$?
|
|
2598
|
-
|
|
2599
|
-
echo "[UILint] Clear exit: $status" >&2
|
|
2600
|
-
|
|
2601
|
-
if [ $status -eq 0 ] && [ -n "$result" ]; then
|
|
2602
|
-
echo "$result"
|
|
2603
|
-
else
|
|
2604
|
-
echo '{"cleared":false}'
|
|
2605
|
-
fi
|
|
2606
|
-
|
|
2607
|
-
exit 0
|
|
2608
|
-
`;
|
|
2609
|
-
var TRACK_SCRIPT = `#!/bin/bash
|
|
2610
|
-
# UILint file tracking hook
|
|
2611
|
-
# Tracks UI file edits for batch validation on agent stop
|
|
2612
|
-
#
|
|
2613
|
-
# IMPORTANT: Cursor hooks communicate over stdio using JSON.
|
|
2614
|
-
# - stdout must be JSON (Cursor will parse it)
|
|
2615
|
-
# - stderr is for logs
|
|
2616
|
-
|
|
2617
|
-
out='{}'
|
|
2618
|
-
|
|
2619
|
-
# Read JSON input from stdin
|
|
2620
|
-
input=$(cat)
|
|
2621
|
-
|
|
2622
|
-
echo "[UILint] afterFileEdit hook triggered" >&2
|
|
2623
|
-
|
|
2624
|
-
# Prefer local monorepo build when developing UILint itself.
|
|
2625
|
-
# Fall back to npx for normal consumers.
|
|
2626
|
-
uilint() {
|
|
2627
|
-
if [ -f "packages/uilint/dist/index.js" ]; then
|
|
2628
|
-
node "packages/uilint/dist/index.js" "$@"
|
|
2629
|
-
else
|
|
2630
|
-
npx uilint "$@"
|
|
2631
|
-
fi
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
# Extract file_path using grep/sed (works without jq)
|
|
2635
|
-
file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)"/\\1/')
|
|
2636
|
-
|
|
2637
|
-
echo "[UILint] Extracted file_path: $file_path" >&2
|
|
2638
|
-
|
|
2639
|
-
if [ -z "$file_path" ]; then
|
|
2640
|
-
echo "[UILint] No file_path found in input, skipping" >&2
|
|
2641
|
-
printf '%s\\n' "$out"
|
|
2642
|
-
exit 0
|
|
2643
|
-
fi
|
|
2644
|
-
|
|
2645
|
-
# Track the file (session command filters for UI files internally)
|
|
2646
|
-
echo "[UILint] Tracking file: $file_path" >&2
|
|
2647
|
-
result=$(uilint session track "$file_path")
|
|
2648
|
-
status=$?
|
|
2649
|
-
|
|
2650
|
-
echo "[UILint] Track exit: $status" >&2
|
|
2651
|
-
|
|
2652
|
-
if [ $status -eq 0 ] && [ -n "$result" ]; then
|
|
2653
|
-
out="$result"
|
|
2654
|
-
fi
|
|
2655
|
-
|
|
2656
|
-
printf '%s\\n' "$out"
|
|
2657
|
-
exit 0
|
|
2658
|
-
`;
|
|
2659
|
-
var SESSION_END_SCRIPT = `#!/bin/bash
|
|
2660
|
-
# UILint session end hook
|
|
2661
|
-
# Scans tracked markup files and returns followup_message for auto-fix
|
|
2662
|
-
#
|
|
2663
|
-
# IMPORTANT: Cursor hooks communicate over stdio using JSON.
|
|
2664
|
-
# - stdout must be JSON (Cursor will parse it)
|
|
2665
|
-
# - stderr is for logs
|
|
2666
|
-
|
|
2667
|
-
echo "[UILint] Session end hook triggered" >&2
|
|
2668
|
-
|
|
2669
|
-
# Read JSON input from stdin (contains status, loop_count)
|
|
2670
|
-
input=$(cat)
|
|
2671
|
-
|
|
2672
|
-
echo "[UILint] Stop input: $input" >&2
|
|
2673
|
-
|
|
2674
|
-
# Prefer local monorepo build when developing UILint itself.
|
|
2675
|
-
# Fall back to npx for normal consumers.
|
|
2676
|
-
uilint() {
|
|
2677
|
-
if [ -f "packages/uilint/dist/index.js" ]; then
|
|
2678
|
-
node "packages/uilint/dist/index.js" "$@"
|
|
2679
|
-
else
|
|
2680
|
-
npx uilint "$@"
|
|
2681
|
-
fi
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
# Extract loop_count to prevent infinite loops
|
|
2685
|
-
loop_count=$(echo "$input" | grep -o '"loop_count"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$')
|
|
2686
|
-
loop_count=\${loop_count:-0}
|
|
2687
|
-
|
|
2688
|
-
echo "[UILint] Loop count: $loop_count" >&2
|
|
2689
|
-
|
|
2690
|
-
# Don't trigger followup if we've already looped 3+ times
|
|
2691
|
-
if [ "$loop_count" -ge 3 ]; then
|
|
2692
|
-
echo "[UILint] Max loops reached, skipping scan" >&2
|
|
2693
|
-
echo '{}'
|
|
2694
|
-
exit 0
|
|
2695
|
-
fi
|
|
2696
|
-
|
|
2697
|
-
# First check what files are tracked
|
|
2698
|
-
echo "[UILint] Checking tracked files..." >&2
|
|
2699
|
-
tracked=$(uilint session list)
|
|
2700
|
-
echo "[UILint] Tracked files: $tracked" >&2
|
|
2701
|
-
|
|
2702
|
-
# Run scan with --hook flag for direct JSON output
|
|
2703
|
-
echo "[UILint] Running scan..." >&2
|
|
2704
|
-
result=$(uilint session scan --hook)
|
|
2705
|
-
status=$?
|
|
2706
|
-
|
|
2707
|
-
echo "[UILint] Scan exit: $status" >&2
|
|
2708
|
-
|
|
2709
|
-
if [ $status -eq 0 ] && [ -n "$result" ]; then
|
|
2710
|
-
echo "$result"
|
|
2711
|
-
else
|
|
2712
|
-
echo '{}'
|
|
2713
|
-
fi
|
|
2714
|
-
|
|
2715
|
-
exit 0
|
|
2716
|
-
`;
|
|
2717
2515
|
var GENSTYLEGUIDE_COMMAND_MD = `# React Style Guide Generator
|
|
2718
2516
|
|
|
2719
2517
|
Analyze the React UI codebase to produce a **prescriptive, semantic** style guide. Focus on consistency, intent, and relationships\u2014not specific values.
|
|
@@ -2832,107 +2630,6 @@ conventions:
|
|
|
2832
2630
|
- **Omit if N/A**: Skip sections that don't apply
|
|
2833
2631
|
- **Max 5 items** per section \u2014 highest impact only
|
|
2834
2632
|
`;
|
|
2835
|
-
var GENRULES_COMMAND_MD = `# ESLint Rule Generator
|
|
2836
|
-
|
|
2837
|
-
Generate custom ESLint rules from your UILint styleguide (\`.uilint/styleguide.md\`).
|
|
2838
|
-
|
|
2839
|
-
## Purpose
|
|
2840
|
-
|
|
2841
|
-
Transform your semantic styleguide rules into concrete, enforceable ESLint rules that:
|
|
2842
|
-
- Run automatically during development
|
|
2843
|
-
- Integrate with your editor
|
|
2844
|
-
- Catch issues before commit
|
|
2845
|
-
- Provide actionable error messages
|
|
2846
|
-
|
|
2847
|
-
## Analysis Steps
|
|
2848
|
-
|
|
2849
|
-
### 1. Read the Styleguide
|
|
2850
|
-
|
|
2851
|
-
Look at \`.uilint/styleguide.md\` for:
|
|
2852
|
-
- **Component Usage** (\`use:\` section) - which components should be used
|
|
2853
|
-
- **Forbidden** patterns - what to disallow
|
|
2854
|
-
- **Semantic Rules** - spacing, consistency, hierarchy
|
|
2855
|
-
- **Patterns** - form handling, conditionals, state management
|
|
2856
|
-
|
|
2857
|
-
### 2. Identify Rule Candidates
|
|
2858
|
-
|
|
2859
|
-
Focus on rules that can be statically analyzed:
|
|
2860
|
-
- Import patterns (e.g., "use Button from shadcn, not raw HTML button")
|
|
2861
|
-
- Forbidden patterns (e.g., "no inline style={{}}")
|
|
2862
|
-
- Component library mixing (e.g., "don't mix MUI and shadcn")
|
|
2863
|
-
- Tailwind patterns (e.g., "no arbitrary values")
|
|
2864
|
-
|
|
2865
|
-
### 3. Generate Rule Files
|
|
2866
|
-
|
|
2867
|
-
Create TypeScript ESLint rules in \`.uilint/rules/\`:
|
|
2868
|
-
|
|
2869
|
-
\`\`\`typescript
|
|
2870
|
-
// .uilint/rules/prefer-shadcn-button.ts
|
|
2871
|
-
import { createRule } from 'uilint-eslint';
|
|
2872
|
-
|
|
2873
|
-
export default createRule({
|
|
2874
|
-
name: 'prefer-shadcn-button',
|
|
2875
|
-
meta: {
|
|
2876
|
-
type: 'problem',
|
|
2877
|
-
docs: { description: 'Use Button from shadcn instead of raw <button>' },
|
|
2878
|
-
messages: {
|
|
2879
|
-
preferButton: 'Use <Button> from @/components/ui/button instead of raw <button>',
|
|
2880
|
-
},
|
|
2881
|
-
schema: [],
|
|
2882
|
-
},
|
|
2883
|
-
defaultOptions: [],
|
|
2884
|
-
create(context) {
|
|
2885
|
-
return {
|
|
2886
|
-
JSXOpeningElement(node) {
|
|
2887
|
-
if (node.name.type === 'JSXIdentifier' && node.name.name === 'button') {
|
|
2888
|
-
context.report({ node, messageId: 'preferButton' });
|
|
2889
|
-
}
|
|
2890
|
-
},
|
|
2891
|
-
};
|
|
2892
|
-
},
|
|
2893
|
-
});
|
|
2894
|
-
\`\`\`
|
|
2895
|
-
|
|
2896
|
-
### 4. Generate ESLint Config
|
|
2897
|
-
|
|
2898
|
-
Create or update \`eslint.config.js\` to include the generated rules:
|
|
2899
|
-
|
|
2900
|
-
\`\`\`javascript
|
|
2901
|
-
import uilint from 'uilint-eslint';
|
|
2902
|
-
import preferShadcnButton from './.uilint/rules/prefer-shadcn-button.js';
|
|
2903
|
-
|
|
2904
|
-
export default [
|
|
2905
|
-
uilint.configs.recommended,
|
|
2906
|
-
{
|
|
2907
|
-
plugins: {
|
|
2908
|
-
'uilint-custom': {
|
|
2909
|
-
rules: {
|
|
2910
|
-
'prefer-shadcn-button': preferShadcnButton,
|
|
2911
|
-
},
|
|
2912
|
-
},
|
|
2913
|
-
},
|
|
2914
|
-
rules: {
|
|
2915
|
-
'uilint-custom/prefer-shadcn-button': 'error',
|
|
2916
|
-
},
|
|
2917
|
-
},
|
|
2918
|
-
];
|
|
2919
|
-
\`\`\`
|
|
2920
|
-
|
|
2921
|
-
## Output
|
|
2922
|
-
|
|
2923
|
-
Generate in \`.uilint/rules/\`:
|
|
2924
|
-
- One TypeScript file per rule
|
|
2925
|
-
- An \`index.ts\` that exports all rules
|
|
2926
|
-
- Update instructions for \`eslint.config.js\`
|
|
2927
|
-
|
|
2928
|
-
## Guidelines
|
|
2929
|
-
|
|
2930
|
-
- **Focus on static analysis** - rules must work without runtime info
|
|
2931
|
-
- **Clear error messages** - tell devs exactly what to do
|
|
2932
|
-
- **No false positives** - better to miss issues than over-report
|
|
2933
|
-
- **Performance** - rules run on every file, keep them fast
|
|
2934
|
-
- **Minimal rules** - generate 3-5 high-impact rules, not dozens
|
|
2935
|
-
`;
|
|
2936
2633
|
|
|
2937
2634
|
// src/utils/skill-loader.ts
|
|
2938
2635
|
import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2, existsSync as existsSync10 } from "fs";
|
|
@@ -3148,106 +2845,18 @@ function toInstallSpecifier(pkgName) {
|
|
|
3148
2845
|
if (range.startsWith("link:")) return pkgName;
|
|
3149
2846
|
return `${pkgName}@${range}`;
|
|
3150
2847
|
}
|
|
3151
|
-
function mergeHooksConfig(existing, ours) {
|
|
3152
|
-
const result = { ...existing };
|
|
3153
|
-
for (const [hookName, hookArray] of Object.entries(result.hooks)) {
|
|
3154
|
-
if (!Array.isArray(hookArray)) continue;
|
|
3155
|
-
result.hooks[hookName] = hookArray.filter(
|
|
3156
|
-
(h) => !LEGACY_HOOK_COMMANDS.includes(h.command)
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3159
|
-
for (const [hookName, ourHooks] of Object.entries(ours.hooks)) {
|
|
3160
|
-
if (!Array.isArray(ourHooks)) continue;
|
|
3161
|
-
const existingHooks = result.hooks[hookName] || [];
|
|
3162
|
-
for (const ourHook of ourHooks) {
|
|
3163
|
-
const alreadyExists = existingHooks.some(
|
|
3164
|
-
(h) => h.command === ourHook.command
|
|
3165
|
-
);
|
|
3166
|
-
if (!alreadyExists) {
|
|
3167
|
-
existingHooks.push(ourHook);
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
result.hooks[hookName] = existingHooks;
|
|
3171
|
-
}
|
|
3172
|
-
return result;
|
|
3173
|
-
}
|
|
3174
2848
|
function createPlan(state, choices, options = {}) {
|
|
3175
2849
|
const actions = [];
|
|
3176
2850
|
const dependencies = [];
|
|
3177
2851
|
const { force = false } = options;
|
|
3178
2852
|
const { items } = choices;
|
|
3179
|
-
const needsCursorDir = items.includes("
|
|
2853
|
+
const needsCursorDir = items.includes("genstyleguide") || items.includes("skill");
|
|
3180
2854
|
if (needsCursorDir && !state.cursorDir.exists) {
|
|
3181
2855
|
actions.push({
|
|
3182
2856
|
type: "create_directory",
|
|
3183
2857
|
path: state.cursorDir.path
|
|
3184
2858
|
});
|
|
3185
2859
|
}
|
|
3186
|
-
if (items.includes("mcp")) {
|
|
3187
|
-
if (!state.mcp.exists || force) {
|
|
3188
|
-
actions.push({
|
|
3189
|
-
type: "create_file",
|
|
3190
|
-
path: state.mcp.path,
|
|
3191
|
-
content: JSON.stringify(MCP_CONFIG, null, 2)
|
|
3192
|
-
});
|
|
3193
|
-
} else if (choices.mcpMerge) {
|
|
3194
|
-
const merged = {
|
|
3195
|
-
mcpServers: {
|
|
3196
|
-
...state.mcp.config?.mcpServers || {},
|
|
3197
|
-
...MCP_CONFIG.mcpServers
|
|
3198
|
-
}
|
|
3199
|
-
};
|
|
3200
|
-
actions.push({
|
|
3201
|
-
type: "create_file",
|
|
3202
|
-
path: state.mcp.path,
|
|
3203
|
-
content: JSON.stringify(merged, null, 2)
|
|
3204
|
-
});
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
if (items.includes("hooks")) {
|
|
3208
|
-
const hooksDir = join11(state.cursorDir.path, "hooks");
|
|
3209
|
-
actions.push({
|
|
3210
|
-
type: "create_directory",
|
|
3211
|
-
path: hooksDir
|
|
3212
|
-
});
|
|
3213
|
-
for (const legacyPath of state.hooks.legacyPaths) {
|
|
3214
|
-
actions.push({
|
|
3215
|
-
type: "delete_file",
|
|
3216
|
-
path: legacyPath
|
|
3217
|
-
});
|
|
3218
|
-
}
|
|
3219
|
-
let finalHooksConfig;
|
|
3220
|
-
if (!state.hooks.exists || force) {
|
|
3221
|
-
finalHooksConfig = HOOKS_CONFIG;
|
|
3222
|
-
} else if (choices.hooksMerge && state.hooks.config) {
|
|
3223
|
-
finalHooksConfig = mergeHooksConfig(state.hooks.config, HOOKS_CONFIG);
|
|
3224
|
-
} else {
|
|
3225
|
-
finalHooksConfig = HOOKS_CONFIG;
|
|
3226
|
-
}
|
|
3227
|
-
actions.push({
|
|
3228
|
-
type: "create_file",
|
|
3229
|
-
path: state.hooks.path,
|
|
3230
|
-
content: JSON.stringify(finalHooksConfig, null, 2)
|
|
3231
|
-
});
|
|
3232
|
-
actions.push({
|
|
3233
|
-
type: "create_file",
|
|
3234
|
-
path: join11(hooksDir, "uilint-session-start.sh"),
|
|
3235
|
-
content: SESSION_START_SCRIPT,
|
|
3236
|
-
permissions: 493
|
|
3237
|
-
});
|
|
3238
|
-
actions.push({
|
|
3239
|
-
type: "create_file",
|
|
3240
|
-
path: join11(hooksDir, "uilint-track.sh"),
|
|
3241
|
-
content: TRACK_SCRIPT,
|
|
3242
|
-
permissions: 493
|
|
3243
|
-
});
|
|
3244
|
-
actions.push({
|
|
3245
|
-
type: "create_file",
|
|
3246
|
-
path: join11(hooksDir, "uilint-session-end.sh"),
|
|
3247
|
-
content: SESSION_END_SCRIPT,
|
|
3248
|
-
permissions: 493
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
2860
|
if (items.includes("genstyleguide")) {
|
|
3252
2861
|
const commandsDir = join11(state.cursorDir.path, "commands");
|
|
3253
2862
|
actions.push({
|
|
@@ -3260,18 +2869,6 @@ function createPlan(state, choices, options = {}) {
|
|
|
3260
2869
|
content: GENSTYLEGUIDE_COMMAND_MD
|
|
3261
2870
|
});
|
|
3262
2871
|
}
|
|
3263
|
-
if (items.includes("genrules")) {
|
|
3264
|
-
const commandsDir = join11(state.cursorDir.path, "commands");
|
|
3265
|
-
actions.push({
|
|
3266
|
-
type: "create_directory",
|
|
3267
|
-
path: commandsDir
|
|
3268
|
-
});
|
|
3269
|
-
actions.push({
|
|
3270
|
-
type: "create_file",
|
|
3271
|
-
path: join11(commandsDir, "genrules.md"),
|
|
3272
|
-
content: GENRULES_COMMAND_MD
|
|
3273
|
-
});
|
|
3274
|
-
}
|
|
3275
2872
|
if (items.includes("skill")) {
|
|
3276
2873
|
const skillsDir = join11(state.cursorDir.path, "skills");
|
|
3277
2874
|
actions.push({
|
|
@@ -3472,80 +3069,52 @@ function walkAst(node, visit) {
|
|
|
3472
3069
|
}
|
|
3473
3070
|
}
|
|
3474
3071
|
}
|
|
3475
|
-
function
|
|
3476
|
-
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3477
|
-
const existing = findImportDeclaration(program2, from);
|
|
3478
|
-
if (existing) {
|
|
3479
|
-
const has = (existing.specifiers ?? []).some(
|
|
3480
|
-
(s) => s?.type === "ImportSpecifier" && (s.imported?.name === name || s.imported?.value === name)
|
|
3481
|
-
);
|
|
3482
|
-
if (has) return { changed: false };
|
|
3483
|
-
const spec = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0]?.specifiers?.[0];
|
|
3484
|
-
if (!spec) return { changed: false };
|
|
3485
|
-
existing.specifiers = [...existing.specifiers ?? [], spec];
|
|
3486
|
-
return { changed: true };
|
|
3487
|
-
}
|
|
3488
|
-
const importDecl = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0];
|
|
3489
|
-
if (!importDecl) return { changed: false };
|
|
3490
|
-
const body = program2.body ?? [];
|
|
3491
|
-
let insertAt = 0;
|
|
3492
|
-
while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
|
|
3493
|
-
insertAt++;
|
|
3494
|
-
}
|
|
3495
|
-
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
3496
|
-
insertAt++;
|
|
3497
|
-
}
|
|
3498
|
-
program2.body.splice(insertAt, 0, importDecl);
|
|
3499
|
-
return { changed: true };
|
|
3500
|
-
}
|
|
3501
|
-
function hasUILintProviderJsx(program2) {
|
|
3072
|
+
function hasUILintDevtoolsJsx(program2) {
|
|
3502
3073
|
let found = false;
|
|
3503
3074
|
walkAst(program2, (node) => {
|
|
3504
3075
|
if (found) return;
|
|
3505
3076
|
if (node.type !== "JSXElement") return;
|
|
3506
3077
|
const name = node.openingElement?.name;
|
|
3507
|
-
if (name?.type === "JSXIdentifier"
|
|
3508
|
-
|
|
3078
|
+
if (name?.type === "JSXIdentifier") {
|
|
3079
|
+
if (name.name === "UILintProvider" || name.name === "uilint-devtools") {
|
|
3080
|
+
found = true;
|
|
3081
|
+
}
|
|
3509
3082
|
}
|
|
3510
3083
|
});
|
|
3511
3084
|
return found;
|
|
3512
3085
|
}
|
|
3513
|
-
function
|
|
3086
|
+
function addDevtoolsElementNextJs(program2) {
|
|
3514
3087
|
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3515
|
-
if (
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3088
|
+
if (hasUILintDevtoolsJsx(program2)) return { changed: false };
|
|
3089
|
+
const devtoolsMod = parseModule2(
|
|
3090
|
+
"const __uilint_devtools = (<uilint-devtools />);"
|
|
3518
3091
|
);
|
|
3519
|
-
const
|
|
3520
|
-
if (!
|
|
3092
|
+
const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
3093
|
+
if (!devtoolsJsx || devtoolsJsx.type !== "JSXElement")
|
|
3521
3094
|
return { changed: false };
|
|
3522
|
-
let
|
|
3095
|
+
let added = false;
|
|
3523
3096
|
walkAst(program2, (node) => {
|
|
3524
|
-
if (
|
|
3525
|
-
if (node.type
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3097
|
+
if (added) return;
|
|
3098
|
+
if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
|
|
3099
|
+
const children = node.children ?? [];
|
|
3100
|
+
const childrenIndex = children.findIndex(
|
|
3101
|
+
(child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
|
|
3102
|
+
);
|
|
3103
|
+
if (childrenIndex === -1) return;
|
|
3104
|
+
children.splice(childrenIndex + 1, 0, devtoolsJsx);
|
|
3105
|
+
added = true;
|
|
3530
3106
|
});
|
|
3531
|
-
if (!
|
|
3532
|
-
throw new Error("Could not find `{children}` in target file to
|
|
3107
|
+
if (!added) {
|
|
3108
|
+
throw new Error("Could not find `{children}` in target file to add devtools.");
|
|
3533
3109
|
}
|
|
3534
3110
|
return { changed: true };
|
|
3535
3111
|
}
|
|
3536
|
-
function
|
|
3112
|
+
function addDevtoolsElementVite(program2) {
|
|
3537
3113
|
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3538
|
-
if (
|
|
3539
|
-
|
|
3540
|
-
'const __uilint_provider = (<UILintProvider enabled={process.env.NODE_ENV !== "production"}></UILintProvider>);'
|
|
3541
|
-
);
|
|
3542
|
-
const providerJsx = providerMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
3543
|
-
if (!providerJsx || providerJsx.type !== "JSXElement")
|
|
3544
|
-
return { changed: false };
|
|
3545
|
-
providerJsx.children = providerJsx.children ?? [];
|
|
3546
|
-
let wrapped = false;
|
|
3114
|
+
if (hasUILintDevtoolsJsx(program2)) return { changed: false };
|
|
3115
|
+
let added = false;
|
|
3547
3116
|
walkAst(program2, (node) => {
|
|
3548
|
-
if (
|
|
3117
|
+
if (added) return;
|
|
3549
3118
|
if (node.type !== "CallExpression") return;
|
|
3550
3119
|
const callee = node.callee;
|
|
3551
3120
|
if (callee?.type !== "MemberExpression") return;
|
|
@@ -3555,17 +3124,44 @@ function wrapFirstRenderCallArgumentWithProvider(program2) {
|
|
|
3555
3124
|
const arg0 = node.arguments?.[0];
|
|
3556
3125
|
if (!arg0) return;
|
|
3557
3126
|
if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3127
|
+
const devtoolsMod = parseModule2(
|
|
3128
|
+
"const __uilint_devtools = (<uilint-devtools />);"
|
|
3129
|
+
);
|
|
3130
|
+
const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
3131
|
+
if (!devtoolsJsx) return;
|
|
3132
|
+
const fragmentMod = parseModule2(
|
|
3133
|
+
"const __fragment = (<></>);"
|
|
3134
|
+
);
|
|
3135
|
+
const fragmentJsx = fragmentMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
3136
|
+
if (!fragmentJsx) return;
|
|
3137
|
+
fragmentJsx.children = [arg0, devtoolsJsx];
|
|
3138
|
+
node.arguments[0] = fragmentJsx;
|
|
3139
|
+
added = true;
|
|
3561
3140
|
});
|
|
3562
|
-
if (!
|
|
3141
|
+
if (!added) {
|
|
3563
3142
|
throw new Error(
|
|
3564
|
-
"Could not find a `.render(<...>)` call to
|
|
3143
|
+
"Could not find a `.render(<...>)` call to add devtools. Expected a React entry like `createRoot(...).render(<App />)`."
|
|
3565
3144
|
);
|
|
3566
3145
|
}
|
|
3567
3146
|
return { changed: true };
|
|
3568
3147
|
}
|
|
3148
|
+
function ensureSideEffectImport(program2, from) {
|
|
3149
|
+
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3150
|
+
const existing = findImportDeclaration(program2, from);
|
|
3151
|
+
if (existing) return { changed: false };
|
|
3152
|
+
const importDecl = parseModule2(`import "${from}";`).$ast.body?.[0];
|
|
3153
|
+
if (!importDecl) return { changed: false };
|
|
3154
|
+
const body = program2.body ?? [];
|
|
3155
|
+
let insertAt = 0;
|
|
3156
|
+
while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
|
|
3157
|
+
insertAt++;
|
|
3158
|
+
}
|
|
3159
|
+
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
3160
|
+
insertAt++;
|
|
3161
|
+
}
|
|
3162
|
+
program2.body.splice(insertAt, 0, importDecl);
|
|
3163
|
+
return { changed: true };
|
|
3164
|
+
}
|
|
3569
3165
|
async function installReactUILintOverlay(opts) {
|
|
3570
3166
|
const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
|
|
3571
3167
|
if (!candidates.length) {
|
|
@@ -3590,17 +3186,15 @@ async function installReactUILintOverlay(opts) {
|
|
|
3590
3186
|
);
|
|
3591
3187
|
}
|
|
3592
3188
|
const program2 = mod.$ast;
|
|
3593
|
-
const
|
|
3189
|
+
const hasDevtoolsImport = !!findImportDeclaration(program2, "uilint-react/devtools");
|
|
3190
|
+
const hasOldImport = !!findImportDeclaration(program2, "uilint-react");
|
|
3191
|
+
const alreadyConfigured = (hasDevtoolsImport || hasOldImport) && hasUILintDevtoolsJsx(program2);
|
|
3594
3192
|
let changed = false;
|
|
3595
|
-
const importRes =
|
|
3596
|
-
program2,
|
|
3597
|
-
"uilint-react",
|
|
3598
|
-
"UILintProvider"
|
|
3599
|
-
);
|
|
3193
|
+
const importRes = ensureSideEffectImport(program2, "uilint-react/devtools");
|
|
3600
3194
|
if (importRes.changed) changed = true;
|
|
3601
3195
|
const mode = opts.mode ?? "next";
|
|
3602
|
-
const
|
|
3603
|
-
if (
|
|
3196
|
+
const addRes = mode === "vite" ? addDevtoolsElementVite(program2) : addDevtoolsElementNextJs(program2);
|
|
3197
|
+
if (addRes.changed) changed = true;
|
|
3604
3198
|
const updated = changed ? generateCode2(mod).code : original;
|
|
3605
3199
|
const modified = updated !== original;
|
|
3606
3200
|
if (modified) {
|
|
@@ -4605,7 +4199,7 @@ async function executeInjectReact(action, options) {
|
|
|
4605
4199
|
return {
|
|
4606
4200
|
action,
|
|
4607
4201
|
success: true,
|
|
4608
|
-
wouldDo: `Inject
|
|
4202
|
+
wouldDo: `Inject <uilint-devtools /> into React app: ${action.projectPath}`
|
|
4609
4203
|
};
|
|
4610
4204
|
}
|
|
4611
4205
|
const result = await installReactUILintOverlay({
|
|
@@ -4796,10 +4390,7 @@ async function execute(plan, options = {}) {
|
|
|
4796
4390
|
if (!result.success) continue;
|
|
4797
4391
|
const { action } = result;
|
|
4798
4392
|
if (action.type === "create_file") {
|
|
4799
|
-
if (action.path.includes("mcp.json")) items.push("mcp");
|
|
4800
|
-
if (action.path.includes("hooks.json")) items.push("hooks");
|
|
4801
4393
|
if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
|
|
4802
|
-
if (action.path.includes("genrules.md")) items.push("genrules");
|
|
4803
4394
|
if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
|
|
4804
4395
|
}
|
|
4805
4396
|
if (action.type === "inject_eslint") items.push("eslint");
|
|
@@ -4838,33 +4429,18 @@ var cliPrompter = {
|
|
|
4838
4429
|
{
|
|
4839
4430
|
value: "next",
|
|
4840
4431
|
label: "UI overlay",
|
|
4841
|
-
hint: "Installs routes +
|
|
4432
|
+
hint: "Installs routes + devtools (Alt+Click to inspect)"
|
|
4842
4433
|
},
|
|
4843
4434
|
{
|
|
4844
4435
|
value: "vite",
|
|
4845
4436
|
label: "UI overlay (Vite)",
|
|
4846
|
-
hint: "Installs jsx-loc-plugin +
|
|
4437
|
+
hint: "Installs jsx-loc-plugin + devtools (Alt+Click to inspect)"
|
|
4847
4438
|
},
|
|
4848
4439
|
{
|
|
4849
4440
|
value: "genstyleguide",
|
|
4850
4441
|
label: "/genstyleguide command",
|
|
4851
4442
|
hint: "Adds .cursor/commands/genstyleguide.md"
|
|
4852
4443
|
},
|
|
4853
|
-
{
|
|
4854
|
-
value: "mcp",
|
|
4855
|
-
label: "MCP Server",
|
|
4856
|
-
hint: "Recommended - works with any MCP-compatible agent"
|
|
4857
|
-
},
|
|
4858
|
-
{
|
|
4859
|
-
value: "hooks",
|
|
4860
|
-
label: "Cursor Hooks",
|
|
4861
|
-
hint: "Auto-validates UI files when the agent stops"
|
|
4862
|
-
},
|
|
4863
|
-
{
|
|
4864
|
-
value: "genrules",
|
|
4865
|
-
label: "/genrules command",
|
|
4866
|
-
hint: "Adds .cursor/commands/genrules.md for ESLint rule generation"
|
|
4867
|
-
},
|
|
4868
4444
|
{
|
|
4869
4445
|
value: "skill",
|
|
4870
4446
|
label: "UI Consistency Agent Skill",
|
|
@@ -4875,22 +4451,6 @@ var cliPrompter = {
|
|
|
4875
4451
|
initialValues: ["eslint", "next", "genstyleguide", "skill"]
|
|
4876
4452
|
});
|
|
4877
4453
|
},
|
|
4878
|
-
async confirmMcpMerge() {
|
|
4879
|
-
return confirm2({
|
|
4880
|
-
message: `${pc.dim(
|
|
4881
|
-
".cursor/mcp.json"
|
|
4882
|
-
)} already exists. Merge UILint config?`,
|
|
4883
|
-
initialValue: true
|
|
4884
|
-
});
|
|
4885
|
-
},
|
|
4886
|
-
async confirmHooksMerge() {
|
|
4887
|
-
return confirm2({
|
|
4888
|
-
message: `${pc.dim(
|
|
4889
|
-
".cursor/hooks.json"
|
|
4890
|
-
)} already exists. Merge UILint hooks?`,
|
|
4891
|
-
initialValue: true
|
|
4892
|
-
});
|
|
4893
|
-
},
|
|
4894
4454
|
async selectNextApp(apps) {
|
|
4895
4455
|
const chosen = await select2({
|
|
4896
4456
|
message: "Which Next.js App Router project should UILint install into?",
|
|
@@ -5069,33 +4629,16 @@ async function promptForField(field, ruleName) {
|
|
|
5069
4629
|
}
|
|
5070
4630
|
async function gatherChoices(state, options, prompter) {
|
|
5071
4631
|
let items;
|
|
5072
|
-
const hasExplicitFlags = options.
|
|
4632
|
+
const hasExplicitFlags = options.genstyleguide !== void 0 || options.skill !== void 0 || options.routes !== void 0 || options.react !== void 0;
|
|
5073
4633
|
if (hasExplicitFlags || options.eslint) {
|
|
5074
4634
|
items = [];
|
|
5075
|
-
if (options.mcp) items.push("mcp");
|
|
5076
|
-
if (options.hooks) items.push("hooks");
|
|
5077
4635
|
if (options.genstyleguide) items.push("genstyleguide");
|
|
5078
|
-
if (options.genrules) items.push("genrules");
|
|
5079
4636
|
if (options.skill) items.push("skill");
|
|
5080
4637
|
if (options.routes || options.react) items.push("next");
|
|
5081
4638
|
if (options.eslint) items.push("eslint");
|
|
5082
|
-
} else if (options.mode) {
|
|
5083
|
-
items = [];
|
|
5084
|
-
if (options.mode === "mcp" || options.mode === "both") items.push("mcp");
|
|
5085
|
-
if (options.mode === "hooks" || options.mode === "both")
|
|
5086
|
-
items.push("hooks");
|
|
5087
|
-
items.push("genstyleguide");
|
|
5088
4639
|
} else {
|
|
5089
4640
|
items = await prompter.selectInstallItems();
|
|
5090
4641
|
}
|
|
5091
|
-
let mcpMerge = true;
|
|
5092
|
-
if (items.includes("mcp") && state.mcp.exists && !options.force) {
|
|
5093
|
-
mcpMerge = await prompter.confirmMcpMerge();
|
|
5094
|
-
}
|
|
5095
|
-
let hooksMerge = true;
|
|
5096
|
-
if (items.includes("hooks") && state.hooks.exists && !options.force) {
|
|
5097
|
-
hooksMerge = await prompter.confirmHooksMerge();
|
|
5098
|
-
}
|
|
5099
4642
|
let nextChoices;
|
|
5100
4643
|
if (items.includes("next")) {
|
|
5101
4644
|
if (state.nextApps.length === 0) {
|
|
@@ -5170,8 +4713,6 @@ async function gatherChoices(state, options, prompter) {
|
|
|
5170
4713
|
}
|
|
5171
4714
|
return {
|
|
5172
4715
|
items,
|
|
5173
|
-
mcpMerge,
|
|
5174
|
-
hooksMerge,
|
|
5175
4716
|
next: nextChoices,
|
|
5176
4717
|
vite: viteChoices,
|
|
5177
4718
|
eslint: eslintChoices
|
|
@@ -5202,23 +4743,11 @@ async function configureRuleOptions(rules, prompter) {
|
|
|
5202
4743
|
function displayResults(result) {
|
|
5203
4744
|
const { summary } = result;
|
|
5204
4745
|
const installedItems = [];
|
|
5205
|
-
if (summary.installedItems.includes("mcp")) {
|
|
5206
|
-
installedItems.push(`${pc.cyan("MCP Server")} \u2192 .cursor/mcp.json`);
|
|
5207
|
-
}
|
|
5208
|
-
if (summary.installedItems.includes("hooks")) {
|
|
5209
|
-
installedItems.push(`${pc.cyan("Hooks")} \u2192 .cursor/hooks.json`);
|
|
5210
|
-
installedItems.push(` ${pc.dim("\u251C")} uilint-session-start.sh`);
|
|
5211
|
-
installedItems.push(` ${pc.dim("\u251C")} uilint-track.sh`);
|
|
5212
|
-
installedItems.push(` ${pc.dim("\u2514")} uilint-session-end.sh`);
|
|
5213
|
-
}
|
|
5214
4746
|
if (summary.installedItems.includes("genstyleguide")) {
|
|
5215
4747
|
installedItems.push(
|
|
5216
4748
|
`${pc.cyan("Command")} \u2192 .cursor/commands/genstyleguide.md`
|
|
5217
4749
|
);
|
|
5218
4750
|
}
|
|
5219
|
-
if (summary.installedItems.includes("genrules")) {
|
|
5220
|
-
installedItems.push(`${pc.cyan("Command")} \u2192 .cursor/commands/genrules.md`);
|
|
5221
|
-
}
|
|
5222
4751
|
if (summary.nextApp) {
|
|
5223
4752
|
installedItems.push(
|
|
5224
4753
|
`${pc.cyan("Next Routes")} \u2192 ${pc.dim(
|
|
@@ -5226,7 +4755,7 @@ function displayResults(result) {
|
|
|
5226
4755
|
)}`
|
|
5227
4756
|
);
|
|
5228
4757
|
installedItems.push(
|
|
5229
|
-
`${pc.cyan("Next
|
|
4758
|
+
`${pc.cyan("Next Devtools")} \u2192 ${pc.dim("<uilint-devtools /> injected")}`
|
|
5230
4759
|
);
|
|
5231
4760
|
installedItems.push(
|
|
5232
4761
|
`${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
|
|
@@ -5236,7 +4765,7 @@ function displayResults(result) {
|
|
|
5236
4765
|
}
|
|
5237
4766
|
if (summary.viteApp) {
|
|
5238
4767
|
installedItems.push(
|
|
5239
|
-
`${pc.cyan("Vite
|
|
4768
|
+
`${pc.cyan("Vite Devtools")} \u2192 ${pc.dim("<uilint-devtools /> injected")}`
|
|
5240
4769
|
);
|
|
5241
4770
|
installedItems.push(
|
|
5242
4771
|
`${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
|
|
@@ -5274,15 +4803,9 @@ function displayResults(result) {
|
|
|
5274
4803
|
if (!hasStyleguide) {
|
|
5275
4804
|
steps.push(`Create a styleguide: ${pc.cyan("/genstyleguide")}`);
|
|
5276
4805
|
}
|
|
5277
|
-
if (summary.installedItems.includes("
|
|
4806
|
+
if (summary.installedItems.includes("genstyleguide")) {
|
|
5278
4807
|
steps.push("Restart Cursor to load the new configuration");
|
|
5279
4808
|
}
|
|
5280
|
-
if (summary.installedItems.includes("mcp")) {
|
|
5281
|
-
steps.push(`The MCP server exposes: ${pc.dim("scan_file, scan_snippet")}`);
|
|
5282
|
-
}
|
|
5283
|
-
if (summary.installedItems.includes("hooks")) {
|
|
5284
|
-
steps.push("Hooks will auto-validate UI files when the agent stops");
|
|
5285
|
-
}
|
|
5286
4809
|
if (summary.nextApp) {
|
|
5287
4810
|
steps.push(
|
|
5288
4811
|
"Run your Next.js dev server - use Alt+Click on any element to inspect"
|
|
@@ -6180,12 +5703,12 @@ async function serve(options) {
|
|
|
6180
5703
|
`UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
|
|
6181
5704
|
);
|
|
6182
5705
|
logInfo("Press Ctrl+C to stop");
|
|
6183
|
-
await new Promise((
|
|
5706
|
+
await new Promise((resolve7) => {
|
|
6184
5707
|
process.on("SIGINT", () => {
|
|
6185
5708
|
logInfo("Shutting down...");
|
|
6186
5709
|
wss.close();
|
|
6187
5710
|
fileWatcher?.close();
|
|
6188
|
-
|
|
5711
|
+
resolve7();
|
|
6189
5712
|
});
|
|
6190
5713
|
});
|
|
6191
5714
|
}
|
|
@@ -6673,184 +6196,9 @@ async function vision(options) {
|
|
|
6673
6196
|
await flushLangfuse();
|
|
6674
6197
|
}
|
|
6675
6198
|
|
|
6676
|
-
// src/commands/session.ts
|
|
6677
|
-
import { existsSync as existsSync20, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
|
|
6678
|
-
import { basename, dirname as dirname13, resolve as resolve7 } from "path";
|
|
6679
|
-
import { createStyleSummary as createStyleSummary3 } from "uilint-core";
|
|
6680
|
-
import {
|
|
6681
|
-
ensureOllamaReady as ensureOllamaReady7,
|
|
6682
|
-
parseCLIInput as parseCLIInput2,
|
|
6683
|
-
readStyleGuideFromProject as readStyleGuideFromProject2,
|
|
6684
|
-
readTailwindThemeTokens as readTailwindThemeTokens3
|
|
6685
|
-
} from "uilint-core/node";
|
|
6686
|
-
var SESSION_FILE = "/tmp/uilint-session.json";
|
|
6687
|
-
var UI_FILE_EXTENSIONS = [".tsx", ".jsx", ".css", ".scss", ".module.css"];
|
|
6688
|
-
function readSession() {
|
|
6689
|
-
if (!existsSync20(SESSION_FILE)) {
|
|
6690
|
-
return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
6691
|
-
}
|
|
6692
|
-
try {
|
|
6693
|
-
const content = readFileSync14(SESSION_FILE, "utf-8");
|
|
6694
|
-
return JSON.parse(content);
|
|
6695
|
-
} catch {
|
|
6696
|
-
return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
6697
|
-
}
|
|
6698
|
-
}
|
|
6699
|
-
function writeSession(state) {
|
|
6700
|
-
writeFileSync10(SESSION_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
6701
|
-
}
|
|
6702
|
-
function isUIFile(filePath) {
|
|
6703
|
-
return UI_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
6704
|
-
}
|
|
6705
|
-
function isScannableMarkupFile(filePath) {
|
|
6706
|
-
return [".tsx", ".jsx", ".html", ".htm"].some(
|
|
6707
|
-
(ext) => filePath.endsWith(ext)
|
|
6708
|
-
);
|
|
6709
|
-
}
|
|
6710
|
-
async function sessionClear() {
|
|
6711
|
-
if (existsSync20(SESSION_FILE)) {
|
|
6712
|
-
unlinkSync2(SESSION_FILE);
|
|
6713
|
-
}
|
|
6714
|
-
console.log(JSON.stringify({ cleared: true }));
|
|
6715
|
-
}
|
|
6716
|
-
async function sessionTrack(filePath) {
|
|
6717
|
-
if (!isUIFile(filePath)) {
|
|
6718
|
-
console.log(
|
|
6719
|
-
JSON.stringify({
|
|
6720
|
-
tracked: false,
|
|
6721
|
-
reason: "not_ui_file",
|
|
6722
|
-
file: filePath,
|
|
6723
|
-
message: `Skipped non-UI file: ${basename(filePath)}`
|
|
6724
|
-
})
|
|
6725
|
-
);
|
|
6726
|
-
return;
|
|
6727
|
-
}
|
|
6728
|
-
const session = readSession();
|
|
6729
|
-
const wasAlreadyTracked = session.files.includes(filePath);
|
|
6730
|
-
if (!wasAlreadyTracked) {
|
|
6731
|
-
session.files.push(filePath);
|
|
6732
|
-
writeSession(session);
|
|
6733
|
-
}
|
|
6734
|
-
console.log(
|
|
6735
|
-
JSON.stringify({
|
|
6736
|
-
tracked: true,
|
|
6737
|
-
file: filePath,
|
|
6738
|
-
total: session.files.length,
|
|
6739
|
-
newlyAdded: !wasAlreadyTracked,
|
|
6740
|
-
message: wasAlreadyTracked ? `Already tracking: ${basename(filePath)} (${session.files.length} files total)` : `Now tracking: ${basename(filePath)} (${session.files.length} files total)`
|
|
6741
|
-
})
|
|
6742
|
-
);
|
|
6743
|
-
}
|
|
6744
|
-
async function sessionScan(options = {}) {
|
|
6745
|
-
const session = readSession();
|
|
6746
|
-
if (session.files.length === 0) {
|
|
6747
|
-
if (options.hookFormat) {
|
|
6748
|
-
console.log("{}");
|
|
6749
|
-
} else {
|
|
6750
|
-
const result = {
|
|
6751
|
-
totalFiles: 0,
|
|
6752
|
-
filesWithIssues: 0,
|
|
6753
|
-
results: [],
|
|
6754
|
-
followupMessage: null
|
|
6755
|
-
};
|
|
6756
|
-
console.log(JSON.stringify(result));
|
|
6757
|
-
}
|
|
6758
|
-
return;
|
|
6759
|
-
}
|
|
6760
|
-
const projectPath = process.cwd();
|
|
6761
|
-
let styleGuide;
|
|
6762
|
-
try {
|
|
6763
|
-
styleGuide = await readStyleGuideFromProject2(projectPath);
|
|
6764
|
-
} catch {
|
|
6765
|
-
if (options.hookFormat) {
|
|
6766
|
-
console.log("{}");
|
|
6767
|
-
} else {
|
|
6768
|
-
const result = {
|
|
6769
|
-
totalFiles: session.files.length,
|
|
6770
|
-
filesWithIssues: 0,
|
|
6771
|
-
results: [],
|
|
6772
|
-
followupMessage: null
|
|
6773
|
-
};
|
|
6774
|
-
console.log(JSON.stringify(result));
|
|
6775
|
-
}
|
|
6776
|
-
return;
|
|
6777
|
-
}
|
|
6778
|
-
await ensureOllamaReady7();
|
|
6779
|
-
const client = await createLLMClient({});
|
|
6780
|
-
const results = [];
|
|
6781
|
-
for (const filePath of session.files) {
|
|
6782
|
-
if (!existsSync20(filePath)) continue;
|
|
6783
|
-
if (!isScannableMarkupFile(filePath)) continue;
|
|
6784
|
-
try {
|
|
6785
|
-
const absolutePath = resolve7(process.cwd(), filePath);
|
|
6786
|
-
const htmlLike = readFileSync14(filePath, "utf-8");
|
|
6787
|
-
const snapshot = parseCLIInput2(htmlLike);
|
|
6788
|
-
const tailwindSearchDir = dirname13(absolutePath);
|
|
6789
|
-
const tailwindTheme = readTailwindThemeTokens3(tailwindSearchDir);
|
|
6790
|
-
const styleSummary = createStyleSummary3(snapshot.styles, {
|
|
6791
|
-
html: snapshot.html,
|
|
6792
|
-
tailwindTheme
|
|
6793
|
-
});
|
|
6794
|
-
const analysis = await client.analyzeStyles(styleSummary, styleGuide);
|
|
6795
|
-
results.push({
|
|
6796
|
-
file: filePath,
|
|
6797
|
-
issues: analysis.issues
|
|
6798
|
-
});
|
|
6799
|
-
} catch {
|
|
6800
|
-
continue;
|
|
6801
|
-
}
|
|
6802
|
-
}
|
|
6803
|
-
const filesWithIssues = results.filter((r) => r.issues.length > 0);
|
|
6804
|
-
let followupMessage = null;
|
|
6805
|
-
if (filesWithIssues.length > 0) {
|
|
6806
|
-
const issueLines = [];
|
|
6807
|
-
for (const fileResult of filesWithIssues) {
|
|
6808
|
-
const fileName = basename(fileResult.file);
|
|
6809
|
-
for (const issue of fileResult.issues) {
|
|
6810
|
-
const type = issue.type?.toUpperCase?.() ?? "ISSUE";
|
|
6811
|
-
const detail = issue.currentValue && issue.expectedValue ? ` (${issue.currentValue} \u2192 ${issue.expectedValue})` : issue.currentValue ? ` (${issue.currentValue})` : "";
|
|
6812
|
-
issueLines.push(`- ${fileName}: [${type}] ${issue.message}${detail}`);
|
|
6813
|
-
if (issue.suggestion) {
|
|
6814
|
-
issueLines.push(` Suggestion: ${issue.suggestion}`);
|
|
6815
|
-
}
|
|
6816
|
-
}
|
|
6817
|
-
}
|
|
6818
|
-
followupMessage = [
|
|
6819
|
-
`UILint scan found UI consistency issues in ${filesWithIssues.length} file(s):`,
|
|
6820
|
-
"",
|
|
6821
|
-
...issueLines,
|
|
6822
|
-
"",
|
|
6823
|
-
"See .uilint/styleguide.md for style rules. Please fix these issues."
|
|
6824
|
-
].join("\n");
|
|
6825
|
-
}
|
|
6826
|
-
if (options.hookFormat) {
|
|
6827
|
-
if (followupMessage) {
|
|
6828
|
-
console.log(JSON.stringify({ followup_message: followupMessage }));
|
|
6829
|
-
} else {
|
|
6830
|
-
console.log("{}");
|
|
6831
|
-
}
|
|
6832
|
-
} else {
|
|
6833
|
-
const result = {
|
|
6834
|
-
totalFiles: results.length,
|
|
6835
|
-
filesWithIssues: filesWithIssues.length,
|
|
6836
|
-
results,
|
|
6837
|
-
followupMessage
|
|
6838
|
-
};
|
|
6839
|
-
console.log(JSON.stringify(result));
|
|
6840
|
-
}
|
|
6841
|
-
if (existsSync20(SESSION_FILE)) {
|
|
6842
|
-
unlinkSync2(SESSION_FILE);
|
|
6843
|
-
}
|
|
6844
|
-
await flushLangfuse();
|
|
6845
|
-
}
|
|
6846
|
-
async function sessionList() {
|
|
6847
|
-
const session = readSession();
|
|
6848
|
-
console.log(JSON.stringify(session));
|
|
6849
|
-
}
|
|
6850
|
-
|
|
6851
6199
|
// src/index.ts
|
|
6852
|
-
import { readFileSync as
|
|
6853
|
-
import { dirname as
|
|
6200
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
6201
|
+
import { dirname as dirname13, join as join20 } from "path";
|
|
6854
6202
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6855
6203
|
function assertNodeVersion(minMajor) {
|
|
6856
6204
|
const ver = process.versions.node || "";
|
|
@@ -6867,9 +6215,9 @@ assertNodeVersion(20);
|
|
|
6867
6215
|
var program = new Command();
|
|
6868
6216
|
function getCLIVersion2() {
|
|
6869
6217
|
try {
|
|
6870
|
-
const __dirname3 =
|
|
6218
|
+
const __dirname3 = dirname13(fileURLToPath4(import.meta.url));
|
|
6871
6219
|
const pkgPath = join20(__dirname3, "..", "package.json");
|
|
6872
|
-
const pkg = JSON.parse(
|
|
6220
|
+
const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
6873
6221
|
return pkg.version || "0.0.0";
|
|
6874
6222
|
} catch {
|
|
6875
6223
|
return "0.0.0";
|
|
@@ -6942,26 +6290,16 @@ program.command("update").description("Update existing style guide with new styl
|
|
|
6942
6290
|
llm: options.llm
|
|
6943
6291
|
});
|
|
6944
6292
|
});
|
|
6945
|
-
program.command("install").description("Install UILint integration
|
|
6946
|
-
"--genrules",
|
|
6947
|
-
"Install /genrules Cursor command for ESLint rule generation"
|
|
6948
|
-
).option("--eslint", "Install uilint-eslint plugin and configure ESLint").option(
|
|
6293
|
+
program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").option("--genstyleguide", "Install /genstyleguide Cursor command").option("--eslint", "Install uilint-eslint plugin and configure ESLint").option(
|
|
6949
6294
|
"--routes",
|
|
6950
6295
|
"Back-compat: install Next.js overlay (routes + deps + inject)"
|
|
6951
6296
|
).option(
|
|
6952
6297
|
"--react",
|
|
6953
6298
|
"Back-compat: install Next.js overlay (routes + deps + inject)"
|
|
6954
|
-
).option(
|
|
6955
|
-
"--mode <mode>",
|
|
6956
|
-
"Integration mode: mcp, hooks, or both (skips interactive prompt)"
|
|
6957
6299
|
).action(async (options) => {
|
|
6958
6300
|
await install({
|
|
6959
6301
|
force: options.force,
|
|
6960
|
-
mode: options.mode,
|
|
6961
|
-
mcp: options.mcp,
|
|
6962
|
-
hooks: options.hooks,
|
|
6963
6302
|
genstyleguide: options.genstyleguide,
|
|
6964
|
-
genrules: options.genrules,
|
|
6965
6303
|
eslint: options.eslint,
|
|
6966
6304
|
routes: options.routes,
|
|
6967
6305
|
react: options.react
|
|
@@ -7006,20 +6344,5 @@ program.command("vision").description("Analyze a screenshot with Ollama vision m
|
|
|
7006
6344
|
debugDump: options.debugDump
|
|
7007
6345
|
});
|
|
7008
6346
|
});
|
|
7009
|
-
var sessionCmd = program.command("session").description(
|
|
7010
|
-
"Manage file tracking for agentic sessions (used by Cursor hooks)"
|
|
7011
|
-
);
|
|
7012
|
-
sessionCmd.command("clear").description("Clear tracked files (called at start of agent turn)").action(async () => {
|
|
7013
|
-
await sessionClear();
|
|
7014
|
-
});
|
|
7015
|
-
sessionCmd.command("track <file>").description("Track a file edit (called on each file edit)").action(async (file) => {
|
|
7016
|
-
await sessionTrack(file);
|
|
7017
|
-
});
|
|
7018
|
-
sessionCmd.command("scan").description("Scan all tracked markup files (called on agent stop)").option("--hook", "Output in Cursor hook format (followup_message JSON only)").action(async (options) => {
|
|
7019
|
-
await sessionScan({ hookFormat: options.hook });
|
|
7020
|
-
});
|
|
7021
|
-
sessionCmd.command("list").description("List tracked files (for debugging)").action(async () => {
|
|
7022
|
-
await sessionList();
|
|
7023
|
-
});
|
|
7024
6347
|
program.parse();
|
|
7025
6348
|
//# sourceMappingURL=index.js.map
|