uilint 0.2.60 → 0.2.61

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.
@@ -2897,8 +2897,183 @@ var skillInstaller = {
2897
2897
  };
2898
2898
 
2899
2899
  // src/commands/init/installers/eslint.ts
2900
- import { join as join13 } from "path";
2900
+ import { join as join14 } from "path";
2901
2901
  import { ruleRegistry, getRulesByCategory, getCategoryMeta } from "uilint-eslint";
2902
+
2903
+ // src/commands/init/installers/ai-hooks.ts
2904
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
2905
+ import { join as join13 } from "path";
2906
+ var POST_EDIT_HOOK_SCRIPT = `#!/bin/bash
2907
+ # AI Editor Hook: Run ESLint on edited files
2908
+ # Triggered after file edits to provide lint feedback
2909
+ #
2910
+ # Output: JSON with lint errors for the AI to see
2911
+
2912
+ # Read JSON input from stdin
2913
+ input=$(cat)
2914
+ file_path=$(echo "$input" | jq -r '.tool_input.file_path // .file_path // empty')
2915
+
2916
+ # Exit if no file path
2917
+ if [[ -z "$file_path" ]]; then
2918
+ exit 0
2919
+ fi
2920
+
2921
+ # Only lint TypeScript/JavaScript files
2922
+ if [[ "$file_path" =~ \\.(ts|tsx|js|jsx)$ ]]; then
2923
+ # Find the package directory (look for eslint.config.ts or package.json)
2924
+ dir=$(dirname "$file_path")
2925
+ while [[ "$dir" != "/" ]]; do
2926
+ if [[ -f "$dir/eslint.config.ts" ]] || [[ -f "$dir/eslint.config.js" ]] || [[ -f "$dir/eslint.config.mjs" ]]; then
2927
+ break
2928
+ fi
2929
+ dir=$(dirname "$dir")
2930
+ done
2931
+
2932
+ # If no config found, exit
2933
+ if [[ "$dir" == "/" ]]; then
2934
+ exit 0
2935
+ fi
2936
+
2937
+ # Run ESLint with --fix first (auto-fix what we can), suppress output
2938
+ (cd "$dir" && npx eslint "$file_path" --fix >/dev/null 2>&1) || true
2939
+
2940
+ # Then report ALL remaining issues
2941
+ remaining=$( (cd "$dir" && npx eslint "$file_path" --format stylish 2>/dev/null) | grep -E "^\\s+[0-9]+:[0-9]+" | head -10)
2942
+
2943
+ if [[ -n "$remaining" ]]; then
2944
+ # Output JSON so the AI sees the lint errors
2945
+ jq -n --arg issues "$remaining" '{"additionalContext": ("ESLint errors - fix these:\\n" + $issues)}'
2946
+ # Exit with error code to signal lint issues
2947
+ exit 1
2948
+ fi
2949
+ fi
2950
+ `;
2951
+ var CLAUDE_HOOK_CONFIG = {
2952
+ hooks: {
2953
+ PostToolUse: [
2954
+ {
2955
+ matcher: "Edit|Write",
2956
+ hooks: [
2957
+ {
2958
+ type: "command",
2959
+ command: "bash .claude/hooks/post-edit.sh"
2960
+ }
2961
+ ]
2962
+ }
2963
+ ]
2964
+ }
2965
+ };
2966
+ var CURSOR_HOOK_CONFIG = {
2967
+ hooks: {
2968
+ afterFileEdit: [
2969
+ {
2970
+ command: "bash .cursor/hooks/post-edit.sh",
2971
+ filePattern: "**/*.{ts,tsx,js,jsx}"
2972
+ }
2973
+ ]
2974
+ }
2975
+ };
2976
+ function isHookInstalled(projectPath, provider) {
2977
+ if (provider === "claude") {
2978
+ const settingsPath = join13(projectPath, ".claude", "settings.json");
2979
+ if (!existsSync12(settingsPath)) return false;
2980
+ try {
2981
+ const content = readFileSync9(settingsPath, "utf-8");
2982
+ const settings = JSON.parse(content);
2983
+ const hooks = settings.hooks?.PostToolUse;
2984
+ if (!Array.isArray(hooks)) return false;
2985
+ return hooks.some(
2986
+ (h) => h.matcher?.includes("Edit") && h.hooks?.some((hh) => hh.command?.includes("post-edit"))
2987
+ );
2988
+ } catch {
2989
+ return false;
2990
+ }
2991
+ } else {
2992
+ const hooksPath = join13(projectPath, ".cursor", "hooks.json");
2993
+ if (!existsSync12(hooksPath)) return false;
2994
+ try {
2995
+ const content = readFileSync9(hooksPath, "utf-8");
2996
+ const hooks = JSON.parse(content);
2997
+ const afterFileEdit = hooks.hooks?.afterFileEdit;
2998
+ if (!Array.isArray(afterFileEdit)) return false;
2999
+ return afterFileEdit.some(
3000
+ (h) => h.command?.includes("post-edit")
3001
+ );
3002
+ } catch {
3003
+ return false;
3004
+ }
3005
+ }
3006
+ }
3007
+ function planClaudeHook(projectPath) {
3008
+ const actions = [];
3009
+ const claudeDir = join13(projectPath, ".claude");
3010
+ const hooksDir = join13(claudeDir, "hooks");
3011
+ const settingsPath = join13(claudeDir, "settings.json");
3012
+ actions.push({
3013
+ type: "create_directory",
3014
+ path: claudeDir
3015
+ });
3016
+ actions.push({
3017
+ type: "create_directory",
3018
+ path: hooksDir
3019
+ });
3020
+ actions.push({
3021
+ type: "create_file",
3022
+ path: join13(hooksDir, "post-edit.sh"),
3023
+ content: POST_EDIT_HOOK_SCRIPT,
3024
+ permissions: 493
3025
+ });
3026
+ if (existsSync12(settingsPath)) {
3027
+ actions.push({
3028
+ type: "merge_json",
3029
+ path: settingsPath,
3030
+ merge: CLAUDE_HOOK_CONFIG
3031
+ });
3032
+ } else {
3033
+ actions.push({
3034
+ type: "create_file",
3035
+ path: settingsPath,
3036
+ content: JSON.stringify(CLAUDE_HOOK_CONFIG, null, 2)
3037
+ });
3038
+ }
3039
+ return actions;
3040
+ }
3041
+ function planCursorHook(projectPath) {
3042
+ const actions = [];
3043
+ const cursorDir = join13(projectPath, ".cursor");
3044
+ const hooksDir = join13(cursorDir, "hooks");
3045
+ const hooksJsonPath = join13(cursorDir, "hooks.json");
3046
+ actions.push({
3047
+ type: "create_directory",
3048
+ path: cursorDir
3049
+ });
3050
+ actions.push({
3051
+ type: "create_directory",
3052
+ path: hooksDir
3053
+ });
3054
+ actions.push({
3055
+ type: "create_file",
3056
+ path: join13(hooksDir, "post-edit.sh"),
3057
+ content: POST_EDIT_HOOK_SCRIPT,
3058
+ permissions: 493
3059
+ });
3060
+ if (existsSync12(hooksJsonPath)) {
3061
+ actions.push({
3062
+ type: "merge_json",
3063
+ path: hooksJsonPath,
3064
+ merge: CURSOR_HOOK_CONFIG
3065
+ });
3066
+ } else {
3067
+ actions.push({
3068
+ type: "create_file",
3069
+ path: hooksJsonPath,
3070
+ content: JSON.stringify(CURSOR_HOOK_CONFIG, null, 2)
3071
+ });
3072
+ }
3073
+ return actions;
3074
+ }
3075
+
3076
+ // src/commands/init/installers/eslint.ts
2902
3077
  async function promptForField(field, currentValue) {
2903
3078
  const hint = field.description ? pc.dim(` ${field.description}`) : "";
2904
3079
  switch (field.type) {
@@ -3187,7 +3362,49 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3187
3362
  }
3188
3363
  }
