chore: update README and CLI usage for cursor_gen, version bump to 1.0.1
- Changed CLI usage instructions from `dart run cursor_gen` to `cursor_gen` for global activation. - Updated project-brief.yaml example and README to reflect new command usage. - Added app_context section in project-brief.yaml for theme variants and RBAC roles. - Fixed bundled template resolution for local and global installs to prevent 'Template not found' errors. - Version bump to 1.0.1 with corresponding updates in CHANGELOG and pubspec.yaml.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// arch-guard.ts — Pre-commit hook: enforces {{ARCHITECTURE}} import boundaries
|
||||
// Generated for {{PROJECT_NAME}} ({{ARCHITECTURE}} architecture)
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
interface ArchRule {
|
||||
sourcePattern: RegExp;
|
||||
forbiddenImports: RegExp[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Architecture-specific rules for {{ARCHITECTURE}}
|
||||
const ARCH_RULES: ArchRule[] = [
|
||||
...getArchRules()
|
||||
];
|
||||
|
||||
function getArchRules(): ArchRule[] {
|
||||
const arch = '{{ARCH_RAW}}';
|
||||
|
||||
if (arch === 'clean') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /features\/\w+\/domain\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//],
|
||||
message: 'domain/ must not import from data/ or presentation/',
|
||||
},
|
||||
{
|
||||
sourcePattern: /features\/\w+\/presentation\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//],
|
||||
message: 'presentation/ must not import from data/ directly (use domain interfaces)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'feature_first') {
|
||||
return [
|
||||
{
|
||||
// A feature file must not import from another feature
|
||||
sourcePattern: /features\/(\w+)\//,
|
||||
forbiddenImports: [], // checked dynamically below
|
||||
message: 'Features must not import from other feature folders',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'mvvm') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /viewmodels\//,
|
||||
forbiddenImports: [/package:flutter\//, /widgets\//],
|
||||
message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getChangedDartFiles(): string[] {
|
||||
try {
|
||||
const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' });
|
||||
return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function checkFile(filePath: string): string[] {
|
||||
const violations: string[] = [];
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const imports = content.match(/^import\s+'[^']+'/gm) ?? [];
|
||||
|
||||
for (const rule of ARCH_RULES) {
|
||||
if (!rule.sourcePattern.test(filePath)) continue;
|
||||
|
||||
// Feature-first cross-feature check
|
||||
if ('{{ARCH_RAW}}' === 'feature_first') {
|
||||
const srcFeatureMatch = filePath.match(/features\/(\w+)\//);
|
||||
if (srcFeatureMatch) {
|
||||
const srcFeature = srcFeatureMatch[1];
|
||||
for (const imp of imports) {
|
||||
const impFeatureMatch = imp.match(/features\/(\w+)\//);
|
||||
if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}\n` +
|
||||
` Fix: Move shared code to core/ or shared/`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standard forbidden import check
|
||||
for (const imp of imports) {
|
||||
for (const forbidden of rule.forbiddenImports) {
|
||||
if (forbidden.test(imp)) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
const changedFiles = getChangedDartFiles();
|
||||
if (changedFiles.length === 0) {
|
||||
console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for {{ARCHITECTURE}} violations...`);
|
||||
|
||||
const allViolations: string[] = [];
|
||||
for (const file of changedFiles) {
|
||||
allViolations.push(...checkFile(file));
|
||||
}
|
||||
|
||||
if (allViolations.length > 0) {
|
||||
console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`);
|
||||
for (const v of allViolations) console.error(v + '\n');
|
||||
console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`);
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
function run(cmd: string): { stdout: string; code: number } {
|
||||
try {
|
||||
const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
return { stdout, code: 0 };
|
||||
} catch (e: any) {
|
||||
return { stdout: e.stdout ?? e.message, code: e.status ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 Running flutter analyze...');
|
||||
const analyze = run('flutter analyze --no-congratulate');
|
||||
if (analyze.code !== 0) {
|
||||
console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🎨 Checking dart format...');
|
||||
const format = run('dart format --output=none --set-exit-if-changed lib/ test/');
|
||||
if (format.code !== 0) {
|
||||
console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`);
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// grind-tests.ts — Pre-push hook: runs flutter test
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
console.log('🧪 Running flutter test...');
|
||||
try {
|
||||
execSync('flutter test --coverage', { stdio: 'inherit' });
|
||||
console.log(`${GREEN}✔ All tests passed${RESET}`);
|
||||
} catch {
|
||||
console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "pre-commit: flutter analyze",
|
||||
"command": "npx ts-node .cursor/hooks/flutter-analyze.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-commit: arch guard",
|
||||
"command": "npx ts-node .cursor/hooks/arch-guard.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-push: run tests",
|
||||
"command": "npx ts-node .cursor/hooks/grind-tests.ts",
|
||||
"events": ["pre-push"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user