mirror of https://github.com/langgenius/dify.git
docs: add cursor rules for test generation and update analyze-component script for complexity scoring and test priority
This commit is contained in:
parent
8390ad95b7
commit
1bb42447cf
|
|
@ -0,0 +1,193 @@
|
|||
# Cursor Rules for Dify Project - Test Generation
|
||||
|
||||
## When Generating Tests
|
||||
|
||||
Follow these rules when asked to generate tests for React components:
|
||||
|
||||
### Tech Stack
|
||||
- Next.js 15 + React 19 + TypeScript
|
||||
- Jest 29.7 + React Testing Library 16.0
|
||||
- Test environment: @happy-dom/jest-environment
|
||||
- File naming: `ComponentName.spec.tsx` (same directory)
|
||||
|
||||
### Component Complexity Guidelines
|
||||
|
||||
#### 🔴 Very Complex Components (Complexity > 50)
|
||||
- **Split before testing**: Break component into smaller pieces
|
||||
- **Integration tests**: Test complex workflows end-to-end
|
||||
- **Data-driven tests**: Use `test.each()` for multiple scenarios
|
||||
- **Performance benchmarks**: Add performance tests for critical paths
|
||||
|
||||
#### ⚠️ Complex Components (Complexity 30-50)
|
||||
- **Multiple describe blocks**: Group related test cases
|
||||
- **Integration scenarios**: Test feature combinations
|
||||
- **Organized structure**: Keep tests maintainable
|
||||
|
||||
#### 📏 Large Components (500+ lines)
|
||||
- **Consider refactoring**: Split into smaller components if possible
|
||||
- **Section testing**: Test major sections separately
|
||||
- **Helper functions**: Reduce test complexity with utilities
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
Apply based on component features:
|
||||
|
||||
#### 1. Rendering Tests (REQUIRED for all)
|
||||
```typescript
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<Component />)
|
||||
expect(screen.getByRole('...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. Props Testing (REQUIRED for all)
|
||||
- Test each prop variation
|
||||
- Test required vs optional props
|
||||
- Test default values
|
||||
- Test prop type validation
|
||||
|
||||
#### 3. State Management
|
||||
|
||||
**State Only (useState):**
|
||||
- Test initial state values
|
||||
- Test all state transitions
|
||||
- Test state reset/cleanup scenarios
|
||||
|
||||
**Effects Only (useEffect):**
|
||||
- Test effect execution conditions
|
||||
- Verify dependencies array correctness
|
||||
- Test cleanup on unmount
|
||||
|
||||
**State + Effects Combined:**
|
||||
- Test state initialization and updates
|
||||
- Test useEffect dependencies array
|
||||
- Test cleanup functions (return from useEffect)
|
||||
- Use `waitFor()` for async state changes
|
||||
|
||||
#### 4. Performance Optimization
|
||||
|
||||
**useCallback:**
|
||||
- Verify callbacks maintain referential equality
|
||||
- Test callback dependencies
|
||||
- Ensure re-renders don't recreate functions unnecessarily
|
||||
|
||||
**useMemo:**
|
||||
- Test memoization dependencies
|
||||
- Ensure expensive computations are cached
|
||||
- Verify memo recomputation conditions
|
||||
|
||||
#### 5. Event Handlers
|
||||
- Test all onClick, onChange, onSubmit handlers
|
||||
- Test keyboard events (Enter, Escape, Tab, etc.)
|
||||
- Verify event.preventDefault() calls if needed
|
||||
- Test event bubbling/propagation
|
||||
- Use `fireEvent` (not userEvent)
|
||||
|
||||
#### 6. API Calls & Async Operations
|
||||
- Mock all API calls using `jest.mock`
|
||||
- Test retry logic if applicable
|
||||
- Verify error handling and user feedback
|
||||
- Use `waitFor()` for async operations
|
||||
|
||||
#### 7. Next.js Routing
|
||||
- Mock useRouter, usePathname, useSearchParams
|
||||
- Test navigation behavior and parameters
|
||||
- Test query string handling
|
||||
- Verify route guards/redirects if any
|
||||
- Test URL parameter updates
|
||||
|
||||
#### 8. Edge Cases (REQUIRED for all)
|
||||
- Test null/undefined/empty values
|
||||
- Test boundary conditions
|
||||
- Test error states
|
||||
- Test loading states
|
||||
- Test unexpected inputs
|
||||
|
||||
#### 9. Accessibility (Optional)
|
||||
- Test keyboard navigation
|
||||
- Verify ARIA attributes
|
||||
- Test focus management
|
||||
- Ensure screen reader compatibility
|
||||
|
||||
#### 10. Snapshots (use sparingly)
|
||||
- Only for stable UI (icons, badges, static layouts)
|
||||
- Snapshot small sections only
|
||||
- Prefer explicit assertions over snapshots
|
||||
- Update snapshots intentionally, not automatically
|
||||
|
||||
**Note**: Dify is a desktop app. NO responsive/mobile testing.
|
||||
|
||||
### Code Style
|
||||
|
||||
- Use `fireEvent` not `userEvent`
|
||||
- AAA pattern: Arrange → Act → Assert
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- TypeScript: No `any` types
|
||||
- Cleanup: `afterEach(() => jest.clearAllMocks())`
|
||||
|
||||
### Dify-Specific Components
|
||||
|
||||
#### General
|
||||
1. **i18n**: Always return key: `t: (key) => key`
|
||||
2. **Toast**: Mock `@/app/components/base/toast`
|
||||
3. **Forms**: Test validation thoroughly
|
||||
|
||||
#### Workflow Components (`workflow/`)
|
||||
- **Node configuration**: Test all node configuration options
|
||||
- **Data validation**: Verify input/output validation rules
|
||||
- **Variable passing**: Test data flow between nodes
|
||||
- **Edge connections**: Test graph structure and connections
|
||||
- **Error handling**: Verify handling of invalid configurations
|
||||
- **Integration**: Test complete workflow execution paths
|
||||
|
||||
#### Dataset Components (`dataset/`)
|
||||
- **File upload**: Test file upload and validation
|
||||
- **File types**: Verify supported format handling
|
||||
- **Pagination**: Test data loading and pagination
|
||||
- **Search & filtering**: Test query functionality
|
||||
- **Data format handling**: Test various data formats
|
||||
- **Error states**: Test upload failures and invalid data
|
||||
|
||||
#### Configuration Components (`app/configuration`, `config/`)
|
||||
- **Form validation**: Test all validation rules thoroughly
|
||||
- **Save/reset functionality**: Test data persistence
|
||||
- **Required vs optional fields**: Verify field validation
|
||||
- **Configuration persistence**: Test state preservation
|
||||
- **Error feedback**: Verify user error messages
|
||||
- **Default values**: Test initial configuration state
|
||||
|
||||
### Test Strategy Quick Reference
|
||||
|
||||
**Always Test:**
|
||||
- ✅ Rendering without crashing
|
||||
- ✅ Props (required, optional, defaults)
|
||||
- ✅ Edge cases (null, undefined, empty)
|
||||
|
||||
**Test When Present:**
|
||||
- 🔄 **useState** → State initialization, transitions, cleanup
|
||||
- ⚡ **useEffect** → Execution, dependencies, cleanup
|
||||
- 🎯 **Event handlers** → All onClick, onChange, onSubmit, keyboard events
|
||||
- 🌐 **API calls** → Loading, success, error states
|
||||
- 🔀 **Routing** → Navigation, params, query strings
|
||||
- 🚀 **useCallback/useMemo** → Referential equality, dependencies
|
||||
- ⚙️ **Workflow** → Node config, data flow, validation
|
||||
- 📚 **Dataset** → Upload, pagination, search
|
||||
- 🎛️ **Configuration** → Form validation, persistence
|
||||
|
||||
**Complex Components (30+):**
|
||||
- Group tests in multiple `describe` blocks
|
||||
- Test integration scenarios
|
||||
- Consider splitting component before testing
|
||||
|
||||
### Coverage Target
|
||||
|
||||
Aim for 100% coverage:
|
||||
- Line: >95%
|
||||
- Branch: >95%
|
||||
- Function: 100%
|
||||
- Statement: 100%
|
||||
|
||||
Generate comprehensive tests covering ALL code paths and scenarios.
|
||||
|
||||
|
|
@ -1,38 +1,89 @@
|
|||
# Production Build Optimization Scripts
|
||||
# Web Scripts
|
||||
|
||||
## optimize-standalone.js
|
||||
Frontend development utility scripts.
|
||||
|
||||
This script removes unnecessary development dependencies from the Next.js standalone build output to reduce the production Docker image size.
|
||||
## 📋 Scripts
|
||||
|
||||
### What it does
|
||||
- `generate-icons.js` - Generate PWA icons
|
||||
- `optimize-standalone.js` - Optimize build output
|
||||
- `analyze-component.js` - **Test generation helper** ⭐
|
||||
|
||||
The script specifically targets and removes `jest-worker` packages that are bundled with Next.js but not needed in production. These packages are included because:
|
||||
---
|
||||
|
||||
1. Next.js includes jest-worker in its compiled dependencies
|
||||
1. terser-webpack-plugin (used by Next.js for minification) depends on jest-worker
|
||||
1. pnpm's dependency resolution creates symlinks to jest-worker in various locations
|
||||
## 🚀 Generate Tests (Using Cursor AI)
|
||||
|
||||
### Usage
|
||||
|
||||
The script is automatically run during Docker builds via the `build:docker` npm script:
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Docker build (removes jest-worker after build)
|
||||
pnpm build:docker
|
||||
# 1. Analyze component
|
||||
pnpm test:gen app/components/base/button/index.tsx
|
||||
|
||||
# Output: Component analysis + Cursor prompt (auto-copied)
|
||||
|
||||
# 2. In Cursor: Cmd+L → Cmd+V → Enter → Apply
|
||||
|
||||
# 3. Verify
|
||||
pnpm test app/components/base/button/index.spec.tsx
|
||||
```
|
||||
|
||||
To run the optimization manually:
|
||||
**Done in < 1 minute!** ✅
|
||||
|
||||
---
|
||||
|
||||
## 📊 How It Works
|
||||
|
||||
### Component Complexity
|
||||
|
||||
Script analyzes and scores components:
|
||||
|
||||
- **0-10**: 🟢 Simple (5-10 min to test)
|
||||
- **11-30Menu 🟡 Medium (15-30 min to test)
|
||||
- **31-50Menu 🟠 Complex (30-60 min to test)
|
||||
- **51+**: 🔴 Very Complex (60+ min, consider refactoring)
|
||||
|
||||
### Test Scenarios (11 types)
|
||||
|
||||
Defined in `.cursorrules`:
|
||||
|
||||
**Must test**: Rendering, Props, Edge Cases
|
||||
**CommonMenuInteractions, Accessibility, i18n, Async
|
||||
**OptionalMenuState, Security, Performance, Snapshots
|
||||
|
||||
Cursor AI auto-selects scenarios based on component features.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Daily Workflow
|
||||
|
||||
```bash
|
||||
node scripts/optimize-standalone.js
|
||||
# New component
|
||||
pnpm test:gen app/components/new-feature/index.tsx
|
||||
# → Cursor → Apply → Done
|
||||
|
||||
# Or even simpler in Cursor
|
||||
# Cmd+I → "Generate test" → Apply
|
||||
```
|
||||
|
||||
### What gets removed
|
||||
---
|
||||
|
||||
- `node_modules/.pnpm/next@*/node_modules/next/dist/compiled/jest-worker`
|
||||
- `node_modules/.pnpm/terser-webpack-plugin@*/node_modules/jest-worker` (symlinks)
|
||||
- `node_modules/.pnpm/jest-worker@*` (actual packages)
|
||||
## 📋 Commands
|
||||
|
||||
### Impact
|
||||
```bash
|
||||
pnpm test:gen <path> # Generate test
|
||||
pnpm test [file] # Run tests
|
||||
pnpm test --coverage # View coverage
|
||||
pnpm lint # Code check
|
||||
pnpm type-check # Type check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Customize
|
||||
|
||||
Edit `.cursorrules` to modify test standards for your team.
|
||||
|
||||
```bash
|
||||
code .cursorrules
|
||||
git commit -m "docs: update test rules"
|
||||
```
|
||||
|
||||
Removing jest-worker saves approximately 36KB per instance from the production image. While this may seem small, it helps ensure production images only contain necessary runtime dependencies.
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
* node scripts/analyze-component.js app/components/workflow/nodes/llm/panel.tsx
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
// ============================================================================
|
||||
// Simple Analyzer
|
||||
|
|
@ -22,7 +22,15 @@ const path = require('path');
|
|||
|
||||
class ComponentAnalyzer {
|
||||
analyze(code, filePath) {
|
||||
const fileName = path.basename(filePath, path.extname(filePath));
|
||||
const fileName = path.basename(filePath, path.extname(filePath))
|
||||
const complexity = this.calculateComplexity(code)
|
||||
const lineCount = code.split('\n').length
|
||||
|
||||
// Count usage references (may take a few seconds)
|
||||
const usageCount = this.countUsageReferences(filePath)
|
||||
|
||||
// Calculate test priority
|
||||
const priority = this.calculateTestPriority(complexity, usageCount)
|
||||
|
||||
return {
|
||||
name: fileName.charAt(0).toUpperCase() + fileName.slice(1),
|
||||
|
|
@ -34,21 +42,21 @@ class ComponentAnalyzer {
|
|||
hasCallbacks: code.includes('useCallback'),
|
||||
hasMemo: code.includes('useMemo'),
|
||||
hasEvents: /on[A-Z]\w+/.test(code),
|
||||
hasI18n: code.includes('useTranslation') || code.includes('react-i18next'),
|
||||
hasRouter: code.includes('useRouter') || code.includes('usePathname'),
|
||||
hasAPI: code.includes('service/') || code.includes('fetch('),
|
||||
isClientComponent: code.includes("'use client'") || code.includes('"use client"'),
|
||||
complexity: this.calculateComplexity(code),
|
||||
lineCount: code.split('\n').length,
|
||||
};
|
||||
complexity,
|
||||
lineCount,
|
||||
usageCount,
|
||||
priority,
|
||||
}
|
||||
}
|
||||
|
||||
detectType(filePath, code) {
|
||||
if (filePath.includes('/hooks/')) return 'hook';
|
||||
if (filePath.includes('/utils/')) return 'util';
|
||||
if (filePath.includes('/page.tsx')) return 'page';
|
||||
if (code.includes('useState') || code.includes('useEffect')) return 'component';
|
||||
return 'component';
|
||||
if (filePath.includes('/hooks/')) return 'hook'
|
||||
if (filePath.includes('/utils/')) return 'util'
|
||||
if (filePath.includes('/page.tsx')) return 'page'
|
||||
if (code.includes('useState') || code.includes('useEffect')) return 'component'
|
||||
return 'component'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,103 +70,204 @@ class ComponentAnalyzer {
|
|||
* 51+: 🔴 Very Complex (60+ min, consider splitting)
|
||||
*/
|
||||
calculateComplexity(code) {
|
||||
let score = 0;
|
||||
let score = 0
|
||||
|
||||
// ===== React Hooks (State Management Complexity) =====
|
||||
const stateHooks = (code.match(/useState/g) || []).length;
|
||||
const effectHooks = (code.match(/useEffect/g) || []).length;
|
||||
const callbackHooks = (code.match(/useCallback/g) || []).length;
|
||||
const memoHooks = (code.match(/useMemo/g) || []).length;
|
||||
const refHooks = (code.match(/useRef/g) || []).length;
|
||||
const customHooks = (code.match(/use[A-Z]\w+/g) || []).length -
|
||||
(stateHooks + effectHooks + callbackHooks + memoHooks + refHooks);
|
||||
const stateHooks = (code.match(/useState/g) || []).length
|
||||
const effectHooks = (code.match(/useEffect/g) || []).length
|
||||
const callbackHooks = (code.match(/useCallback/g) || []).length
|
||||
const memoHooks = (code.match(/useMemo/g) || []).length
|
||||
const refHooks = (code.match(/useRef/g) || []).length
|
||||
const customHooks = (code.match(/use[A-Z]\w+/g) || []).length
|
||||
- (stateHooks + effectHooks + callbackHooks + memoHooks + refHooks)
|
||||
|
||||
score += stateHooks * 5; // Each state +5 (need to test state changes)
|
||||
score += effectHooks * 6; // Each effect +6 (need to test deps & cleanup)
|
||||
score += callbackHooks * 2; // Each callback +2
|
||||
score += memoHooks * 2; // Each memo +2
|
||||
score += refHooks * 1; // Each ref +1
|
||||
score += customHooks * 3; // Each custom hook +3
|
||||
score += stateHooks * 5 // Each state +5 (need to test state changes)
|
||||
score += effectHooks * 6 // Each effect +6 (need to test deps & cleanup)
|
||||
score += callbackHooks * 2 // Each callback +2
|
||||
score += memoHooks * 2 // Each memo +2
|
||||
score += refHooks * 1 // Each ref +1
|
||||
score += customHooks * 3 // Each custom hook +3
|
||||
|
||||
// ===== Control Flow Complexity (Cyclomatic Complexity) =====
|
||||
score += (code.match(/if\s*\(/g) || []).length * 2; // if statement
|
||||
score += (code.match(/else\s+if/g) || []).length * 2; // else if
|
||||
score += (code.match(/\?\s*[^:]+\s*:/g) || []).length * 1; // ternary operator
|
||||
score += (code.match(/switch\s*\(/g) || []).length * 3; // switch
|
||||
score += (code.match(/case\s+/g) || []).length * 1; // case branch
|
||||
score += (code.match(/&&/g) || []).length * 1; // logical AND
|
||||
score += (code.match(/\|\|/g) || []).length * 1; // logical OR
|
||||
score += (code.match(/\?\?/g) || []).length * 1; // nullish coalescing
|
||||
score += (code.match(/if\s*\(/g) || []).length * 2 // if statement
|
||||
score += (code.match(/else\s+if/g) || []).length * 2 // else if
|
||||
score += (code.match(/\?\s*[^:]+\s*:/g) || []).length * 1 // ternary operator
|
||||
score += (code.match(/switch\s*\(/g) || []).length * 3 // switch
|
||||
score += (code.match(/case\s+/g) || []).length * 1 // case branch
|
||||
score += (code.match(/&&/g) || []).length * 1 // logical AND
|
||||
score += (code.match(/\|\|/g) || []).length * 1 // logical OR
|
||||
score += (code.match(/\?\?/g) || []).length * 1 // nullish coalescing
|
||||
|
||||
// ===== Loop Complexity =====
|
||||
score += (code.match(/\.map\(/g) || []).length * 2; // map
|
||||
score += (code.match(/\.filter\(/g) || []).length * 1; // filter
|
||||
score += (code.match(/\.reduce\(/g) || []).length * 3; // reduce (complex)
|
||||
score += (code.match(/for\s*\(/g) || []).length * 2; // for loop
|
||||
score += (code.match(/while\s*\(/g) || []).length * 3; // while loop
|
||||
score += (code.match(/\.map\(/g) || []).length * 2 // map
|
||||
score += (code.match(/\.filter\(/g) || []).length * 1 // filter
|
||||
score += (code.match(/\.reduce\(/g) || []).length * 3 // reduce (complex)
|
||||
score += (code.match(/for\s*\(/g) || []).length * 2 // for loop
|
||||
score += (code.match(/while\s*\(/g) || []).length * 3 // while loop
|
||||
|
||||
// ===== Props and Events Complexity =====
|
||||
const propsMatches = code.match(/(\w+)\s*:\s*\w+/g) || [];
|
||||
const propsCount = Math.min(propsMatches.length, 20); // Max 20 props
|
||||
score += Math.floor(propsCount / 2); // Every 2 props +1
|
||||
const propsMatches = code.match(/(\w+)\s*:\s*\w+/g) || []
|
||||
const propsCount = Math.min(propsMatches.length, 20) // Max 20 props
|
||||
score += Math.floor(propsCount / 2) // Every 2 props +1
|
||||
|
||||
const eventHandlers = (code.match(/on[A-Z]\w+/g) || []).length;
|
||||
score += eventHandlers * 2; // Each event handler +2
|
||||
const eventHandlers = (code.match(/on[A-Z]\w+/g) || []).length
|
||||
score += eventHandlers * 2 // Each event handler +2
|
||||
|
||||
// ===== API Call Complexity =====
|
||||
score += (code.match(/fetch\(/g) || []).length * 4; // fetch
|
||||
score += (code.match(/axios\./g) || []).length * 4; // axios
|
||||
score += (code.match(/useSWR/g) || []).length * 4; // SWR
|
||||
score += (code.match(/useQuery/g) || []).length * 4; // React Query
|
||||
score += (code.match(/\.then\(/g) || []).length * 2; // Promise
|
||||
score += (code.match(/await\s+/g) || []).length * 2; // async/await
|
||||
score += (code.match(/fetch\(/g) || []).length * 4 // fetch
|
||||
score += (code.match(/axios\./g) || []).length * 4 // axios
|
||||
score += (code.match(/useSWR/g) || []).length * 4 // SWR
|
||||
score += (code.match(/useQuery/g) || []).length * 4 // React Query
|
||||
score += (code.match(/\.then\(/g) || []).length * 2 // Promise
|
||||
score += (code.match(/await\s+/g) || []).length * 2 // async/await
|
||||
|
||||
// ===== Third-party Library Integration =====
|
||||
const hasReactFlow = /reactflow|ReactFlow/.test(code);
|
||||
const hasMonaco = /@monaco-editor/.test(code);
|
||||
const hasEcharts = /echarts/.test(code);
|
||||
const hasLexical = /lexical/.test(code);
|
||||
const hasReactFlow = /reactflow|ReactFlow/.test(code)
|
||||
const hasMonaco = /@monaco-editor/.test(code)
|
||||
const hasEcharts = /echarts/.test(code)
|
||||
const hasLexical = /lexical/.test(code)
|
||||
|
||||
if (hasReactFlow) score += 15; // ReactFlow is very complex
|
||||
if (hasMonaco) score += 12; // Monaco Editor
|
||||
if (hasEcharts) score += 8; // Echarts
|
||||
if (hasLexical) score += 10; // Lexical Editor
|
||||
if (hasReactFlow) score += 15 // ReactFlow is very complex
|
||||
if (hasMonaco) score += 12 // Monaco Editor
|
||||
if (hasEcharts) score += 8 // Echarts
|
||||
if (hasLexical) score += 10 // Lexical Editor
|
||||
|
||||
// ===== Code Size Complexity =====
|
||||
const lines = code.split('\n').length;
|
||||
if (lines > 500) score += 10;
|
||||
else if (lines > 300) score += 6;
|
||||
else if (lines > 150) score += 3;
|
||||
const lines = code.split('\n').length
|
||||
if (lines > 500) score += 10
|
||||
else if (lines > 300) score += 6
|
||||
else if (lines > 150) score += 3
|
||||
|
||||
// ===== Nesting Depth (deep nesting reduces readability) =====
|
||||
const maxNesting = this.calculateNestingDepth(code);
|
||||
score += Math.max(0, (maxNesting - 3)) * 2; // Over 3 levels, +2 per level
|
||||
const maxNesting = this.calculateNestingDepth(code)
|
||||
score += Math.max(0, (maxNesting - 3)) * 2 // Over 3 levels, +2 per level
|
||||
|
||||
// ===== Context and Global State =====
|
||||
score += (code.match(/useContext/g) || []).length * 3;
|
||||
score += (code.match(/useStore|useAppStore/g) || []).length * 4;
|
||||
score += (code.match(/zustand|redux/g) || []).length * 3;
|
||||
score += (code.match(/useContext/g) || []).length * 3
|
||||
score += (code.match(/useStore|useAppStore/g) || []).length * 4
|
||||
score += (code.match(/zustand|redux/g) || []).length * 3
|
||||
|
||||
return Math.min(score, 100); // Max 100 points
|
||||
return Math.min(score, 100) // Max 100 points
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum nesting depth
|
||||
*/
|
||||
calculateNestingDepth(code) {
|
||||
let maxDepth = 0;
|
||||
let currentDepth = 0;
|
||||
let maxDepth = 0
|
||||
let currentDepth = 0
|
||||
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
if (code[i] === '{') {
|
||||
currentDepth++;
|
||||
maxDepth = Math.max(maxDepth, currentDepth);
|
||||
} else if (code[i] === '}') {
|
||||
currentDepth--;
|
||||
currentDepth++
|
||||
maxDepth = Math.max(maxDepth, currentDepth)
|
||||
}
|
||||
else if (code[i] === '}') {
|
||||
currentDepth--
|
||||
}
|
||||
}
|
||||
|
||||
return maxDepth;
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many times a component is referenced in the codebase
|
||||
* Uses grep for searching import statements
|
||||
*/
|
||||
countUsageReferences(filePath) {
|
||||
try {
|
||||
const { execSync } = require('node:child_process')
|
||||
|
||||
// Get component name from file path
|
||||
const fileName = path.basename(filePath, path.extname(filePath))
|
||||
|
||||
// If the file is index.tsx, use the parent directory name as the component name
|
||||
// e.g., app/components/base/avatar/index.tsx -> search for 'avatar'
|
||||
// Otherwise use the file name
|
||||
// e.g., app/components/base/button.tsx -> search for 'button'
|
||||
let searchName = fileName
|
||||
if (fileName === 'index') {
|
||||
const parentDir = path.dirname(filePath)
|
||||
searchName = path.basename(parentDir)
|
||||
}
|
||||
|
||||
// Build search pattern for import statements
|
||||
// Match: from '@/app/components/base/avatar'
|
||||
// Match: from './avatar'
|
||||
// Match: from '../avatar'
|
||||
// Simplified pattern to avoid shell quote issues
|
||||
const searchPattern = `/${searchName}'`
|
||||
|
||||
// Use grep to search across all TypeScript files
|
||||
// -r: recursive
|
||||
// -l: list files with matches only
|
||||
// --include: only .ts and .tsx files
|
||||
// --exclude: exclude test and story files
|
||||
const grepCommand = `grep -rl --include="*.ts" --include="*.tsx" --exclude="*.spec.ts" --exclude="*.spec.tsx" --exclude="*.test.ts" --exclude="*.test.tsx" --exclude="*.stories.tsx" "${searchPattern}" app/ 2>/dev/null | wc -l`
|
||||
|
||||
// eslint-disable-next-line sonarjs/os-command
|
||||
const result = execSync(grepCommand, {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
})
|
||||
|
||||
return Number.parseInt(result.trim(), 10) || 0
|
||||
}
|
||||
catch {
|
||||
// If command fails, return 0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate test priority based on complexity and usage
|
||||
*
|
||||
* Priority Score = Complexity Score + Usage Score
|
||||
* - Complexity: 0-100
|
||||
* - Usage: 0-50
|
||||
* - Total: 0-150
|
||||
*
|
||||
* Priority Levels:
|
||||
* - 0-30: Low
|
||||
* - 31-70: Medium
|
||||
* - 71-100: High
|
||||
* - 100+: Critical
|
||||
*/
|
||||
calculateTestPriority(complexity, usageCount) {
|
||||
const complexityScore = complexity
|
||||
|
||||
// Usage score calculation
|
||||
let usageScore
|
||||
if (usageCount === 0)
|
||||
usageScore = 0
|
||||
else if (usageCount <= 5)
|
||||
usageScore = 10
|
||||
else if (usageCount <= 20)
|
||||
usageScore = 20
|
||||
else if (usageCount <= 50)
|
||||
usageScore = 35
|
||||
else
|
||||
usageScore = 50
|
||||
|
||||
const totalScore = complexityScore + usageScore
|
||||
|
||||
return {
|
||||
score: totalScore,
|
||||
level: this.getPriorityLevel(totalScore),
|
||||
usageScore,
|
||||
complexityScore,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority level based on score
|
||||
*/
|
||||
getPriorityLevel(score) {
|
||||
if (score > 100) return '🔴 CRITICAL'
|
||||
if (score > 70) return '🟠 HIGH'
|
||||
if (score > 30) return '🟡 MEDIUM'
|
||||
return '🟢 LOW'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +276,8 @@ class ComponentAnalyzer {
|
|||
// ============================================================================
|
||||
|
||||
class CursorPromptBuilder {
|
||||
build(analysis, sourceCode) {
|
||||
const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx');
|
||||
build(analysis, _sourceCode) {
|
||||
const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx')
|
||||
|
||||
return `
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
|
|
@ -184,6 +293,8 @@ class CursorPromptBuilder {
|
|||
Type: ${analysis.type}
|
||||
Complexity: ${analysis.complexity} ${this.getComplexityLevel(analysis.complexity)}
|
||||
Lines: ${analysis.lineCount}
|
||||
Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ? 's' : ''}
|
||||
Test Priority: ${analysis.priority.score} ${analysis.priority.level}
|
||||
|
||||
Features Detected:
|
||||
${analysis.hasProps ? '✓' : '✗'} Props/TypeScript interfaces
|
||||
|
|
@ -192,10 +303,8 @@ Features Detected:
|
|||
${analysis.hasCallbacks ? '✓' : '✗'} Callbacks (useCallback)
|
||||
${analysis.hasMemo ? '✓' : '✗'} Memoization (useMemo)
|
||||
${analysis.hasEvents ? '✓' : '✗'} Event handlers
|
||||
${analysis.hasI18n ? '✓' : '✗'} Internationalization
|
||||
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
|
||||
${analysis.hasAPI ? '✓' : '✗'} API calls
|
||||
${analysis.isClientComponent ? '✓' : '✗'} Client component
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📝 TASK FOR CURSOR AI:
|
||||
|
|
@ -218,70 +327,172 @@ ${this.getSpecificGuidelines(analysis)}
|
|||
|
||||
Generate a comprehensive test file for @${analysis.path} following the project's testing guidelines in .cursorrules.
|
||||
|
||||
Focus on:
|
||||
Including but not limited to:
|
||||
${this.buildFocusPoints(analysis)}
|
||||
|
||||
Create the test file at: ${testPath}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
getComplexityLevel(score) {
|
||||
if (score < 10) return '🟢 Simple';
|
||||
if (score < 30) return '🟡 Medium';
|
||||
return '🔴 Complex';
|
||||
if (score < 10) return '🟢 Simple'
|
||||
if (score < 30) return '🟡 Medium'
|
||||
return '🔴 Complex'
|
||||
}
|
||||
|
||||
buildFocusPoints(analysis) {
|
||||
const points = [];
|
||||
const points = []
|
||||
|
||||
if (analysis.hasProps) points.push('- Testing all prop variations');
|
||||
if (analysis.hasState) points.push('- Testing state management and updates');
|
||||
if (analysis.hasEffects) points.push('- Testing side effects and cleanup');
|
||||
if (analysis.hasEvents) points.push('- Testing user interactions and event handlers');
|
||||
if (analysis.hasI18n) points.push('- Mocking i18n (useTranslation)');
|
||||
if (analysis.hasRouter) points.push('- Mocking Next.js router hooks');
|
||||
if (analysis.hasAPI) points.push('- Mocking API calls');
|
||||
if (analysis.hasState) points.push('- Testing state management and updates')
|
||||
if (analysis.hasEffects) points.push('- Testing side effects and cleanup')
|
||||
if (analysis.hasCallbacks) points.push('- Testing callback stability and memoization')
|
||||
if (analysis.hasMemo) points.push('- Testing memoization logic and dependencies')
|
||||
if (analysis.hasEvents) points.push('- Testing user interactions and event handlers')
|
||||
if (analysis.hasRouter) points.push('- Mocking Next.js router hooks')
|
||||
if (analysis.hasAPI) points.push('- Mocking API calls')
|
||||
points.push('- Testing edge cases and error handling')
|
||||
points.push('- Testing all prop variations')
|
||||
|
||||
points.push('- Testing accessibility (ARIA attributes)');
|
||||
points.push('- Testing edge cases and error handling');
|
||||
|
||||
return points.join('\n');
|
||||
return points.join('\n')
|
||||
}
|
||||
|
||||
getSpecificGuidelines(analysis) {
|
||||
const guidelines = [];
|
||||
const guidelines = []
|
||||
|
||||
if (analysis.complexity > 30) {
|
||||
guidelines.push('⚠️ This is a COMPLEX component. Consider:');
|
||||
guidelines.push(' - Breaking tests into multiple describe blocks');
|
||||
guidelines.push(' - Testing integration scenarios');
|
||||
guidelines.push(' - Adding performance tests');
|
||||
// ===== Test Priority Guidance =====
|
||||
if (analysis.priority.level.includes('CRITICAL')) {
|
||||
guidelines.push('🔴 CRITICAL PRIORITY component:')
|
||||
guidelines.push(` - Used in ${analysis.usageCount} places across the codebase`)
|
||||
guidelines.push(' - Changes will have WIDE impact')
|
||||
guidelines.push(' - Require comprehensive test coverage')
|
||||
guidelines.push(' - Add regression tests for all use cases')
|
||||
guidelines.push(' - Consider integration tests with dependent components')
|
||||
}
|
||||
else if (analysis.usageCount > 50) {
|
||||
guidelines.push('🟠 VERY HIGH USAGE component:')
|
||||
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
|
||||
guidelines.push(' - Changes may affect many parts of the application')
|
||||
guidelines.push(' - Comprehensive test coverage is CRITICAL')
|
||||
guidelines.push(' - Add tests for all common usage patterns')
|
||||
guidelines.push(' - Consider regression tests')
|
||||
}
|
||||
else if (analysis.usageCount > 20) {
|
||||
guidelines.push('🟡 HIGH USAGE component:')
|
||||
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
|
||||
guidelines.push(' - Test coverage is important to prevent widespread bugs')
|
||||
guidelines.push(' - Add tests for common usage patterns')
|
||||
}
|
||||
|
||||
// ===== Complexity Warning =====
|
||||
if (analysis.complexity > 50) {
|
||||
guidelines.push('🔴 VERY COMPLEX component detected. Consider:')
|
||||
guidelines.push(' - Splitting component into smaller pieces before testing')
|
||||
guidelines.push(' - Creating integration tests for complex workflows')
|
||||
guidelines.push(' - Using test.each() for data-driven tests')
|
||||
guidelines.push(' - Adding performance benchmarks')
|
||||
}
|
||||
else if (analysis.complexity > 30) {
|
||||
guidelines.push('⚠️ This is a COMPLEX component. Consider:')
|
||||
guidelines.push(' - Breaking tests into multiple describe blocks')
|
||||
guidelines.push(' - Testing integration scenarios')
|
||||
guidelines.push(' - Grouping related test cases')
|
||||
}
|
||||
|
||||
// ===== State Management =====
|
||||
if (analysis.hasState && analysis.hasEffects) {
|
||||
guidelines.push('🔄 State + Effects detected:')
|
||||
guidelines.push(' - Test state initialization and updates')
|
||||
guidelines.push(' - Test useEffect dependencies array')
|
||||
guidelines.push(' - Test cleanup functions (return from useEffect)')
|
||||
guidelines.push(' - Use waitFor() for async state changes')
|
||||
}
|
||||
else if (analysis.hasState) {
|
||||
guidelines.push('📊 State management detected:')
|
||||
guidelines.push(' - Test initial state values')
|
||||
guidelines.push(' - Test all state transitions')
|
||||
guidelines.push(' - Test state reset/cleanup scenarios')
|
||||
}
|
||||
else if (analysis.hasEffects) {
|
||||
guidelines.push('⚡ Side effects detected:')
|
||||
guidelines.push(' - Test effect execution conditions')
|
||||
guidelines.push(' - Verify dependencies array correctness')
|
||||
guidelines.push(' - Test cleanup on unmount')
|
||||
}
|
||||
|
||||
// ===== Performance Optimization =====
|
||||
if (analysis.hasCallbacks || analysis.hasMemo) {
|
||||
const features = []
|
||||
if (analysis.hasCallbacks) features.push('useCallback')
|
||||
if (analysis.hasMemo) features.push('useMemo')
|
||||
|
||||
guidelines.push(`🚀 Performance optimization (${features.join(', ')}):`)
|
||||
guidelines.push(' - Verify callbacks maintain referential equality')
|
||||
guidelines.push(' - Test memoization dependencies')
|
||||
guidelines.push(' - Ensure expensive computations are cached')
|
||||
}
|
||||
|
||||
// ===== API Calls =====
|
||||
if (analysis.hasAPI) {
|
||||
guidelines.push('🌐 This component makes API calls:');
|
||||
guidelines.push(' - Mock all API calls using jest.mock');
|
||||
guidelines.push(' - Test loading, success, and error states');
|
||||
guidelines.push(' - Test retry logic if applicable');
|
||||
guidelines.push('🌐 API calls detected:')
|
||||
guidelines.push(' - Mock all API calls using jest.mock')
|
||||
guidelines.push(' - Test retry logic if applicable')
|
||||
guidelines.push(' - Verify error handling and user feedback')
|
||||
}
|
||||
|
||||
// ===== Routing =====
|
||||
if (analysis.hasRouter) {
|
||||
guidelines.push('🔀 This component uses routing:');
|
||||
guidelines.push(' - Mock useRouter, usePathname, useSearchParams');
|
||||
guidelines.push(' - Test navigation behavior');
|
||||
guidelines.push(' - Test URL parameter handling');
|
||||
guidelines.push('🔀 Next.js routing detected:')
|
||||
guidelines.push(' - Mock useRouter, usePathname, useSearchParams')
|
||||
guidelines.push(' - Test navigation behavior and parameters')
|
||||
guidelines.push(' - Test query string handling')
|
||||
guidelines.push(' - Verify route guards/redirects if any')
|
||||
}
|
||||
|
||||
// ===== Event Handlers =====
|
||||
if (analysis.hasEvents) {
|
||||
guidelines.push('🎯 Event handlers detected:')
|
||||
guidelines.push(' - Test all onClick, onChange, onSubmit handlers')
|
||||
guidelines.push(' - Test keyboard events (Enter, Escape, etc.)')
|
||||
guidelines.push(' - Verify event.preventDefault() calls if needed')
|
||||
guidelines.push(' - Test event bubbling/propagation')
|
||||
}
|
||||
|
||||
// ===== Domain-Specific Components =====
|
||||
if (analysis.path.includes('workflow')) {
|
||||
guidelines.push('⚙️ This is a Workflow component:');
|
||||
guidelines.push(' - Test node configuration');
|
||||
guidelines.push(' - Test data validation');
|
||||
guidelines.push(' - Test variable passing');
|
||||
guidelines.push('⚙️ Workflow component:')
|
||||
guidelines.push(' - Test node configuration and validation')
|
||||
guidelines.push(' - Test data flow and variable passing')
|
||||
guidelines.push(' - Test edge connections and graph structure')
|
||||
guidelines.push(' - Verify error handling for invalid configs')
|
||||
}
|
||||
|
||||
return guidelines.length > 0 ? '\n' + guidelines.join('\n') + '\n' : '';
|
||||
if (analysis.path.includes('dataset')) {
|
||||
guidelines.push('📚 Dataset component:')
|
||||
guidelines.push(' - Test file upload and validation')
|
||||
guidelines.push(' - Test pagination and data loading')
|
||||
guidelines.push(' - Test search and filtering')
|
||||
guidelines.push(' - Verify data format handling')
|
||||
}
|
||||
|
||||
if (analysis.path.includes('app/configuration') || analysis.path.includes('config')) {
|
||||
guidelines.push('⚙️ Configuration component:')
|
||||
guidelines.push(' - Test form validation thoroughly')
|
||||
guidelines.push(' - Test save/reset functionality')
|
||||
guidelines.push(' - Test required vs optional fields')
|
||||
guidelines.push(' - Verify configuration persistence')
|
||||
}
|
||||
|
||||
// ===== File Size Warning =====
|
||||
if (analysis.lineCount > 500) {
|
||||
guidelines.push('📏 Large component (500+ lines):')
|
||||
guidelines.push(' - Consider splitting into smaller components')
|
||||
guidelines.push(' - Test major sections separately')
|
||||
guidelines.push(' - Use helper functions to reduce test complexity')
|
||||
}
|
||||
|
||||
return guidelines.length > 0 ? `\n${guidelines.join('\n')}\n` : ''
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +501,7 @@ Create the test file at: ${testPath}
|
|||
// ============================================================================
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error(`
|
||||
|
|
@ -305,43 +516,114 @@ Examples:
|
|||
|
||||
This tool analyzes your component and generates a prompt for Cursor AI.
|
||||
Copy the output and use it in Cursor Chat (Cmd+L) or Composer (Cmd+I).
|
||||
`);
|
||||
process.exit(1);
|
||||
`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const componentPath = args[0];
|
||||
const absolutePath = path.resolve(process.cwd(), componentPath);
|
||||
const componentPath = args[0]
|
||||
const absolutePath = path.resolve(process.cwd(), componentPath)
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
console.error(`❌ Error: File not found: ${componentPath}`);
|
||||
process.exit(1);
|
||||
console.error(`❌ Error: File not found: ${componentPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Read source code
|
||||
const sourceCode = fs.readFileSync(absolutePath, 'utf-8');
|
||||
const sourceCode = fs.readFileSync(absolutePath, 'utf-8')
|
||||
|
||||
// Analyze
|
||||
const analyzer = new ComponentAnalyzer();
|
||||
const analysis = analyzer.analyze(sourceCode, componentPath);
|
||||
const analyzer = new ComponentAnalyzer()
|
||||
const analysis = analyzer.analyze(sourceCode, componentPath)
|
||||
|
||||
// Check if component is too complex - suggest refactoring instead of testing
|
||||
if (analysis.complexity > 50 || analysis.lineCount > 300) {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ⚠️ COMPONENT TOO COMPLEX TO TEST ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📍 Component: ${analysis.name}
|
||||
📂 Path: ${analysis.path}
|
||||
|
||||
📊 Component Metrics:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Complexity: ${analysis.complexity} ${analysis.complexity > 50 ? '🔴 TOO HIGH' : '⚠️ WARNING'}
|
||||
Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '🔴 TOO LARGE' : '⚠️ WARNING'}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🚫 RECOMMENDATION: REFACTOR BEFORE TESTING
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
This component is too complex to test effectively. Please consider:
|
||||
|
||||
1️⃣ **Split into smaller components**
|
||||
- Extract reusable UI sections into separate components
|
||||
- Separate business logic from presentation
|
||||
- Create smaller, focused components (< 300 lines each)
|
||||
|
||||
2️⃣ **Extract custom hooks**
|
||||
- Move state management logic to custom hooks
|
||||
- Extract complex data transformation logic
|
||||
- Separate API calls into dedicated hooks
|
||||
|
||||
3️⃣ **Simplify logic**
|
||||
- Reduce nesting depth
|
||||
- Break down complex conditions
|
||||
- Extract helper functions
|
||||
|
||||
4️⃣ **After refactoring**
|
||||
- Run this tool again on each smaller component
|
||||
- Generate tests for the refactored components
|
||||
- Tests will be easier to write and maintain
|
||||
|
||||
💡 TIP: Aim for components with:
|
||||
- Complexity score < 30 (preferably < 20)
|
||||
- Line count < 300 (preferably < 200)
|
||||
- Single responsibility principle
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Build prompt for Cursor
|
||||
const builder = new CursorPromptBuilder();
|
||||
const prompt = builder.build(analysis, sourceCode);
|
||||
const builder = new CursorPromptBuilder()
|
||||
const prompt = builder.build(analysis, sourceCode)
|
||||
|
||||
// Output
|
||||
console.log(prompt);
|
||||
console.log(prompt)
|
||||
|
||||
// Also save to clipboard if pbcopy is available (macOS)
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('which pbcopy', { stdio: 'ignore' });
|
||||
const { spawnSync } = require('node:child_process')
|
||||
|
||||
// Save prompt to clipboard
|
||||
execSync('pbcopy', { input: prompt.split('📋 COPY THIS TO CURSOR:')[1].split('━━━')[0].trim() });
|
||||
console.log('\n📋 Prompt copied to clipboard! Paste it in Cursor Chat (Cmd+L).\n');
|
||||
} catch {
|
||||
// pbcopy not available, skip
|
||||
const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' })
|
||||
if (checkPbcopy.status !== 0) return
|
||||
const parts = prompt.split('📋 COPY THIS TO CURSOR:')
|
||||
if (parts.length < 2) return
|
||||
|
||||
const afterMarker = parts[1]
|
||||
const lines = afterMarker.split('\n')
|
||||
|
||||
const startIdx = lines.findIndex(line => line.includes('━━━')) + 1
|
||||
const endIdx = lines.findIndex((line, idx) => idx > startIdx && line.includes('━━━'))
|
||||
|
||||
if (startIdx === 0 || endIdx === -1) return
|
||||
|
||||
const copyContent = lines.slice(startIdx, endIdx).join('\n').trim()
|
||||
|
||||
if (!copyContent) return
|
||||
|
||||
const result = spawnSync('pbcopy', [], {
|
||||
input: copyContent,
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
if (result.status === 0)
|
||||
console.log('\n📋 Prompt copied to clipboard! Paste it in Cursor Chat (Cmd+L).\n')
|
||||
}
|
||||
catch {
|
||||
// pbcopy failed, but don't break the script
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,5 +631,4 @@ Copy the output and use it in Cursor Chat (Cmd+L) or Composer (Cmd+I).
|
|||
// Run
|
||||
// ============================================================================
|
||||
|
||||
main();
|
||||
|
||||
main()
|
||||
|
|
|
|||
Loading…
Reference in New Issue