3189
3364
  }
3190
- return { configuredRules };
3365
+ log("");
3366
+ const claudeInstalled = isHookInstalled(project.projectPath, "claude");
3367
+ const cursorInstalled = isHookInstalled(project.projectPath, "cursor");
3368
+ const hookOptions = [];
3369
+ if (!claudeInstalled) {
3370
+ hookOptions.push({
3371
+ value: "claude",
3372
+ label: "Claude Code",
3373
+ hint: "Auto-lint on file edit via .claude/hooks/post-edit.sh"
3374
+ });
3375
+ }
3376
+ if (!cursorInstalled) {
3377
+ hookOptions.push({
3378
+ value: "cursor",
3379
+ label: "Cursor",
3380
+ hint: "Auto-lint on file edit via .cursor/hooks/post-edit.sh"
3381
+ });
3382
+ }
3383
+ let aiHooks = [];
3384
+ if (hookOptions.length > 0) {
3385
+ hookOptions.push({
3386
+ value: "none",
3387
+ label: "None",
3388
+ hint: "Skip AI editor integration"
3389
+ });
3390
+ const selectedHooks = await multiselect({
3391
+ message: "Install AI editor hooks? " + pc.dim("(auto-lint on file edit)"),
3392
+ options: hookOptions,
3393
+ initialValues: [],
3394
+ required: false
3395
+ });
3396
+ aiHooks = selectedHooks.filter((h) => h === "claude" || h === "cursor");
3397
+ if (aiHooks.length > 0) {
3398
+ log(
3399
+ pc.dim(` \u2192 Will install hooks for: ${aiHooks.join(", ")}`)
3400
+ );
3401
+ }
3402
+ } else {
3403
+ log(
3404
+ pc.dim(" AI hooks already installed (Claude and Cursor)")
3405
+ );
3406
+ }
3407
+ return { configuredRules, aiHooks };
3191
3408
  },
