mindsystem-cc 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ms-flutter-code-quality
|
|
3
|
+
description: Refactors Flutter/Dart code to follow quality guidelines. Applies code patterns, widget organization, folder structure, and simplification. Spawned by execute-phase/do-work.
|
|
4
|
+
model: sonnet
|
|
5
|
+
tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch
|
|
6
|
+
color: cyan
|
|
7
|
+
skills:
|
|
8
|
+
- flutter-code-quality
|
|
9
|
+
- flutter-code-simplification
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
You are an expert Flutter/Dart code quality specialist. Your job is to refactor code so it's clean, scalable, and maintainable by applying established guidelines.
|
|
13
|
+
|
|
14
|
+
**Core principle:** Apply the guidelines. Verify with tests. Report what was fixed.
|
|
15
|
+
|
|
16
|
+
<input_contract>
|
|
17
|
+
You receive:
|
|
18
|
+
- A list of files to refactor (via git diff or explicit list)
|
|
19
|
+
- Files are Flutter/Dart code (.dart extension)
|
|
20
|
+
|
|
21
|
+
You return:
|
|
22
|
+
- Refactored files that follow guidelines
|
|
23
|
+
- Verification results (analyze + test)
|
|
24
|
+
- Report of what was changed
|
|
25
|
+
</input_contract>
|
|
26
|
+
|
|
27
|
+
## Key Principles
|
|
28
|
+
|
|
29
|
+
### 1. Preserve Behavior (Non-negotiable)
|
|
30
|
+
Functionality comes before code quality. Only improve code quality if you can maintain functionality. Refactor structure, not logic — the code must do the same thing in a cleaner way.
|
|
31
|
+
|
|
32
|
+
### 2. Apply Guidelines
|
|
33
|
+
If code doesn't follow the guidelines, refactor it so it does. The guidelines exist to be applied, not considered.
|
|
34
|
+
|
|
35
|
+
### 3. Verify with Tests
|
|
36
|
+
Run `flutter analyze` and `flutter test` after changes. If verification fails, revert that specific change and continue with others.
|
|
37
|
+
|
|
38
|
+
### 4. Comprehensive Coverage
|
|
39
|
+
Apply four lenses:
|
|
40
|
+
1. Code quality patterns (anti-patterns, idioms, type safety)
|
|
41
|
+
2. Widget organization (build structure, consistent ordering)
|
|
42
|
+
3. Folder structure (flat, feature-based)
|
|
43
|
+
4. Simplification (clarity, DRY, remove unnecessary complexity)
|
|
44
|
+
|
|
45
|
+
## Four-Pass Refactoring
|
|
46
|
+
|
|
47
|
+
### Pass 1: Code Quality Patterns
|
|
48
|
+
|
|
49
|
+
Fetch guidelines first:
|
|
50
|
+
```
|
|
51
|
+
WebFetch: https://gist.githubusercontent.com/rolandtolnay/edf9ea7d5adf218f45accb3411f0627c/raw/flutter-code-quality-guidelines.md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Replace anti-patterns:
|
|
55
|
+
- `useState<bool>` for loading → provider state
|
|
56
|
+
- Manual try-catch in providers → `AsyncValue.guard()`
|
|
57
|
+
- `.toList()..sort()` → `.sorted()`
|
|
58
|
+
- Functions with 4+ params → define inside build()
|
|
59
|
+
- Hardcoded hex colors → `context.color.*`
|
|
60
|
+
- `.asData?.value` → `.value`
|
|
61
|
+
- Inline filtering → computed property on entity
|
|
62
|
+
|
|
63
|
+
Apply positive patterns:
|
|
64
|
+
- Sealed classes for complex state
|
|
65
|
+
- Records for multiple return values
|
|
66
|
+
- Computed properties on entities/enums
|
|
67
|
+
- `firstWhereOrNull` with fallbacks
|
|
68
|
+
- Immutable collection methods
|
|
69
|
+
|
|
70
|
+
### Pass 2: Widget Organization
|
|
71
|
+
|
|
72
|
+
Enforce build() structure:
|
|
73
|
+
- Order: providers → hooks → derived values → widget tree
|
|
74
|
+
- Local variables for unconditional widgets
|
|
75
|
+
- Builder functions for conditional rendering
|
|
76
|
+
- Extract file-private widgets to own file
|
|
77
|
+
- Move functions with 4+ params inside build()
|
|
78
|
+
|
|
79
|
+
Enforce async UX:
|
|
80
|
+
- Loading from provider state, not useState
|
|
81
|
+
- Error handling via `ref.listen` + toast
|
|
82
|
+
- First-load errors with retry button
|
|
83
|
+
|
|
84
|
+
### Pass 3: Folder Structure
|
|
85
|
+
|
|
86
|
+
Enforce organization:
|
|
87
|
+
- Feature-based folders
|
|
88
|
+
- Screens at feature root
|
|
89
|
+
- `widgets/` only when 2+ widgets
|
|
90
|
+
- `providers/` only when 2+ providers
|
|
91
|
+
- `domain/` for models and repositories
|
|
92
|
+
- Flatten deep `lib/features/x/presentation/` paths
|
|
93
|
+
|
|
94
|
+
### Pass 4: Simplification
|
|
95
|
+
|
|
96
|
+
Apply `flutter-code-simplification` skill principles:
|
|
97
|
+
|
|
98
|
+
- Repeated null-checks → extract to local variable
|
|
99
|
+
- Duplicated logic → extract to shared method
|
|
100
|
+
- Scattered boolean flags → consolidate to sealed class or enum
|
|
101
|
+
- Large build() methods → extract to builder methods
|
|
102
|
+
- Unnecessary indirection → simplify to direct calls
|
|
103
|
+
|
|
104
|
+
## Process
|
|
105
|
+
|
|
106
|
+
1. **Identify targets** - Parse scope to find modified .dart files
|
|
107
|
+
2. **Fetch guidelines** - WebFetch flutter-code-quality-guidelines.md from gist
|
|
108
|
+
3. **Refactor Pass 1** - Apply code quality patterns
|
|
109
|
+
4. **Refactor Pass 2** - Apply widget organization rules
|
|
110
|
+
5. **Refactor Pass 3** - Apply folder structure conventions
|
|
111
|
+
6. **Refactor Pass 4** - Apply simplification principles
|
|
112
|
+
7. **Verify** - Run `fvm flutter analyze` and `fvm flutter test`
|
|
113
|
+
8. **If verification fails** - Revert the failing change, continue with others
|
|
114
|
+
9. **Report** - Document what was refactored
|
|
115
|
+
|
|
116
|
+
<output_format>
|
|
117
|
+
|
|
118
|
+
**If changes were made:**
|
|
119
|
+
```
|
|
120
|
+
## Refactoring Complete
|
|
121
|
+
|
|
122
|
+
**Files:** [count] analyzed, [count] modified
|
|
123
|
+
|
|
124
|
+
### Code Quality
|
|
125
|
+
- `path/file.dart:42` - useState → provider state
|
|
126
|
+
- `path/file.dart:67` - .toList()..sort() → .sorted()
|
|
127
|
+
|
|
128
|
+
### Widget Organization
|
|
129
|
+
- `path/file.dart:120` - Reordered build(): providers → hooks → derived → tree
|
|
130
|
+
|
|
131
|
+
### Folder Structure
|
|
132
|
+
- Moved `path/nested/widget.dart` → `path/widget.dart`
|
|
133
|
+
|
|
134
|
+
### Simplification
|
|
135
|
+
- `path/file.dart:150` - Extracted repeated logic to `_buildHeader()`
|
|
136
|
+
|
|
137
|
+
### Verification
|
|
138
|
+
- flutter analyze: pass
|
|
139
|
+
- flutter test: pass
|
|
140
|
+
|
|
141
|
+
### Modified Files
|
|
142
|
+
[list of file paths]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**If no changes needed:**
|
|
146
|
+
```
|
|
147
|
+
## Refactoring Complete
|
|
148
|
+
|
|
149
|
+
**Files:** [count] analyzed, 0 modified
|
|
150
|
+
|
|
151
|
+
Code already follows guidelines.
|
|
152
|
+
|
|
153
|
+
### Verification
|
|
154
|
+
- flutter analyze: pass
|
|
155
|
+
- flutter test: pass
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
</output_format>
|
|
159
|
+
|
|
160
|
+
<success_criteria>
|
|
161
|
+
- All functionality preserved — no behavior changes
|
|
162
|
+
- Guidelines fetched from gist
|
|
163
|
+
- All target .dart files refactored through four passes
|
|
164
|
+
- Code follows guidelines after refactoring
|
|
165
|
+
- `flutter analyze` passes
|
|
166
|
+
- `flutter test` passes
|
|
167
|
+
- Report documents what was changed
|
|
168
|
+
</success_criteria>
|
|
@@ -4,11 +4,13 @@ description: Simplifies Flutter/Dart code for clarity, consistency, and maintain
|
|
|
4
4
|
model: sonnet
|
|
5
5
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
6
6
|
color: cyan
|
|
7
|
+
skills:
|
|
8
|
+
- flutter-code-simplification
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
You are an expert Flutter/Dart code simplification specialist. Your expertise lies in making code easier to read, understand, and maintain without changing what it does. You prioritize readable, explicit code over overly compact solutions.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Apply simplification principles and Flutter patterns from the `flutter-code-simplification` skill.
|
|
12
14
|
|
|
13
15
|
<input_contract>
|
|
14
16
|
You receive:
|
|
@@ -21,55 +23,6 @@ You return:
|
|
|
21
23
|
- If no changes needed: clear statement that code already follows good patterns
|
|
22
24
|
</input_contract>
|
|
23
25
|
|
|
24
|
-
## Key Principles
|
|
25
|
-
|
|
26
|
-
### 1. Preserve Functionality
|
|
27
|
-
Never change what the code does—only how it does it. All original features, outputs, and behaviors must remain intact.
|
|
28
|
-
|
|
29
|
-
### 2. Enhance Clarity
|
|
30
|
-
- Reduce unnecessary complexity and nesting
|
|
31
|
-
- Eliminate redundant code and abstractions
|
|
32
|
-
- Improve readability through clear naming
|
|
33
|
-
- Consolidate related logic and duplicates (DRY)
|
|
34
|
-
- Choose clarity over brevity—explicit code is often better than compact code
|
|
35
|
-
|
|
36
|
-
### 3. Maintain Balance
|
|
37
|
-
Avoid over-simplification that could:
|
|
38
|
-
- Create overly clever solutions that are hard to understand
|
|
39
|
-
- Combine too many concerns into single functions/components
|
|
40
|
-
- Remove helpful abstractions that improve code organization
|
|
41
|
-
- Prioritize "fewer lines" over readability
|
|
42
|
-
- Make code harder to debug or extend
|
|
43
|
-
|
|
44
|
-
### 4. Apply Judgment
|
|
45
|
-
Use your expertise to determine what improves the code. These principles guide your decisions—they are not a checklist. If a change doesn't clearly improve clarity while preserving behavior, don't make it.
|
|
46
|
-
|
|
47
|
-
## Flutter Patterns to Consider
|
|
48
|
-
|
|
49
|
-
These are common opportunities in Flutter/Dart code. Apply when they genuinely improve clarity.
|
|
50
|
-
|
|
51
|
-
**State & Data:**
|
|
52
|
-
- Scattered boolean flags → sealed class variants with switch expressions (when it consolidates and clarifies)
|
|
53
|
-
- Same parameters repeated across functions → records or typed classes
|
|
54
|
-
- Manual try-catch in providers → `AsyncValue.guard()` with centralized error handling
|
|
55
|
-
- Check `ref.mounted` after async operations
|
|
56
|
-
|
|
57
|
-
**Widget Structure:**
|
|
58
|
-
- Large `build()` methods → extract into local variables or builder methods
|
|
59
|
-
- Widgets with many boolean parameters → consider composition or typed mode objects
|
|
60
|
-
- Keep build() order: providers → hooks → derived values → widget tree
|
|
61
|
-
|
|
62
|
-
**Collections:**
|
|
63
|
-
- Mutation patterns → immutable methods (`.sorted()`, `.where()`, etc.)
|
|
64
|
-
- Null-unsafe access → `firstWhereOrNull` with fallbacks
|
|
65
|
-
- Repeated enum switches → computed properties on the enum itself
|
|
66
|
-
|
|
67
|
-
**Code Organization:**
|
|
68
|
-
- Duplicated logic across files → extract to shared location
|
|
69
|
-
- Related methods scattered in class → group by concern
|
|
70
|
-
- Unnecessary indirection (factories creating one type, wrappers adding no behavior) → use concrete types directly
|
|
71
|
-
- **Exception:** API layer interfaces with implementation in same file are intentional (interface provides at-a-glance documentation)
|
|
72
|
-
|
|
73
26
|
## Process
|
|
74
27
|
|
|
75
28
|
1. **Identify targets** - Parse scope to find modified .dart files
|
package/bin/install.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const readline = require('readline');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
|
|
8
9
|
// Colors (using 256-color mode for better terminal compatibility)
|
|
9
10
|
const cyan = '\x1b[38;5;37m'; // Closest to #2FA7A0 in 256-color palette
|
|
@@ -54,6 +55,7 @@ function parseConfigDirArg() {
|
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
57
|
const explicitConfigDir = parseConfigDirArg();
|
|
58
|
+
const hasForce = args.includes('--force') || args.includes('-f');
|
|
57
59
|
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
58
60
|
|
|
59
61
|
console.log(banner);
|
|
@@ -66,6 +68,7 @@ if (hasHelp) {
|
|
|
66
68
|
${cyan}-g, --global${reset} Install globally (to Claude config directory)
|
|
67
69
|
${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
|
|
68
70
|
${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
|
|
71
|
+
${cyan}-f, --force${reset} Overwrite modified files without prompting
|
|
69
72
|
${cyan}-h, --help${reset} Show this help message
|
|
70
73
|
|
|
71
74
|
${yellow}Examples:${reset}
|
|
@@ -99,6 +102,308 @@ function expandTilde(filePath) {
|
|
|
99
102
|
return filePath;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Compute SHA-256 checksum truncated to 16 chars
|
|
107
|
+
*/
|
|
108
|
+
function computeChecksum(content) {
|
|
109
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read and parse manifest file, return null if missing/corrupted
|
|
114
|
+
*/
|
|
115
|
+
function readManifest(claudeDir) {
|
|
116
|
+
const manifestPath = path.join(claudeDir, 'mindsystem', '.manifest.json');
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(manifestPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.log(` ${yellow}⚠${reset} Manifest corrupted, treating as fresh install`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write manifest JSON file
|
|
131
|
+
*/
|
|
132
|
+
function writeManifest(claudeDir, manifest) {
|
|
133
|
+
const manifestDir = path.join(claudeDir, 'mindsystem');
|
|
134
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
135
|
+
const manifestPath = path.join(manifestDir, '.manifest.json');
|
|
136
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if running in interactive TTY
|
|
141
|
+
*/
|
|
142
|
+
function isInteractive() {
|
|
143
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Recursively collect files with relative paths
|
|
148
|
+
* @param {string} baseDir - The base directory for relative path calculation
|
|
149
|
+
* @param {string} currentDir - The current directory being scanned
|
|
150
|
+
* @param {string} destPrefix - The destination prefix (e.g., 'commands/ms', 'agents')
|
|
151
|
+
* @returns {Array<{relativePath: string, absolutePath: string}>}
|
|
152
|
+
*/
|
|
153
|
+
function collectFiles(baseDir, currentDir, destPrefix) {
|
|
154
|
+
const files = [];
|
|
155
|
+
if (!fs.existsSync(currentDir)) {
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
162
|
+
const relativeToCurrent = path.relative(baseDir, absolutePath);
|
|
163
|
+
const relativePath = path.join(destPrefix, relativeToCurrent);
|
|
164
|
+
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
files.push(...collectFiles(baseDir, absolutePath, destPrefix));
|
|
167
|
+
} else {
|
|
168
|
+
files.push({ relativePath, absolutePath });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build complete list of files to install from all source directories
|
|
176
|
+
* @param {string} src - The source directory (mindsystem package root)
|
|
177
|
+
* @returns {Array<{relativePath: string, absolutePath: string}>}
|
|
178
|
+
*/
|
|
179
|
+
function buildInstallManifest(src) {
|
|
180
|
+
const files = [];
|
|
181
|
+
|
|
182
|
+
// commands/ms
|
|
183
|
+
const commandsSrc = path.join(src, 'commands', 'ms');
|
|
184
|
+
files.push(...collectFiles(commandsSrc, commandsSrc, 'commands/ms'));
|
|
185
|
+
|
|
186
|
+
// mindsystem
|
|
187
|
+
const mindsystemSrc = path.join(src, 'mindsystem');
|
|
188
|
+
files.push(...collectFiles(mindsystemSrc, mindsystemSrc, 'mindsystem'));
|
|
189
|
+
|
|
190
|
+
// agents
|
|
191
|
+
const agentsSrc = path.join(src, 'agents');
|
|
192
|
+
files.push(...collectFiles(agentsSrc, agentsSrc, 'agents'));
|
|
193
|
+
|
|
194
|
+
// scripts -> mindsystem/scripts
|
|
195
|
+
const scriptsSrc = path.join(src, 'scripts');
|
|
196
|
+
files.push(...collectFiles(scriptsSrc, scriptsSrc, 'mindsystem/scripts'));
|
|
197
|
+
|
|
198
|
+
// skills
|
|
199
|
+
const skillsSrc = path.join(src, 'skills');
|
|
200
|
+
files.push(...collectFiles(skillsSrc, skillsSrc, 'skills'));
|
|
201
|
+
|
|
202
|
+
// CHANGELOG.md -> mindsystem/CHANGELOG.md
|
|
203
|
+
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
204
|
+
if (fs.existsSync(changelogSrc)) {
|
|
205
|
+
files.push({ relativePath: 'mindsystem/CHANGELOG.md', absolutePath: changelogSrc });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return files;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Compare manifests to detect orphans and conflicts
|
|
213
|
+
* @param {Object|null} oldManifest - Previous manifest or null for fresh install
|
|
214
|
+
* @param {string} claudeDir - Target directory
|
|
215
|
+
* @param {Array} newFiles - Files to install
|
|
216
|
+
* @param {string} pathPrefix - Path prefix for content replacement
|
|
217
|
+
* @returns {{orphans: string[], conflicts: Array<{relativePath: string, reason: string}>}}
|
|
218
|
+
*/
|
|
219
|
+
function compareManifests(oldManifest, claudeDir, newFiles, pathPrefix) {
|
|
220
|
+
const orphans = [];
|
|
221
|
+
const conflicts = [];
|
|
222
|
+
|
|
223
|
+
// Build set of new file paths
|
|
224
|
+
const newFilePaths = new Set(newFiles.map(f => f.relativePath));
|
|
225
|
+
|
|
226
|
+
// Find orphans (files in old manifest but not in new)
|
|
227
|
+
if (oldManifest && oldManifest.files) {
|
|
228
|
+
for (const oldPath of Object.keys(oldManifest.files)) {
|
|
229
|
+
if (!newFilePaths.has(oldPath)) {
|
|
230
|
+
const fullPath = path.join(claudeDir, oldPath);
|
|
231
|
+
if (fs.existsSync(fullPath)) {
|
|
232
|
+
orphans.push(oldPath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Find conflicts (installed files modified by user)
|
|
239
|
+
if (oldManifest && oldManifest.files) {
|
|
240
|
+
for (const fileInfo of newFiles) {
|
|
241
|
+
const destPath = path.join(claudeDir, fileInfo.relativePath);
|
|
242
|
+
if (!fs.existsSync(destPath)) {
|
|
243
|
+
continue; // New file, no conflict
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const oldChecksum = oldManifest.files[fileInfo.relativePath];
|
|
247
|
+
if (!oldChecksum) {
|
|
248
|
+
continue; // File not in old manifest, treat as new
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Read current installed content
|
|
252
|
+
const installedContent = fs.readFileSync(destPath, 'utf8');
|
|
253
|
+
const installedChecksum = computeChecksum(installedContent);
|
|
254
|
+
|
|
255
|
+
// If installed file differs from what we last installed, it's been modified
|
|
256
|
+
if (installedChecksum !== oldChecksum) {
|
|
257
|
+
// Read source content with path replacement to compare
|
|
258
|
+
let sourceContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
|
|
259
|
+
if (fileInfo.absolutePath.endsWith('.md')) {
|
|
260
|
+
sourceContent = sourceContent.replace(/~\/\.claude\//g, pathPrefix);
|
|
261
|
+
}
|
|
262
|
+
const sourceChecksum = computeChecksum(sourceContent);
|
|
263
|
+
|
|
264
|
+
// Only conflict if source is also different (user modified AND we have changes)
|
|
265
|
+
if (sourceChecksum !== installedChecksum) {
|
|
266
|
+
conflicts.push({
|
|
267
|
+
relativePath: fileInfo.relativePath,
|
|
268
|
+
reason: 'locally modified'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { orphans, conflicts };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Interactive conflict resolution
|
|
280
|
+
* @param {Array} conflicts - List of conflicting files
|
|
281
|
+
* @param {boolean} forceOverwrite - Skip prompts and overwrite all
|
|
282
|
+
* @returns {Promise<{overwrite: Set<string>, keep: Set<string>}>}
|
|
283
|
+
*/
|
|
284
|
+
async function resolveConflicts(conflicts, forceOverwrite) {
|
|
285
|
+
const overwrite = new Set();
|
|
286
|
+
const keep = new Set();
|
|
287
|
+
|
|
288
|
+
if (conflicts.length === 0) {
|
|
289
|
+
return { overwrite, keep };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (forceOverwrite) {
|
|
293
|
+
for (const c of conflicts) {
|
|
294
|
+
overwrite.add(c.relativePath);
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${yellow}⚠${reset} Force overwriting ${conflicts.length} modified file(s)`);
|
|
297
|
+
return { overwrite, keep };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isInteractive()) {
|
|
301
|
+
for (const c of conflicts) {
|
|
302
|
+
overwrite.add(c.relativePath);
|
|
303
|
+
}
|
|
304
|
+
console.log(` ${yellow}⚠${reset} Non-interactive mode: overwriting ${conflicts.length} modified file(s)`);
|
|
305
|
+
return { overwrite, keep };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const rl = readline.createInterface({
|
|
309
|
+
input: process.stdin,
|
|
310
|
+
output: process.stdout
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
314
|
+
|
|
315
|
+
console.log(`\n ${yellow}${conflicts.length} file(s) have local modifications:${reset}\n`);
|
|
316
|
+
|
|
317
|
+
let overwriteAll = false;
|
|
318
|
+
let keepAll = false;
|
|
319
|
+
|
|
320
|
+
for (const conflict of conflicts) {
|
|
321
|
+
if (overwriteAll) {
|
|
322
|
+
overwrite.add(conflict.relativePath);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (keepAll) {
|
|
326
|
+
keep.add(conflict.relativePath);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(` ${dim}${conflict.relativePath}${reset}`);
|
|
331
|
+
const answer = await question(` [O]verwrite, [K]eep, [A]ll overwrite, [N]one keep? `);
|
|
332
|
+
|
|
333
|
+
switch (answer.toLowerCase().trim()) {
|
|
334
|
+
case 'o':
|
|
335
|
+
case 'overwrite':
|
|
336
|
+
overwrite.add(conflict.relativePath);
|
|
337
|
+
break;
|
|
338
|
+
case 'k':
|
|
339
|
+
case 'keep':
|
|
340
|
+
keep.add(conflict.relativePath);
|
|
341
|
+
break;
|
|
342
|
+
case 'a':
|
|
343
|
+
case 'all':
|
|
344
|
+
overwriteAll = true;
|
|
345
|
+
overwrite.add(conflict.relativePath);
|
|
346
|
+
break;
|
|
347
|
+
case 'n':
|
|
348
|
+
case 'none':
|
|
349
|
+
keepAll = true;
|
|
350
|
+
keep.add(conflict.relativePath);
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
// Default to overwrite
|
|
354
|
+
overwrite.add(conflict.relativePath);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
rl.close();
|
|
359
|
+
console.log('');
|
|
360
|
+
return { overwrite, keep };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Remove orphaned files and empty directories
|
|
365
|
+
* @param {string} claudeDir - Target directory
|
|
366
|
+
* @param {string[]} filesToRemove - Relative paths of files to remove
|
|
367
|
+
*/
|
|
368
|
+
function cleanupOrphanedFiles(claudeDir, filesToRemove) {
|
|
369
|
+
if (filesToRemove.length === 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const dirsToCheck = new Set();
|
|
374
|
+
|
|
375
|
+
for (const relativePath of filesToRemove) {
|
|
376
|
+
const fullPath = path.join(claudeDir, relativePath);
|
|
377
|
+
try {
|
|
378
|
+
if (fs.existsSync(fullPath)) {
|
|
379
|
+
fs.unlinkSync(fullPath);
|
|
380
|
+
console.log(` ${yellow}✗${reset} Removed ${relativePath}`);
|
|
381
|
+
// Track parent directories for cleanup
|
|
382
|
+
dirsToCheck.add(path.dirname(fullPath));
|
|
383
|
+
}
|
|
384
|
+
} catch (e) {
|
|
385
|
+
console.log(` ${yellow}⚠${reset} Failed to remove ${relativePath}: ${e.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Remove empty directories (deepest first)
|
|
390
|
+
const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
|
|
391
|
+
for (const dir of sortedDirs) {
|
|
392
|
+
try {
|
|
393
|
+
// Don't remove the claudeDir itself or its immediate children (commands, agents, etc.)
|
|
394
|
+
if (dir === claudeDir || path.dirname(dir) === claudeDir) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const entries = fs.readdirSync(dir);
|
|
398
|
+
if (entries.length === 0) {
|
|
399
|
+
fs.rmdirSync(dir);
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// Ignore errors (directory not empty or doesn't exist)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
102
407
|
/**
|
|
103
408
|
* Recursively copy directory, replacing paths in .md files
|
|
104
409
|
*/
|
|
@@ -144,10 +449,33 @@ function copyDir(srcDir, destDir) {
|
|
|
144
449
|
}
|
|
145
450
|
}
|
|
146
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Install a single file with path replacement
|
|
454
|
+
* @param {string} srcPath - Source file path
|
|
455
|
+
* @param {string} destPath - Destination file path
|
|
456
|
+
* @param {string} pathPrefix - Path prefix for .md file replacement
|
|
457
|
+
*/
|
|
458
|
+
function installFile(srcPath, destPath, pathPrefix) {
|
|
459
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
460
|
+
|
|
461
|
+
if (srcPath.endsWith('.md')) {
|
|
462
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
463
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
464
|
+
fs.writeFileSync(destPath, content);
|
|
465
|
+
} else {
|
|
466
|
+
fs.copyFileSync(srcPath, destPath);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Make shell scripts executable
|
|
470
|
+
if (srcPath.endsWith('.sh')) {
|
|
471
|
+
fs.chmodSync(destPath, '755');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
147
475
|
/**
|
|
148
476
|
* Install to the specified directory
|
|
149
477
|
*/
|
|
150
|
-
function install(isGlobal) {
|
|
478
|
+
async function install(isGlobal) {
|
|
151
479
|
const src = path.join(__dirname, '..');
|
|
152
480
|
// Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default ~/.claude
|
|
153
481
|
const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
@@ -168,93 +496,106 @@ function install(isGlobal) {
|
|
|
168
496
|
|
|
169
497
|
console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
|
|
170
498
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
499
|
+
// Phase 1: Read old manifest
|
|
500
|
+
const oldManifest = readManifest(claudeDir);
|
|
501
|
+
|
|
502
|
+
// Phase 2: Build list of files to install
|
|
503
|
+
const filesToInstall = buildInstallManifest(src);
|
|
504
|
+
|
|
505
|
+
// Phase 3: Compare and detect conflicts
|
|
506
|
+
const { orphans, conflicts } = compareManifests(oldManifest, claudeDir, filesToInstall, pathPrefix);
|
|
507
|
+
|
|
508
|
+
// Phase 4: Resolve conflicts interactively
|
|
509
|
+
const { overwrite, keep } = await resolveConflicts(conflicts, hasForce);
|
|
510
|
+
|
|
511
|
+
// Phase 5: Install files (skipping kept files)
|
|
512
|
+
const newManifestFiles = {};
|
|
513
|
+
const categories = {
|
|
514
|
+
'commands/ms': { count: 0, label: 'commands/ms' },
|
|
515
|
+
'mindsystem': { count: 0, label: 'mindsystem' },
|
|
516
|
+
'agents': { count: 0, label: 'agents' },
|
|
517
|
+
'mindsystem/scripts': { count: 0, label: 'scripts' },
|
|
518
|
+
'skills': { count: 0, label: 'skills' }
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
for (const fileInfo of filesToInstall) {
|
|
522
|
+
const destPath = path.join(claudeDir, fileInfo.relativePath);
|
|
523
|
+
|
|
524
|
+
// Skip files user wants to keep
|
|
525
|
+
if (keep.has(fileInfo.relativePath)) {
|
|
526
|
+
// Still need to track in manifest with current checksum
|
|
527
|
+
const installedContent = fs.readFileSync(destPath, 'utf8');
|
|
528
|
+
newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
174
531
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const msDest = path.join(commandsDir, 'ms');
|
|
178
|
-
copyWithPathReplacement(msSrc, msDest, pathPrefix);
|
|
179
|
-
console.log(` ${green}✓${reset} Installed commands/ms`);
|
|
532
|
+
// Install the file
|
|
533
|
+
installFile(fileInfo.absolutePath, destPath, pathPrefix);
|
|
180
534
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
535
|
+
// Compute checksum of installed content (after path replacement)
|
|
536
|
+
let installedContent;
|
|
537
|
+
if (fileInfo.absolutePath.endsWith('.md')) {
|
|
538
|
+
installedContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
|
|
539
|
+
installedContent = installedContent.replace(/~\/\.claude\//g, pathPrefix);
|
|
540
|
+
} else {
|
|
541
|
+
installedContent = fs.readFileSync(destPath, 'utf8');
|
|
542
|
+
}
|
|
543
|
+
newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
|
|
186
544
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
545
|
+
// Track category counts
|
|
546
|
+
for (const prefix of Object.keys(categories)) {
|
|
547
|
+
if (fileInfo.relativePath.startsWith(prefix)) {
|
|
548
|
+
categories[prefix].count++;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
193
552
|
}
|
|
194
553
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
fs.mkdirSync(scriptsDest, { recursive: true });
|
|
200
|
-
const scriptEntries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
|
|
201
|
-
for (const entry of scriptEntries) {
|
|
202
|
-
const srcPath = path.join(scriptsSrc, entry.name);
|
|
203
|
-
const destPath = path.join(scriptsDest, entry.name);
|
|
204
|
-
if (entry.isDirectory()) {
|
|
205
|
-
// Recursively copy directories (like ms-lookup/)
|
|
206
|
-
copyDir(srcPath, destPath);
|
|
207
|
-
} else {
|
|
208
|
-
fs.copyFileSync(srcPath, destPath);
|
|
209
|
-
// Make shell scripts executable
|
|
210
|
-
if (entry.name.endsWith('.sh')) {
|
|
211
|
-
fs.chmodSync(destPath, '755');
|
|
212
|
-
}
|
|
213
|
-
}
|
|
554
|
+
// Print install summaries
|
|
555
|
+
for (const [prefix, info] of Object.entries(categories)) {
|
|
556
|
+
if (info.count > 0) {
|
|
557
|
+
console.log(` ${green}✓${reset} Installed ${info.label}`);
|
|
214
558
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Phase 6: Write VERSION file
|
|
562
|
+
const versionDest = path.join(claudeDir, 'mindsystem', 'VERSION');
|
|
563
|
+
fs.writeFileSync(versionDest, pkg.version);
|
|
564
|
+
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
565
|
+
|
|
566
|
+
// Phase 7: Check Python for ms-lookup
|
|
567
|
+
const msLookupPath = path.join(claudeDir, 'mindsystem', 'scripts', 'ms-lookup');
|
|
568
|
+
if (fs.existsSync(msLookupPath)) {
|
|
569
|
+
try {
|
|
570
|
+
const { execSync } = require('child_process');
|
|
571
|
+
const pyVersion = execSync('python3 --version 2>&1', { encoding: 'utf8' });
|
|
572
|
+
const versionMatch = pyVersion.match(/(\d+)\.(\d+)/);
|
|
573
|
+
if (versionMatch) {
|
|
574
|
+
const [, major, minor] = versionMatch;
|
|
575
|
+
if (parseInt(major) < 3 || (parseInt(major) === 3 && parseInt(minor) < 9)) {
|
|
576
|
+
console.log(` ${yellow}⚠${reset} Python 3.9+ required for ms-lookup (found ${major}.${minor})`);
|
|
577
|
+
} else {
|
|
578
|
+
console.log(` ${green}✓${reset} Installed ms-lookup CLI (Python ${major}.${minor})`);
|
|
231
579
|
}
|
|
232
|
-
} catch (e) {
|
|
233
|
-
console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
|
|
234
580
|
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
|
|
235
583
|
}
|
|
236
584
|
}
|
|
237
585
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
copyWithPathReplacement(skillsSrc, skillsDest, pathPrefix);
|
|
243
|
-
console.log(` ${green}✓${reset} Installed skills`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Copy CHANGELOG.md
|
|
247
|
-
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
248
|
-
const changelogDest = path.join(claudeDir, 'mindsystem', 'CHANGELOG.md');
|
|
249
|
-
if (fs.existsSync(changelogSrc)) {
|
|
250
|
-
fs.copyFileSync(changelogSrc, changelogDest);
|
|
251
|
-
console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
|
|
586
|
+
// Phase 8: Cleanup orphaned files
|
|
587
|
+
if (orphans.length > 0) {
|
|
588
|
+
console.log('');
|
|
589
|
+
cleanupOrphanedFiles(claudeDir, orphans);
|
|
252
590
|
}
|
|
253
591
|
|
|
254
|
-
//
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
592
|
+
// Phase 9: Write new manifest
|
|
593
|
+
const newManifest = {
|
|
594
|
+
version: pkg.version,
|
|
595
|
+
installedAt: new Date().toISOString(),
|
|
596
|
+
files: newManifestFiles
|
|
597
|
+
};
|
|
598
|
+
writeManifest(claudeDir, newManifest);
|
|
258
599
|
|
|
259
600
|
console.log(`
|
|
260
601
|
${green}Done!${reset} Launch Claude Code and run ${cyan}/ms:help${reset}.
|
|
@@ -264,7 +605,7 @@ function install(isGlobal) {
|
|
|
264
605
|
/**
|
|
265
606
|
* Prompt for install location
|
|
266
607
|
*/
|
|
267
|
-
function promptLocation() {
|
|
608
|
+
async function promptLocation() {
|
|
268
609
|
const rl = readline.createInterface({
|
|
269
610
|
input: process.stdin,
|
|
270
611
|
output: process.stdout
|
|
@@ -280,25 +621,35 @@ function promptLocation() {
|
|
|
280
621
|
${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
|
|
281
622
|
`);
|
|
282
623
|
|
|
283
|
-
|
|
284
|
-
rl.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
624
|
+
return new Promise((resolve) => {
|
|
625
|
+
rl.question(` Choice ${dim}[1]${reset}: `, async (answer) => {
|
|
626
|
+
rl.close();
|
|
627
|
+
const choice = answer.trim() || '1';
|
|
628
|
+
const isGlobal = choice !== '2';
|
|
629
|
+
await install(isGlobal);
|
|
630
|
+
resolve();
|
|
631
|
+
});
|
|
288
632
|
});
|
|
289
633
|
}
|
|
290
634
|
|
|
291
635
|
// Main
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
636
|
+
async function main() {
|
|
637
|
+
if (hasGlobal && hasLocal) {
|
|
638
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
641
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
} else if (hasGlobal) {
|
|
644
|
+
await install(true);
|
|
645
|
+
} else if (hasLocal) {
|
|
646
|
+
await install(false);
|
|
647
|
+
} else {
|
|
648
|
+
await promptLocation();
|
|
649
|
+
}
|
|
304
650
|
}
|
|
651
|
+
|
|
652
|
+
main().catch((err) => {
|
|
653
|
+
console.error(` ${yellow}Error: ${err.message}${reset}`);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-code-quality
|
|
3
|
+
description: Flutter/Dart code quality, widget organization, and folder structure guidelines. Use when reviewing, refactoring, or cleaning up Flutter code after implementation.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: forgeblast
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
date: January 2026
|
|
9
|
+
argument-hint: <file-or-pattern>
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Flutter Code Quality
|
|
13
|
+
|
|
14
|
+
Comprehensive guidelines for Flutter/Dart code quality, widget organization, and folder structure. Combines pattern-level rules (anti-patterns, idioms) with structural guidance (build method organization, folder conventions).
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
1. Fetch flutter-code-quality-guidelines.md from the gist URL below (always fresh)
|
|
19
|
+
2. Apply embedded widget organization and folder structure rules
|
|
20
|
+
3. Check files against all guidelines
|
|
21
|
+
4. Output findings in terse `file:line` format
|
|
22
|
+
|
|
23
|
+
## Code Quality Guidelines (Remote)
|
|
24
|
+
|
|
25
|
+
Fetch fresh guidelines before each review:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
https://gist.githubusercontent.com/rolandtolnay/edf9ea7d5adf218f45accb3411f0627c/raw/flutter-code-quality-guidelines.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use WebFetch to retrieve. Contains: anti-patterns, widget patterns, state management, collections, hooks, theme/styling, etc.
|
|
32
|
+
|
|
33
|
+
## Widget Organization Guidelines (Embedded)
|
|
34
|
+
|
|
35
|
+
### Build Method Structure
|
|
36
|
+
|
|
37
|
+
Order inside `build()`:
|
|
38
|
+
1. Providers (reads/watches)
|
|
39
|
+
2. Hooks (if using flutter_hooks)
|
|
40
|
+
3. Derived variables needed for rendering
|
|
41
|
+
4. Widget variables (in render order)
|
|
42
|
+
|
|
43
|
+
### When to Use What
|
|
44
|
+
|
|
45
|
+
| Scenario | Pattern |
|
|
46
|
+
|----------|---------|
|
|
47
|
+
| Widget always shown, no conditions | Local variable: `final header = Container(...);` |
|
|
48
|
+
| Widget depends on condition/null check | Builder function: `Widget? _buildContent() { if (data == null) return null; ... }` |
|
|
49
|
+
| Subtree is large, reusable, or has own state | Extract to standalone widget in own file |
|
|
50
|
+
| Function needs 3 or fewer params | Define outside `build()` as class method |
|
|
51
|
+
| Function needs 4+ params (esp. hooks) | Define inside `build()` to capture scope |
|
|
52
|
+
|
|
53
|
+
### Rules
|
|
54
|
+
|
|
55
|
+
- **No file-private widgets** - If big enough to be a widget, it gets its own file
|
|
56
|
+
- **Define local variables in render order** - Top-to-bottom matches screen layout
|
|
57
|
+
- **Extract non-trivial conditions** - `final canSubmit = isValid && !isLoading && selectedId != null;`
|
|
58
|
+
- **Pass `WidgetRef` only** when function needs both ref and context - Use `ref.context` inside
|
|
59
|
+
|
|
60
|
+
### Async UX Conventions
|
|
61
|
+
|
|
62
|
+
- **Button loading** - Watch provider state, not separate `useState<bool>`
|
|
63
|
+
- **Retriable errors** - Listen to provider, show toast on error, user retries by tapping again
|
|
64
|
+
- **First-load errors** - Render error view with retry button that invalidates provider
|
|
65
|
+
|
|
66
|
+
### Sorting/Filtering
|
|
67
|
+
|
|
68
|
+
- Simple options: Use enum with computed properties
|
|
69
|
+
- Options with behavior: Use sealed class
|
|
70
|
+
- Complex multi-field filtering: Dedicated `Filter` class
|
|
71
|
+
|
|
72
|
+
## Folder Structure Guidelines (Embedded)
|
|
73
|
+
|
|
74
|
+
### Core Rules
|
|
75
|
+
|
|
76
|
+
1. **Organize by feature** - Each feature gets its own folder
|
|
77
|
+
2. **Screens at feature root** - `account_screen.dart`, `edit_account_screen.dart`
|
|
78
|
+
3. **Create subfolders only when justified:**
|
|
79
|
+
- `widgets/` when 2+ reusable widgets exist
|
|
80
|
+
- `providers/` when 2+ provider files exist
|
|
81
|
+
- `domain/` usually always (models, repositories)
|
|
82
|
+
4. **Split large features into subfeatures** - Each subfeature follows same rules
|
|
83
|
+
|
|
84
|
+
### Example
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
lib/features/
|
|
88
|
+
account/
|
|
89
|
+
account_screen.dart
|
|
90
|
+
edit_account_screen.dart
|
|
91
|
+
widgets/
|
|
92
|
+
account_avatar.dart
|
|
93
|
+
account_form.dart
|
|
94
|
+
providers/
|
|
95
|
+
account_provider.dart
|
|
96
|
+
domain/
|
|
97
|
+
account.dart
|
|
98
|
+
account_repository.dart
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Application Priority
|
|
102
|
+
|
|
103
|
+
When reviewing code, apply in this order:
|
|
104
|
+
|
|
105
|
+
1. **Code Quality Patterns** (from fetched gist) - Anti-patterns, idioms, provider patterns
|
|
106
|
+
2. **Widget Organization** (above) - Build structure, extraction rules, async UX
|
|
107
|
+
3. **Folder Structure** (above) - File placement, feature boundaries
|
|
108
|
+
|
|
109
|
+
## Anti-Patterns Quick Reference
|
|
110
|
+
|
|
111
|
+
Flag these patterns (details in fetched guidelines):
|
|
112
|
+
|
|
113
|
+
- `useState<bool>` for loading states
|
|
114
|
+
- Manual try-catch in provider actions
|
|
115
|
+
- `.toList()..sort()` instead of `.sorted()`
|
|
116
|
+
- `_handleAction(ref, controller, user, state)` with 4+ params
|
|
117
|
+
- Hardcoded hex colors
|
|
118
|
+
- Deep `lib/features/x/presentation/` directories
|
|
119
|
+
- Barrel files that only re-export
|
|
120
|
+
- Boolean flags instead of sealed classes
|
|
121
|
+
- Magic numbers scattered across widgets
|
|
122
|
+
- `where((e) => e.status == Status.active)` instead of computed property
|
|
123
|
+
- Generic provider names like `expansionVisibilityProvider`
|
|
124
|
+
- `.asData?.value` instead of `.value`
|
|
125
|
+
|
|
126
|
+
## Output Format
|
|
127
|
+
|
|
128
|
+
Group by file. Use `file:line` format. Terse findings.
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
## lib/home/home_screen.dart
|
|
132
|
+
|
|
133
|
+
lib/home/home_screen.dart:42 - useState for loading state -> use provider
|
|
134
|
+
lib/home/home_screen.dart:67 - .toList()..sort() -> .sorted()
|
|
135
|
+
lib/home/home_screen.dart:89 - hardcoded Color(0xFF...) -> context.color.*
|
|
136
|
+
|
|
137
|
+
## lib/models/user.dart
|
|
138
|
+
|
|
139
|
+
pass
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
State issue + location. Skip explanation unless fix non-obvious. No preamble.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-code-simplification
|
|
3
|
+
description: Flutter/Dart code simplification principles. Use when simplifying, refactoring, or cleaning up Flutter code for clarity and maintainability.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: forgeblast
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
date: January 2026
|
|
9
|
+
argument-hint: <file-or-pattern>
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Flutter Code Simplification
|
|
13
|
+
|
|
14
|
+
Principles and patterns for simplifying Flutter/Dart code. Simplification means making code easier to reason about — not making it shorter at the cost of clarity.
|
|
15
|
+
|
|
16
|
+
## Core Philosophy
|
|
17
|
+
|
|
18
|
+
**Clarity over brevity.** Explicit code is often better than compact code. The goal is code that's easy to read, understand, and maintain without changing what it does.
|
|
19
|
+
|
|
20
|
+
## Principles
|
|
21
|
+
|
|
22
|
+
### 1. Preserve Functionality
|
|
23
|
+
|
|
24
|
+
Never change what the code does — only how it does it. All original features, outputs, and behaviors must remain intact.
|
|
25
|
+
|
|
26
|
+
### 2. Enhance Clarity
|
|
27
|
+
|
|
28
|
+
- Reduce unnecessary complexity and nesting
|
|
29
|
+
- Eliminate redundant code and abstractions
|
|
30
|
+
- Improve readability through clear naming
|
|
31
|
+
- Consolidate related logic and duplicates (DRY)
|
|
32
|
+
- Choose clarity over brevity — explicit code is often better than compact code
|
|
33
|
+
|
|
34
|
+
### 3. Maintain Balance
|
|
35
|
+
|
|
36
|
+
Avoid over-simplification that could:
|
|
37
|
+
- Create overly clever solutions that are hard to understand
|
|
38
|
+
- Combine too many concerns into single functions/components
|
|
39
|
+
- Remove helpful abstractions that improve code organization
|
|
40
|
+
- Prioritize "fewer lines" over readability
|
|
41
|
+
- Make code harder to debug or extend
|
|
42
|
+
|
|
43
|
+
### 4. Apply Judgment
|
|
44
|
+
|
|
45
|
+
Use expertise to determine what improves the code. These principles guide decisions — they are not a checklist. If a change doesn't clearly improve clarity while preserving behavior, don't make it.
|
|
46
|
+
|
|
47
|
+
## Flutter Patterns
|
|
48
|
+
|
|
49
|
+
Common opportunities in Flutter/Dart code. Apply when they genuinely improve clarity.
|
|
50
|
+
|
|
51
|
+
### State & Data
|
|
52
|
+
|
|
53
|
+
| Pattern | Simplification |
|
|
54
|
+
|---------|----------------|
|
|
55
|
+
| Scattered boolean flags | Sealed class variants with switch expressions (when it consolidates and clarifies) |
|
|
56
|
+
| Same parameters repeated across functions | Records or typed classes |
|
|
57
|
+
| Manual try-catch in providers | `AsyncValue.guard()` with centralized error handling |
|
|
58
|
+
| Async operations without mount check | Check `ref.mounted` after async operations |
|
|
59
|
+
|
|
60
|
+
### Widget Structure
|
|
61
|
+
|
|
62
|
+
| Pattern | Simplification |
|
|
63
|
+
|---------|----------------|
|
|
64
|
+
| Large `build()` methods | Extract into local variables or builder methods |
|
|
65
|
+
| Widgets with many boolean parameters | Consider composition or typed mode objects |
|
|
66
|
+
| Unordered build() | Keep order: providers → hooks → derived values → widget tree |
|
|
67
|
+
|
|
68
|
+
### Collections
|
|
69
|
+
|
|
70
|
+
| Pattern | Simplification |
|
|
71
|
+
|---------|----------------|
|
|
72
|
+
| Mutation patterns | Immutable methods (`.sorted()`, `.where()`, etc.) |
|
|
73
|
+
| Null-unsafe access | `firstWhereOrNull` with fallbacks |
|
|
74
|
+
| Repeated enum switches | Computed properties on the enum itself |
|
|
75
|
+
|
|
76
|
+
### Code Organization
|
|
77
|
+
|
|
78
|
+
| Pattern | Simplification |
|
|
79
|
+
|---------|----------------|
|
|
80
|
+
| Duplicated logic across files | Extract to shared location |
|
|
81
|
+
| Related methods scattered in class | Group by concern |
|
|
82
|
+
| Unnecessary indirection (factories creating one type, wrappers adding no behavior) | Use concrete types directly |
|
|
83
|
+
|
|
84
|
+
**Exception:** API layer interfaces with implementation in same file are intentional — interface provides at-a-glance documentation.
|
|
85
|
+
|
|
86
|
+
## Output Format
|
|
87
|
+
|
|
88
|
+
Group by file. Use `file:line` format. Terse findings.
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
## lib/home/home_screen.dart
|
|
92
|
+
|
|
93
|
+
lib/home/home_screen.dart:42 - scattered booleans -> sealed class
|
|
94
|
+
lib/home/home_screen.dart:67 - .toList()..sort() -> .sorted()
|
|
95
|
+
lib/home/home_screen.dart:120 - large build() -> extract builder methods
|
|
96
|
+
|
|
97
|
+
## lib/models/user.dart
|
|
98
|
+
|
|
99
|
+
pass
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
State issue + location. Skip explanation unless fix non-obvious. No preamble.
|