3192
3409
  plan(targets, config, project) {
3193
3410
  const actions = [];
@@ -3202,7 +3419,7 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3202
3419
  for (const target of targets) {
3203
3420
  const pkgInfo = project.packages.find((p) => p.path === target.path);
3204
3421
  if (!pkgInfo || !pkgInfo.eslintConfigPath) continue;
3205
- const rulesDir = join13(target.path, ".uilint", "rules");
3422
+ const rulesDir = join14(target.path, ".uilint", "rules");
3206
3423
  actions.push({
3207
3424
  type: "create_directory",
3208
3425
  path: rulesDir
@@ -3227,13 +3444,20 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3227
3444
  hasExistingRules: pkgInfo.hasUilintRules
3228
3445
  });
3229
3446
  }
3230
- const gitignorePath = join13(project.workspaceRoot, ".gitignore");
3447
+ const gitignorePath = join14(project.workspaceRoot, ".gitignore");
3231
3448
  actions.push({
3232
3449
  type: "append_to_file",
3233
3450
  path: gitignorePath,
3234
3451
  content: "\n# UILint cache\n.uilint/.cache\n",
3235
3452
  ifNotContains: ".uilint/.cache"
3236
3453
  });
3454
+ const { aiHooks } = eslintConfig;
3455
+ if (aiHooks?.includes("claude")) {
3456
+ actions.push(...planClaudeHook(project.projectPath));
3457
+ }
3458
+ if (aiHooks?.includes("cursor")) {
3459
+ actions.push(...planCursorHook(project.projectPath));
3460
+ }
3237
3461
  return { actions, dependencies };
3238
3462
  },
3239
3463
  async *execute(targets, config, project) {
@@ -3254,9 +3478,25 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3254
3478
  detail: `\u2192 ${target.hint}`
3255
3479
  };
3256
3480
  }
3481
+ const { aiHooks } = eslintConfig;
3482
+ if (aiHooks?.includes("claude")) {
3483
+ yield {
3484
+ type: "progress",
3485
+ message: "Installing Claude Code hook",
3486
+ detail: "\u2192 .claude/hooks/post-edit.sh"
3487
+ };
3488
+ }
3489
+ if (aiHooks?.includes("cursor")) {
3490
+ yield {
3491
+ type: "progress",
3492
+ message: "Installing Cursor hook",
3493
+ detail: "\u2192 .cursor/hooks/post-edit.sh"
3494
+ };
3495
+ }
3496
+ const hookSuffix = aiHooks?.length ? ` + ${aiHooks.length} AI hook(s)` : "";
3257
3497
  yield {
3258
3498
  type: "complete",
3259
- message: `ESLint plugin installed in ${targets.length} package(s)`
3499
+ message: `ESLint plugin installed in ${targets.length} package(s)${hookSuffix}`
3260
3500
  };
3261
3501
  },
3262
3502
  planRemove(targets, project) {
@@ -3269,7 +3509,7 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3269
3509
  packagePath: target.path,
3270
3510
  configPath: pkgInfo.eslintConfigPath
3271
3511
  });
3272
- const rulesDir = join13(target.path, ".uilint", "rules");
3512
+ const rulesDir = join14(target.path, ".uilint", "rules");
3273
3513
  actions.push({
3274
3514
  type: "remove_directory",
3275
3515
  path: rulesDir
@@ -3280,12 +3520,12 @@ ${semanticCat?.icon ?? "\u{1F9E0}"} ${semanticCat?.name ?? "Semantic rules"} (${
3280
3520
  };
3281
3521
 
3282
3522
  // src/utils/client-boundary-tracer.ts
3283
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
3284
- import { join as join14, dirname as dirname4, relative as relative3 } from "path";
3523
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
3524
+ import { join as join15, dirname as dirname4, relative as relative3 } from "path";
3285
3525
  import { parseModule as parseModule4 } from "magicast";
3286
3526
  function hasUseClientDirective(filePath) {
3287
3527
  try {
3288
- const content = readFileSync9(filePath, "utf-8");
3528
+ const content = readFileSync10(filePath, "utf-8");
3289
3529
  const mod = parseModule4(content);
3290
3530
  const program = mod.$ast;
3291
3531
  if (!program || program.type !== "Program") return false;
@@ -3334,33 +3574,33 @@ function resolveImportPath(importSource, fromFile, projectPath) {
3334
3574
  let basePath;
3335
3575
  if (importSource.startsWith("@/")) {
3336
3576
  const withoutAlias = importSource.slice(2);
3337
- const srcPath = join14(projectPath, "src", withoutAlias);
3338
- const rootPath = join14(projectPath, withoutAlias);
3339
- basePath = existsSync12(dirname4(srcPath)) ? srcPath : rootPath;
3577
+ const srcPath = join15(projectPath, "src", withoutAlias);
3578
+ const rootPath = join15(projectPath, withoutAlias);
3579
+ basePath = existsSync13(dirname4(srcPath)) ? srcPath : rootPath;
3340
3580
  } else if (importSource.startsWith("~/")) {
3341
- basePath = join14(projectPath, importSource.slice(2));
3581
+ basePath = join15(projectPath, importSource.slice(2));
3342
3582
  } else if (importSource.startsWith(".")) {
3343
- basePath = join14(fromDir, importSource);
3583
+ basePath = join15(fromDir, importSource);
3344
3584
  } else {
3345
3585
  return null;
3346
3586
  }
3347
3587
  const extensions = [".tsx", ".ts", ".jsx", ".js"];
3348
3588
  for (const ext of extensions) {
3349
3589
  const fullPath = basePath + ext;
3350
- if (existsSync12(fullPath)) return fullPath;
3590
+ if (existsSync13(fullPath)) return fullPath;
3351
3591
  }
3352
3592
  for (const ext of extensions) {
3353
- const indexPath = join14(basePath, `index${ext}`);
3354
- if (existsSync12(indexPath)) return indexPath;
3593
+ const indexPath = join15(basePath, `index${ext}`);
3594
+ if (existsSync13(indexPath)) return indexPath;
3355
3595
  }
3356
- if (existsSync12(basePath)) return basePath;
3596
+ if (existsSync13(basePath)) return basePath;
3357
3597
  return null;
3358
3598
  }
3359
3599
  function findLayoutFile2(projectPath, appRoot) {
3360
3600
  const extensions = [".tsx", ".jsx", ".ts", ".js"];
3361
3601
  for (const ext of extensions) {
3362
- const layoutPath = join14(projectPath, appRoot, `layout${ext}`);
3363
- if (existsSync12(layoutPath)) return layoutPath;
3602
+ const layoutPath = join15(projectPath, appRoot, `layout${ext}`);
3603
+ if (existsSync13(layoutPath)) return layoutPath;
3364
3604
  }
3365
3605
  return null;
3366
3606
  }
@@ -3381,7 +3621,7 @@ function traceClientBoundaries(projectPath, appRoot) {
3381
3621
  }
3382
3622
  let program;
3383
3623
  try {
3384
- const content = readFileSync9(layoutFile, "utf-8");
3624
+ const content = readFileSync10(layoutFile, "utf-8");
3385
3625
  const mod = parseModule4(content);
3386
3626
  program = mod.$ast;
3387
3627
  } catch {
@@ -3418,8 +3658,8 @@ function providersFileExists(projectPath, appRoot) {
3418
3658
  const names = ["providers", "Providers"];
3419
3659
  for (const name of names) {
3420
3660
  for (const ext of extensions) {
3421
- const providersPath = join14(projectPath, appRoot, `${name}${ext}`);
3422
- if (existsSync12(providersPath)) return providersPath;
3661
+ const providersPath = join15(projectPath, appRoot, `${name}${ext}`);
3662
+ if (existsSync13(providersPath)) return providersPath;
3423
3663
  }
3424
3664
  }
3425
3665
  return null;
@@ -3768,4 +4008,4 @@ export {
3768
4008
  analyze,
3769
4009
  execute
3770
4010
  };
3771
- //# sourceMappingURL=chunk-3LKX26SH.js.map
4011
+ //# sourceMappingURL=chunk-SQFSFBUP.js.map