claude-code-starter 0.2.0 → 0.4.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.
- package/README.md +24 -10
- package/dist/cli.js +1968 -206
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -163,6 +163,62 @@ function detectFrameworks(packageJson, files, rootDir) {
|
|
|
163
163
|
if (cargo.includes("axum")) add("axum");
|
|
164
164
|
if (cargo.includes("rocket")) add("rocket");
|
|
165
165
|
}
|
|
166
|
+
const packageSwiftPath = path.join(rootDir, "Package.swift");
|
|
167
|
+
const xcodeprojExists = files.some((f) => f.endsWith(".xcodeproj")) || files.some((f) => f.endsWith(".xcworkspace"));
|
|
168
|
+
if (fs.existsSync(packageSwiftPath)) {
|
|
169
|
+
const packageSwift = fs.readFileSync(packageSwiftPath, "utf-8").toLowerCase();
|
|
170
|
+
if (packageSwift.includes("vapor")) add("vapor");
|
|
171
|
+
}
|
|
172
|
+
if (xcodeprojExists || files.some((f) => f.endsWith(".swift"))) {
|
|
173
|
+
const srcFiles = listSourceFilesShallow(rootDir, [".swift"]);
|
|
174
|
+
const hasSwiftUI = srcFiles.some(
|
|
175
|
+
(f) => f.includes("ContentView") || f.includes("App.swift") || f.includes("@main struct")
|
|
176
|
+
);
|
|
177
|
+
const hasUIKit = srcFiles.some(
|
|
178
|
+
(f) => f.includes("ViewController") || f.includes("AppDelegate") || f.includes("SceneDelegate")
|
|
179
|
+
);
|
|
180
|
+
if (hasSwiftUI) add("swiftui");
|
|
181
|
+
if (hasUIKit) add("uikit");
|
|
182
|
+
for (const file of srcFiles.slice(0, 10)) {
|
|
183
|
+
try {
|
|
184
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
185
|
+
if (content.includes("import SwiftData") || content.includes("@Model")) add("swiftdata");
|
|
186
|
+
if (content.includes("import Combine") || content.includes("PassthroughSubject"))
|
|
187
|
+
add("combine");
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const gradlePath = path.join(rootDir, "build.gradle");
|
|
193
|
+
const gradleKtsPath = path.join(rootDir, "build.gradle.kts");
|
|
194
|
+
const appGradlePath = path.join(rootDir, "app", "build.gradle");
|
|
195
|
+
const appGradleKtsPath = path.join(rootDir, "app", "build.gradle.kts");
|
|
196
|
+
let gradleContent = "";
|
|
197
|
+
for (const gPath of [gradlePath, gradleKtsPath, appGradlePath, appGradleKtsPath]) {
|
|
198
|
+
if (fs.existsSync(gPath)) {
|
|
199
|
+
gradleContent += fs.readFileSync(gPath, "utf-8").toLowerCase();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (gradleContent) {
|
|
203
|
+
if (gradleContent.includes("com.android") || gradleContent.includes("android {")) {
|
|
204
|
+
if (gradleContent.includes("compose") || gradleContent.includes("androidx.compose") || gradleContent.includes("composeoptions")) {
|
|
205
|
+
add("jetpack-compose");
|
|
206
|
+
} else {
|
|
207
|
+
add("android-views");
|
|
208
|
+
}
|
|
209
|
+
if (gradleContent.includes("room") || gradleContent.includes("androidx.room")) {
|
|
210
|
+
add("room");
|
|
211
|
+
}
|
|
212
|
+
if (gradleContent.includes("hilt") || gradleContent.includes("dagger.hilt")) {
|
|
213
|
+
add("hilt");
|
|
214
|
+
}
|
|
215
|
+
if (gradleContent.includes("ktor")) {
|
|
216
|
+
add("ktor-android");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (gradleContent.includes("spring")) add("spring");
|
|
220
|
+
if (gradleContent.includes("quarkus")) add("quarkus");
|
|
221
|
+
}
|
|
166
222
|
return frameworks;
|
|
167
223
|
}
|
|
168
224
|
const allDeps = {
|
|
@@ -356,6 +412,27 @@ function hasDevDep(packageJson, dep) {
|
|
|
356
412
|
const deps = packageJson.dependencies || {};
|
|
357
413
|
return dep in devDeps || dep in deps;
|
|
358
414
|
}
|
|
415
|
+
function listSourceFilesShallow(rootDir, extensions) {
|
|
416
|
+
const files = [];
|
|
417
|
+
const ignoreDirs = ["node_modules", ".git", "build", "dist", "Pods", ".build", "DerivedData"];
|
|
418
|
+
function scan(dir, depth) {
|
|
419
|
+
if (depth > 2) return;
|
|
420
|
+
try {
|
|
421
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
422
|
+
if (ignoreDirs.includes(entry.name)) continue;
|
|
423
|
+
const fullPath = path.join(dir, entry.name);
|
|
424
|
+
if (entry.isDirectory()) {
|
|
425
|
+
scan(fullPath, depth + 1);
|
|
426
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
427
|
+
files.push(fullPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
scan(rootDir, 0);
|
|
434
|
+
return files;
|
|
435
|
+
}
|
|
359
436
|
function countSourceFiles(rootDir, _languages) {
|
|
360
437
|
const extensions = [
|
|
361
438
|
// JavaScript/TypeScript
|
|
@@ -539,6 +616,7 @@ function generateClaudeMd(projectInfo) {
|
|
|
539
616
|
sections.push("| `/status` | Show current task state |");
|
|
540
617
|
sections.push("| `/done` | Mark current task complete |");
|
|
541
618
|
sections.push("| `/analyze <area>` | Deep-dive into specific code |");
|
|
619
|
+
sections.push("| `/code-review` | Review changes for quality and security |");
|
|
542
620
|
sections.push("");
|
|
543
621
|
sections.push("## Common Operations");
|
|
544
622
|
sections.push("");
|
|
@@ -553,6 +631,18 @@ function generateClaudeMd(projectInfo) {
|
|
|
553
631
|
sections.push("3. **Test Before Done** - Run tests before marking complete");
|
|
554
632
|
sections.push("4. **Update State** - Keep task.md current as you work");
|
|
555
633
|
sections.push("5. **Match Patterns** - Follow existing code conventions");
|
|
634
|
+
sections.push("6. **TDD Workflow** - Write failing tests first, then implement");
|
|
635
|
+
sections.push("");
|
|
636
|
+
sections.push("## Quality Gates");
|
|
637
|
+
sections.push("");
|
|
638
|
+
sections.push("Enforced constraints for code quality:");
|
|
639
|
+
sections.push("");
|
|
640
|
+
sections.push("| Constraint | Limit | Action |");
|
|
641
|
+
sections.push("|------------|-------|--------|");
|
|
642
|
+
sections.push("| Lines per function | 20 max | Decompose immediately |");
|
|
643
|
+
sections.push("| Parameters per function | 3 max | Use options object |");
|
|
644
|
+
sections.push("| Lines per file | 200 max | Split by responsibility |");
|
|
645
|
+
sections.push("| Test coverage | 80% min | Add tests before merge |");
|
|
556
646
|
sections.push("");
|
|
557
647
|
sections.push("## Skills");
|
|
558
648
|
sections.push("");
|
|
@@ -757,9 +847,16 @@ function generateSettings(stack) {
|
|
|
757
847
|
}
|
|
758
848
|
function getSkillsForStack(stack) {
|
|
759
849
|
const skills = [
|
|
850
|
+
// Core methodology skills
|
|
760
851
|
{ name: "pattern-discovery", description: "Finding codebase patterns" },
|
|
761
852
|
{ name: "systematic-debugging", description: "Debugging approach" },
|
|
762
|
-
{ name: "testing-methodology", description: "Testing strategy" }
|
|
853
|
+
{ name: "testing-methodology", description: "Testing strategy" },
|
|
854
|
+
// Process discipline skills
|
|
855
|
+
{ name: "iterative-development", description: "TDD loops until tests pass" },
|
|
856
|
+
{ name: "commit-hygiene", description: "Atomic commits and PR size limits" },
|
|
857
|
+
{ name: "code-deduplication", description: "Prevent semantic code duplication" },
|
|
858
|
+
{ name: "simplicity-rules", description: "Code complexity constraints" },
|
|
859
|
+
{ name: "security", description: "Security patterns and secrets management" }
|
|
763
860
|
];
|
|
764
861
|
if (stack.frameworks.includes("nextjs")) {
|
|
765
862
|
skills.push({ name: "nextjs-patterns", description: "Next.js App Router patterns" });
|
|
@@ -776,6 +873,21 @@ function getSkillsForStack(stack) {
|
|
|
776
873
|
if (stack.frameworks.includes("prisma") || stack.frameworks.includes("drizzle")) {
|
|
777
874
|
skills.push({ name: "database-patterns", description: "Database and ORM patterns" });
|
|
778
875
|
}
|
|
876
|
+
if (stack.frameworks.includes("swiftui")) {
|
|
877
|
+
skills.push({ name: "swiftui-patterns", description: "SwiftUI declarative UI patterns" });
|
|
878
|
+
}
|
|
879
|
+
if (stack.frameworks.includes("uikit")) {
|
|
880
|
+
skills.push({ name: "uikit-patterns", description: "UIKit view controller patterns" });
|
|
881
|
+
}
|
|
882
|
+
if (stack.frameworks.includes("vapor")) {
|
|
883
|
+
skills.push({ name: "vapor-patterns", description: "Vapor server-side Swift patterns" });
|
|
884
|
+
}
|
|
885
|
+
if (stack.frameworks.includes("jetpack-compose")) {
|
|
886
|
+
skills.push({ name: "compose-patterns", description: "Jetpack Compose UI patterns" });
|
|
887
|
+
}
|
|
888
|
+
if (stack.frameworks.includes("android-views")) {
|
|
889
|
+
skills.push({ name: "android-views-patterns", description: "Android XML views patterns" });
|
|
890
|
+
}
|
|
779
891
|
return skills;
|
|
780
892
|
}
|
|
781
893
|
function generateSkills(stack) {
|
|
@@ -783,6 +895,11 @@ function generateSkills(stack) {
|
|
|
783
895
|
artifacts.push(generatePatternDiscoverySkill());
|
|
784
896
|
artifacts.push(generateSystematicDebuggingSkill());
|
|
785
897
|
artifacts.push(generateTestingMethodologySkill(stack));
|
|
898
|
+
artifacts.push(generateIterativeDevelopmentSkill(stack));
|
|
899
|
+
artifacts.push(generateCommitHygieneSkill());
|
|
900
|
+
artifacts.push(generateCodeDeduplicationSkill());
|
|
901
|
+
artifacts.push(generateSimplicityRulesSkill());
|
|
902
|
+
artifacts.push(generateSecuritySkill(stack));
|
|
786
903
|
if (stack.frameworks.includes("nextjs")) {
|
|
787
904
|
artifacts.push(generateNextJsSkill());
|
|
788
905
|
}
|
|
@@ -795,6 +912,21 @@ function generateSkills(stack) {
|
|
|
795
912
|
if (stack.frameworks.includes("nestjs")) {
|
|
796
913
|
artifacts.push(generateNestJSSkill());
|
|
797
914
|
}
|
|
915
|
+
if (stack.frameworks.includes("swiftui")) {
|
|
916
|
+
artifacts.push(generateSwiftUISkill());
|
|
917
|
+
}
|
|
918
|
+
if (stack.frameworks.includes("uikit")) {
|
|
919
|
+
artifacts.push(generateUIKitSkill());
|
|
920
|
+
}
|
|
921
|
+
if (stack.frameworks.includes("vapor")) {
|
|
922
|
+
artifacts.push(generateVaporSkill());
|
|
923
|
+
}
|
|
924
|
+
if (stack.frameworks.includes("jetpack-compose")) {
|
|
925
|
+
artifacts.push(generateJetpackComposeSkill());
|
|
926
|
+
}
|
|
927
|
+
if (stack.frameworks.includes("android-views")) {
|
|
928
|
+
artifacts.push(generateAndroidViewsSkill());
|
|
929
|
+
}
|
|
798
930
|
return artifacts;
|
|
799
931
|
}
|
|
800
932
|
function generatePatternDiscoverySkill() {
|
|
@@ -1634,236 +1766,1773 @@ describe('UsersService', () => {
|
|
|
1634
1766
|
isNew: true
|
|
1635
1767
|
};
|
|
1636
1768
|
}
|
|
1637
|
-
function
|
|
1638
|
-
const agents = [
|
|
1639
|
-
{ name: "code-reviewer", description: "Reviews code for quality and security" },
|
|
1640
|
-
{ name: "test-writer", description: "Generates tests for code" }
|
|
1641
|
-
];
|
|
1642
|
-
if (stack.hasDocker) {
|
|
1643
|
-
agents.push({ name: "docker-helper", description: "Helps with Docker and containerization" });
|
|
1644
|
-
}
|
|
1645
|
-
return agents;
|
|
1646
|
-
}
|
|
1647
|
-
function generateAgents(stack) {
|
|
1648
|
-
const artifacts = [];
|
|
1649
|
-
artifacts.push(generateCodeReviewerAgent(stack));
|
|
1650
|
-
artifacts.push(generateTestWriterAgent(stack));
|
|
1651
|
-
return artifacts;
|
|
1652
|
-
}
|
|
1653
|
-
function generateCodeReviewerAgent(stack) {
|
|
1654
|
-
const lintCommand = getLintCommand(stack);
|
|
1769
|
+
function generateSwiftUISkill() {
|
|
1655
1770
|
return {
|
|
1656
|
-
type: "
|
|
1657
|
-
path: ".claude/
|
|
1771
|
+
type: "skill",
|
|
1772
|
+
path: ".claude/skills/swiftui-patterns.md",
|
|
1658
1773
|
content: `---
|
|
1659
|
-
name:
|
|
1660
|
-
description:
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
model: sonnet
|
|
1774
|
+
name: swiftui-patterns
|
|
1775
|
+
description: SwiftUI declarative UI patterns and best practices
|
|
1776
|
+
globs:
|
|
1777
|
+
- "**/*.swift"
|
|
1664
1778
|
---
|
|
1665
1779
|
|
|
1666
|
-
|
|
1780
|
+
# SwiftUI Patterns
|
|
1667
1781
|
|
|
1668
|
-
##
|
|
1782
|
+
## View Structure
|
|
1669
1783
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
${stack.formatter === "prettier" ? "- `.prettierrc`" : ""}
|
|
1673
|
-
${stack.languages.includes("typescript") ? "- `tsconfig.json`" : ""}
|
|
1674
|
-
${stack.languages.includes("python") ? "- `pyproject.toml` or `setup.cfg`" : ""}
|
|
1784
|
+
\`\`\`swift
|
|
1785
|
+
import SwiftUI
|
|
1675
1786
|
|
|
1676
|
-
|
|
1787
|
+
struct ContentView: View {
|
|
1788
|
+
@State private var count = 0
|
|
1789
|
+
@StateObject private var viewModel = ContentViewModel()
|
|
1677
1790
|
|
|
1678
|
-
|
|
1791
|
+
var body: some View {
|
|
1792
|
+
VStack(spacing: 16) {
|
|
1793
|
+
Text("Count: \\(count)")
|
|
1794
|
+
.font(.title)
|
|
1679
1795
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1796
|
+
Button("Increment") {
|
|
1797
|
+
count += 1
|
|
1798
|
+
}
|
|
1799
|
+
.buttonStyle(.borderedProminent)
|
|
1800
|
+
}
|
|
1801
|
+
.padding()
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1687
1804
|
|
|
1688
|
-
|
|
1805
|
+
#Preview {
|
|
1806
|
+
ContentView()
|
|
1807
|
+
}
|
|
1808
|
+
\`\`\`
|
|
1689
1809
|
|
|
1690
|
-
|
|
1810
|
+
## Property Wrappers
|
|
1691
1811
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1812
|
+
| Wrapper | Use Case |
|
|
1813
|
+
|---------|----------|
|
|
1814
|
+
| \`@State\` | Simple value types owned by view |
|
|
1815
|
+
| \`@Binding\` | Two-way connection to parent's state |
|
|
1816
|
+
| \`@StateObject\` | Reference type owned by view (create once) |
|
|
1817
|
+
| \`@ObservedObject\` | Reference type passed from parent |
|
|
1818
|
+
| \`@EnvironmentObject\` | Shared data through view hierarchy |
|
|
1819
|
+
| \`@Environment\` | System environment values |
|
|
1695
1820
|
|
|
1696
|
-
|
|
1697
|
-
`,
|
|
1698
|
-
isNew: true
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
function generateTestWriterAgent(stack) {
|
|
1702
|
-
const testCommand = getTestCommand(stack);
|
|
1703
|
-
return {
|
|
1704
|
-
type: "agent",
|
|
1705
|
-
path: ".claude/agents/test-writer.md",
|
|
1706
|
-
content: `---
|
|
1707
|
-
name: test-writer
|
|
1708
|
-
description: Generates comprehensive tests for code
|
|
1709
|
-
tools: Read, Grep, Glob, Write, Edit, Bash(${testCommand})
|
|
1710
|
-
model: sonnet
|
|
1711
|
-
---
|
|
1821
|
+
## MVVM Pattern
|
|
1712
1822
|
|
|
1713
|
-
|
|
1823
|
+
\`\`\`swift
|
|
1824
|
+
// ViewModel
|
|
1825
|
+
@MainActor
|
|
1826
|
+
class UserViewModel: ObservableObject {
|
|
1827
|
+
@Published var users: [User] = []
|
|
1828
|
+
@Published var isLoading = false
|
|
1829
|
+
@Published var error: Error?
|
|
1714
1830
|
|
|
1715
|
-
|
|
1831
|
+
private let service: UserService
|
|
1716
1832
|
|
|
1717
|
-
|
|
1833
|
+
init(service: UserService = .shared) {
|
|
1834
|
+
self.service = service
|
|
1835
|
+
}
|
|
1718
1836
|
|
|
1719
|
-
|
|
1837
|
+
func fetchUsers() async {
|
|
1838
|
+
isLoading = true
|
|
1839
|
+
defer { isLoading = false }
|
|
1720
1840
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
4. Run tests to verify they pass
|
|
1841
|
+
do {
|
|
1842
|
+
users = try await service.getUsers()
|
|
1843
|
+
} catch {
|
|
1844
|
+
self.error = error
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1729
1848
|
|
|
1730
|
-
|
|
1849
|
+
// View
|
|
1850
|
+
struct UsersView: View {
|
|
1851
|
+
@StateObject private var viewModel = UserViewModel()
|
|
1731
1852
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1853
|
+
var body: some View {
|
|
1854
|
+
List(viewModel.users) { user in
|
|
1855
|
+
UserRow(user: user)
|
|
1856
|
+
}
|
|
1857
|
+
.overlay {
|
|
1858
|
+
if viewModel.isLoading {
|
|
1859
|
+
ProgressView()
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
.task {
|
|
1863
|
+
await viewModel.fetchUsers()
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
\`\`\`
|
|
1736
1868
|
|
|
1737
|
-
##
|
|
1869
|
+
## Navigation (iOS 16+)
|
|
1870
|
+
|
|
1871
|
+
\`\`\`swift
|
|
1872
|
+
struct AppNavigation: View {
|
|
1873
|
+
@State private var path = NavigationPath()
|
|
1874
|
+
|
|
1875
|
+
var body: some View {
|
|
1876
|
+
NavigationStack(path: $path) {
|
|
1877
|
+
HomeView()
|
|
1878
|
+
.navigationDestination(for: User.self) { user in
|
|
1879
|
+
UserDetailView(user: user)
|
|
1880
|
+
}
|
|
1881
|
+
.navigationDestination(for: Settings.self) { settings in
|
|
1882
|
+
SettingsView(settings: settings)
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
\`\`\`
|
|
1738
1888
|
|
|
1739
|
-
|
|
1740
|
-
- Descriptive test names
|
|
1741
|
-
- Mock external dependencies
|
|
1742
|
-
- Don't test implementation details
|
|
1743
|
-
- Aim for behavior coverage
|
|
1889
|
+
## Async/Await Patterns
|
|
1744
1890
|
|
|
1745
|
-
|
|
1891
|
+
\`\`\`swift
|
|
1892
|
+
// Task modifier for view lifecycle
|
|
1893
|
+
.task {
|
|
1894
|
+
await loadData()
|
|
1895
|
+
}
|
|
1746
1896
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1897
|
+
// Task with cancellation
|
|
1898
|
+
.task(id: searchText) {
|
|
1899
|
+
try? await Task.sleep(for: .milliseconds(300))
|
|
1900
|
+
await search(searchText)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Refreshable
|
|
1904
|
+
.refreshable {
|
|
1905
|
+
await viewModel.refresh()
|
|
1906
|
+
}
|
|
1749
1907
|
\`\`\`
|
|
1908
|
+
|
|
1909
|
+
## Best Practices
|
|
1910
|
+
|
|
1911
|
+
1. **Keep views small** - Extract subviews for reusability
|
|
1912
|
+
2. **Use \`@MainActor\`** - For ViewModels updating UI
|
|
1913
|
+
3. **Prefer value types** - Structs over classes when possible
|
|
1914
|
+
4. **Use \`.task\`** - Instead of \`.onAppear\` for async work
|
|
1915
|
+
5. **Preview extensively** - Use #Preview for rapid iteration
|
|
1750
1916
|
`,
|
|
1751
1917
|
isNew: true
|
|
1752
1918
|
};
|
|
1753
1919
|
}
|
|
1754
|
-
function
|
|
1755
|
-
switch (stack.linter) {
|
|
1756
|
-
case "eslint":
|
|
1757
|
-
return `${stack.packageManager === "bun" ? "bun" : "npx"} eslint .`;
|
|
1758
|
-
case "biome":
|
|
1759
|
-
return `${stack.packageManager === "bun" ? "bun" : "npx"} biome check .`;
|
|
1760
|
-
case "ruff":
|
|
1761
|
-
return "ruff check .";
|
|
1762
|
-
default:
|
|
1763
|
-
return "";
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
function getTestCommand(stack) {
|
|
1767
|
-
switch (stack.testingFramework) {
|
|
1768
|
-
case "vitest":
|
|
1769
|
-
return `${stack.packageManager || "npm"} ${stack.packageManager === "npm" ? "run " : ""}test`;
|
|
1770
|
-
case "jest":
|
|
1771
|
-
return `${stack.packageManager || "npm"} ${stack.packageManager === "npm" ? "run " : ""}test`;
|
|
1772
|
-
case "bun-test":
|
|
1773
|
-
return "bun test";
|
|
1774
|
-
case "pytest":
|
|
1775
|
-
return "pytest";
|
|
1776
|
-
case "go-test":
|
|
1777
|
-
return "go test ./...";
|
|
1778
|
-
case "rust-test":
|
|
1779
|
-
return "cargo test";
|
|
1780
|
-
default:
|
|
1781
|
-
return `${stack.packageManager || "npm"} test`;
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
function generateRules(stack) {
|
|
1785
|
-
const artifacts = [];
|
|
1786
|
-
if (stack.languages.includes("typescript")) {
|
|
1787
|
-
artifacts.push(generateTypeScriptRules());
|
|
1788
|
-
}
|
|
1789
|
-
if (stack.languages.includes("python")) {
|
|
1790
|
-
artifacts.push(generatePythonRules());
|
|
1791
|
-
}
|
|
1792
|
-
artifacts.push(generateCodeStyleRule(stack));
|
|
1793
|
-
return artifacts;
|
|
1794
|
-
}
|
|
1795
|
-
function generateTypeScriptRules() {
|
|
1920
|
+
function generateUIKitSkill() {
|
|
1796
1921
|
return {
|
|
1797
|
-
type: "
|
|
1798
|
-
path: ".claude/
|
|
1922
|
+
type: "skill",
|
|
1923
|
+
path: ".claude/skills/uikit-patterns.md",
|
|
1799
1924
|
content: `---
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1925
|
+
name: uikit-patterns
|
|
1926
|
+
description: UIKit view controller patterns and best practices
|
|
1927
|
+
globs:
|
|
1928
|
+
- "**/*.swift"
|
|
1803
1929
|
---
|
|
1804
1930
|
|
|
1805
|
-
#
|
|
1931
|
+
# UIKit Patterns
|
|
1806
1932
|
|
|
1807
|
-
##
|
|
1933
|
+
## View Controller Structure
|
|
1808
1934
|
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
- Use strict mode (\`strict: true\` in tsconfig)
|
|
1812
|
-
- Enable \`noUncheckedIndexedAccess\` for safer array access
|
|
1935
|
+
\`\`\`swift
|
|
1936
|
+
import UIKit
|
|
1813
1937
|
|
|
1814
|
-
|
|
1938
|
+
class UserViewController: UIViewController {
|
|
1939
|
+
// MARK: - Properties
|
|
1940
|
+
private let viewModel: UserViewModel
|
|
1941
|
+
private var cancellables = Set<AnyCancellable>()
|
|
1815
1942
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1943
|
+
// MARK: - UI Components
|
|
1944
|
+
private lazy var tableView: UITableView = {
|
|
1945
|
+
let table = UITableView()
|
|
1946
|
+
table.translatesAutoresizingMaskIntoConstraints = false
|
|
1947
|
+
table.delegate = self
|
|
1948
|
+
table.dataSource = self
|
|
1949
|
+
table.register(UserCell.self, forCellReuseIdentifier: UserCell.identifier)
|
|
1950
|
+
return table
|
|
1951
|
+
}()
|
|
1820
1952
|
|
|
1821
|
-
//
|
|
1822
|
-
|
|
1823
|
-
|
|
1953
|
+
// MARK: - Lifecycle
|
|
1954
|
+
init(viewModel: UserViewModel) {
|
|
1955
|
+
self.viewModel = viewModel
|
|
1956
|
+
super.init(nibName: nil, bundle: nil)
|
|
1957
|
+
}
|
|
1824
1958
|
|
|
1825
|
-
|
|
1959
|
+
required init?(coder: NSCoder) {
|
|
1960
|
+
fatalError("init(coder:) has not been implemented")
|
|
1961
|
+
}
|
|
1826
1962
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1963
|
+
override func viewDidLoad() {
|
|
1964
|
+
super.viewDidLoad()
|
|
1965
|
+
setupUI()
|
|
1966
|
+
setupBindings()
|
|
1967
|
+
viewModel.fetchUsers()
|
|
1968
|
+
}
|
|
1831
1969
|
|
|
1832
|
-
|
|
1970
|
+
// MARK: - Setup
|
|
1971
|
+
private func setupUI() {
|
|
1972
|
+
view.backgroundColor = .systemBackground
|
|
1973
|
+
view.addSubview(tableView)
|
|
1974
|
+
|
|
1975
|
+
NSLayoutConstraint.activate([
|
|
1976
|
+
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
1977
|
+
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
1978
|
+
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
1979
|
+
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
1980
|
+
])
|
|
1981
|
+
}
|
|
1833
1982
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1983
|
+
private func setupBindings() {
|
|
1984
|
+
viewModel.$users
|
|
1985
|
+
.receive(on: DispatchQueue.main)
|
|
1986
|
+
.sink { [weak self] _ in
|
|
1987
|
+
self?.tableView.reloadData()
|
|
1988
|
+
}
|
|
1989
|
+
.store(in: &cancellables)
|
|
1990
|
+
}
|
|
1840
1991
|
}
|
|
1841
|
-
|
|
1842
|
-
return {
|
|
1843
|
-
type: "rule",
|
|
1844
|
-
path: ".claude/rules/python.md",
|
|
1845
|
-
content: `---
|
|
1846
|
-
paths:
|
|
1847
|
-
- "**/*.py"
|
|
1848
|
-
---
|
|
1992
|
+
\`\`\`
|
|
1849
1993
|
|
|
1850
|
-
|
|
1994
|
+
## Coordinator Pattern
|
|
1851
1995
|
|
|
1852
|
-
|
|
1996
|
+
\`\`\`swift
|
|
1997
|
+
protocol Coordinator: AnyObject {
|
|
1998
|
+
var childCoordinators: [Coordinator] { get set }
|
|
1999
|
+
var navigationController: UINavigationController { get set }
|
|
2000
|
+
func start()
|
|
2001
|
+
}
|
|
1853
2002
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
- Max line length: 88 (Black default)
|
|
2003
|
+
class AppCoordinator: Coordinator {
|
|
2004
|
+
var childCoordinators: [Coordinator] = []
|
|
2005
|
+
var navigationController: UINavigationController
|
|
1858
2006
|
|
|
1859
|
-
|
|
2007
|
+
init(navigationController: UINavigationController) {
|
|
2008
|
+
self.navigationController = navigationController
|
|
2009
|
+
}
|
|
1860
2010
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2011
|
+
func start() {
|
|
2012
|
+
let vc = HomeViewController()
|
|
2013
|
+
vc.coordinator = self
|
|
2014
|
+
navigationController.pushViewController(vc, animated: false)
|
|
2015
|
+
}
|
|
1865
2016
|
|
|
1866
|
-
|
|
2017
|
+
func showUserDetail(_ user: User) {
|
|
2018
|
+
let vc = UserDetailViewController(user: user)
|
|
2019
|
+
navigationController.pushViewController(vc, animated: true)
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
\`\`\`
|
|
2023
|
+
|
|
2024
|
+
## Table View Cell
|
|
2025
|
+
|
|
2026
|
+
\`\`\`swift
|
|
2027
|
+
class UserCell: UITableViewCell {
|
|
2028
|
+
static let identifier = "UserCell"
|
|
2029
|
+
|
|
2030
|
+
private let nameLabel: UILabel = {
|
|
2031
|
+
let label = UILabel()
|
|
2032
|
+
label.font = .preferredFont(forTextStyle: .headline)
|
|
2033
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
2034
|
+
return label
|
|
2035
|
+
}()
|
|
2036
|
+
|
|
2037
|
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
2038
|
+
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
2039
|
+
setupUI()
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
required init?(coder: NSCoder) {
|
|
2043
|
+
fatalError("init(coder:) has not been implemented")
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private func setupUI() {
|
|
2047
|
+
contentView.addSubview(nameLabel)
|
|
2048
|
+
NSLayoutConstraint.activate([
|
|
2049
|
+
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
|
2050
|
+
nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
|
|
2051
|
+
])
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
func configure(with user: User) {
|
|
2055
|
+
nameLabel.text = user.name
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
\`\`\`
|
|
2059
|
+
|
|
2060
|
+
## Best Practices
|
|
2061
|
+
|
|
2062
|
+
1. **Use Auto Layout** - Programmatic constraints over Storyboards
|
|
2063
|
+
2. **MARK comments** - Organize code sections
|
|
2064
|
+
3. **Coordinator pattern** - For navigation logic
|
|
2065
|
+
4. **Dependency injection** - Pass dependencies via init
|
|
2066
|
+
5. **Combine for bindings** - Reactive updates from ViewModel
|
|
2067
|
+
`,
|
|
2068
|
+
isNew: true
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function generateVaporSkill() {
|
|
2072
|
+
return {
|
|
2073
|
+
type: "skill",
|
|
2074
|
+
path: ".claude/skills/vapor-patterns.md",
|
|
2075
|
+
content: `---
|
|
2076
|
+
name: vapor-patterns
|
|
2077
|
+
description: Vapor server-side Swift patterns
|
|
2078
|
+
globs:
|
|
2079
|
+
- "**/*.swift"
|
|
2080
|
+
- "Package.swift"
|
|
2081
|
+
---
|
|
2082
|
+
|
|
2083
|
+
# Vapor Patterns
|
|
2084
|
+
|
|
2085
|
+
## Route Structure
|
|
2086
|
+
|
|
2087
|
+
\`\`\`swift
|
|
2088
|
+
import Vapor
|
|
2089
|
+
|
|
2090
|
+
func routes(_ app: Application) throws {
|
|
2091
|
+
// Basic routes
|
|
2092
|
+
app.get { req in
|
|
2093
|
+
"Hello, world!"
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Route groups
|
|
2097
|
+
let api = app.grouped("api", "v1")
|
|
2098
|
+
|
|
2099
|
+
// Controller registration
|
|
2100
|
+
try api.register(collection: UserController())
|
|
2101
|
+
}
|
|
2102
|
+
\`\`\`
|
|
2103
|
+
|
|
2104
|
+
## Controller Pattern
|
|
2105
|
+
|
|
2106
|
+
\`\`\`swift
|
|
2107
|
+
import Vapor
|
|
2108
|
+
|
|
2109
|
+
struct UserController: RouteCollection {
|
|
2110
|
+
func boot(routes: RoutesBuilder) throws {
|
|
2111
|
+
let users = routes.grouped("users")
|
|
2112
|
+
|
|
2113
|
+
users.get(use: index)
|
|
2114
|
+
users.post(use: create)
|
|
2115
|
+
users.group(":userID") { user in
|
|
2116
|
+
user.get(use: show)
|
|
2117
|
+
user.put(use: update)
|
|
2118
|
+
user.delete(use: delete)
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// GET /users
|
|
2123
|
+
func index(req: Request) async throws -> [UserDTO] {
|
|
2124
|
+
try await User.query(on: req.db).all().map { $0.toDTO() }
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// POST /users
|
|
2128
|
+
func create(req: Request) async throws -> UserDTO {
|
|
2129
|
+
let input = try req.content.decode(CreateUserInput.self)
|
|
2130
|
+
let user = User(name: input.name, email: input.email)
|
|
2131
|
+
try await user.save(on: req.db)
|
|
2132
|
+
return user.toDTO()
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// GET /users/:userID
|
|
2136
|
+
func show(req: Request) async throws -> UserDTO {
|
|
2137
|
+
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
|
|
2138
|
+
throw Abort(.notFound)
|
|
2139
|
+
}
|
|
2140
|
+
return user.toDTO()
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
\`\`\`
|
|
2144
|
+
|
|
2145
|
+
## Fluent Models
|
|
2146
|
+
|
|
2147
|
+
\`\`\`swift
|
|
2148
|
+
import Fluent
|
|
2149
|
+
import Vapor
|
|
2150
|
+
|
|
2151
|
+
final class User: Model, Content {
|
|
2152
|
+
static let schema = "users"
|
|
2153
|
+
|
|
2154
|
+
@ID(key: .id)
|
|
2155
|
+
var id: UUID?
|
|
2156
|
+
|
|
2157
|
+
@Field(key: "name")
|
|
2158
|
+
var name: String
|
|
2159
|
+
|
|
2160
|
+
@Field(key: "email")
|
|
2161
|
+
var email: String
|
|
2162
|
+
|
|
2163
|
+
@Timestamp(key: "created_at", on: .create)
|
|
2164
|
+
var createdAt: Date?
|
|
2165
|
+
|
|
2166
|
+
@Children(for: \\.$user)
|
|
2167
|
+
var posts: [Post]
|
|
2168
|
+
|
|
2169
|
+
init() {}
|
|
2170
|
+
|
|
2171
|
+
init(id: UUID? = nil, name: String, email: String) {
|
|
2172
|
+
self.id = id
|
|
2173
|
+
self.name = name
|
|
2174
|
+
self.email = email
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Migration
|
|
2179
|
+
struct CreateUser: AsyncMigration {
|
|
2180
|
+
func prepare(on database: Database) async throws {
|
|
2181
|
+
try await database.schema("users")
|
|
2182
|
+
.id()
|
|
2183
|
+
.field("name", .string, .required)
|
|
2184
|
+
.field("email", .string, .required)
|
|
2185
|
+
.field("created_at", .datetime)
|
|
2186
|
+
.unique(on: "email")
|
|
2187
|
+
.create()
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
func revert(on database: Database) async throws {
|
|
2191
|
+
try await database.schema("users").delete()
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
\`\`\`
|
|
2195
|
+
|
|
2196
|
+
## DTOs and Validation
|
|
2197
|
+
|
|
2198
|
+
\`\`\`swift
|
|
2199
|
+
struct CreateUserInput: Content, Validatable {
|
|
2200
|
+
var name: String
|
|
2201
|
+
var email: String
|
|
2202
|
+
|
|
2203
|
+
static func validations(_ validations: inout Validations) {
|
|
2204
|
+
validations.add("name", as: String.self, is: !.empty)
|
|
2205
|
+
validations.add("email", as: String.self, is: .email)
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
struct UserDTO: Content {
|
|
2210
|
+
var id: UUID?
|
|
2211
|
+
var name: String
|
|
2212
|
+
var email: String
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
extension User {
|
|
2216
|
+
func toDTO() -> UserDTO {
|
|
2217
|
+
UserDTO(id: id, name: name, email: email)
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
\`\`\`
|
|
2221
|
+
|
|
2222
|
+
## Middleware
|
|
2223
|
+
|
|
2224
|
+
\`\`\`swift
|
|
2225
|
+
struct AuthMiddleware: AsyncMiddleware {
|
|
2226
|
+
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
|
2227
|
+
guard let token = request.headers.bearerAuthorization?.token else {
|
|
2228
|
+
throw Abort(.unauthorized)
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Validate token
|
|
2232
|
+
let user = try await validateToken(token, on: request)
|
|
2233
|
+
request.auth.login(user)
|
|
2234
|
+
|
|
2235
|
+
return try await next.respond(to: request)
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
\`\`\`
|
|
2239
|
+
|
|
2240
|
+
## Testing
|
|
2241
|
+
|
|
2242
|
+
\`\`\`swift
|
|
2243
|
+
@testable import App
|
|
2244
|
+
import XCTVapor
|
|
2245
|
+
|
|
2246
|
+
final class UserTests: XCTestCase {
|
|
2247
|
+
var app: Application!
|
|
2248
|
+
|
|
2249
|
+
override func setUp() async throws {
|
|
2250
|
+
app = Application(.testing)
|
|
2251
|
+
try configure(app)
|
|
2252
|
+
try await app.autoMigrate()
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
override func tearDown() async throws {
|
|
2256
|
+
try await app.autoRevert()
|
|
2257
|
+
app.shutdown()
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
func testCreateUser() async throws {
|
|
2261
|
+
try app.test(.POST, "api/v1/users", beforeRequest: { req in
|
|
2262
|
+
try req.content.encode(CreateUserInput(name: "Test", email: "test@example.com"))
|
|
2263
|
+
}, afterResponse: { res in
|
|
2264
|
+
XCTAssertEqual(res.status, .ok)
|
|
2265
|
+
let user = try res.content.decode(UserDTO.self)
|
|
2266
|
+
XCTAssertEqual(user.name, "Test")
|
|
2267
|
+
})
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
\`\`\`
|
|
2271
|
+
`,
|
|
2272
|
+
isNew: true
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function generateJetpackComposeSkill() {
|
|
2276
|
+
return {
|
|
2277
|
+
type: "skill",
|
|
2278
|
+
path: ".claude/skills/compose-patterns.md",
|
|
2279
|
+
content: `---
|
|
2280
|
+
name: compose-patterns
|
|
2281
|
+
description: Jetpack Compose UI patterns and best practices
|
|
2282
|
+
globs:
|
|
2283
|
+
- "**/*.kt"
|
|
2284
|
+
---
|
|
2285
|
+
|
|
2286
|
+
# Jetpack Compose Patterns
|
|
2287
|
+
|
|
2288
|
+
## Composable Structure
|
|
2289
|
+
|
|
2290
|
+
\`\`\`kotlin
|
|
2291
|
+
@Composable
|
|
2292
|
+
fun UserScreen(
|
|
2293
|
+
viewModel: UserViewModel = hiltViewModel()
|
|
2294
|
+
) {
|
|
2295
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
2296
|
+
|
|
2297
|
+
UserScreenContent(
|
|
2298
|
+
uiState = uiState,
|
|
2299
|
+
onRefresh = viewModel::refresh,
|
|
2300
|
+
onUserClick = viewModel::selectUser
|
|
2301
|
+
)
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
@Composable
|
|
2305
|
+
private fun UserScreenContent(
|
|
2306
|
+
uiState: UserUiState,
|
|
2307
|
+
onRefresh: () -> Unit,
|
|
2308
|
+
onUserClick: (User) -> Unit
|
|
2309
|
+
) {
|
|
2310
|
+
Scaffold(
|
|
2311
|
+
topBar = {
|
|
2312
|
+
TopAppBar(title = { Text("Users") })
|
|
2313
|
+
}
|
|
2314
|
+
) { padding ->
|
|
2315
|
+
when (uiState) {
|
|
2316
|
+
is UserUiState.Loading -> LoadingIndicator()
|
|
2317
|
+
is UserUiState.Success -> UserList(
|
|
2318
|
+
users = uiState.users,
|
|
2319
|
+
onUserClick = onUserClick,
|
|
2320
|
+
modifier = Modifier.padding(padding)
|
|
2321
|
+
)
|
|
2322
|
+
is UserUiState.Error -> ErrorMessage(uiState.message)
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
\`\`\`
|
|
2327
|
+
|
|
2328
|
+
## State Management
|
|
2329
|
+
|
|
2330
|
+
\`\`\`kotlin
|
|
2331
|
+
// UI State
|
|
2332
|
+
sealed interface UserUiState {
|
|
2333
|
+
object Loading : UserUiState
|
|
2334
|
+
data class Success(val users: List<User>) : UserUiState
|
|
2335
|
+
data class Error(val message: String) : UserUiState
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// ViewModel
|
|
2339
|
+
@HiltViewModel
|
|
2340
|
+
class UserViewModel @Inject constructor(
|
|
2341
|
+
private val repository: UserRepository
|
|
2342
|
+
) : ViewModel() {
|
|
2343
|
+
|
|
2344
|
+
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
|
|
2345
|
+
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
2346
|
+
|
|
2347
|
+
init {
|
|
2348
|
+
loadUsers()
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
fun refresh() {
|
|
2352
|
+
loadUsers()
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
private fun loadUsers() {
|
|
2356
|
+
viewModelScope.launch {
|
|
2357
|
+
_uiState.value = UserUiState.Loading
|
|
2358
|
+
repository.getUsers()
|
|
2359
|
+
.onSuccess { users ->
|
|
2360
|
+
_uiState.value = UserUiState.Success(users)
|
|
2361
|
+
}
|
|
2362
|
+
.onFailure { error ->
|
|
2363
|
+
_uiState.value = UserUiState.Error(error.message ?: "Unknown error")
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
\`\`\`
|
|
2369
|
+
|
|
2370
|
+
## Reusable Components
|
|
2371
|
+
|
|
2372
|
+
\`\`\`kotlin
|
|
2373
|
+
@Composable
|
|
2374
|
+
fun UserCard(
|
|
2375
|
+
user: User,
|
|
2376
|
+
onClick: () -> Unit,
|
|
2377
|
+
modifier: Modifier = Modifier
|
|
2378
|
+
) {
|
|
2379
|
+
Card(
|
|
2380
|
+
onClick = onClick,
|
|
2381
|
+
modifier = modifier.fillMaxWidth()
|
|
2382
|
+
) {
|
|
2383
|
+
Row(
|
|
2384
|
+
modifier = Modifier.padding(16.dp),
|
|
2385
|
+
verticalAlignment = Alignment.CenterVertically
|
|
2386
|
+
) {
|
|
2387
|
+
AsyncImage(
|
|
2388
|
+
model = user.avatarUrl,
|
|
2389
|
+
contentDescription = null,
|
|
2390
|
+
modifier = Modifier
|
|
2391
|
+
.size(48.dp)
|
|
2392
|
+
.clip(CircleShape)
|
|
2393
|
+
)
|
|
2394
|
+
Spacer(modifier = Modifier.width(16.dp))
|
|
2395
|
+
Column {
|
|
2396
|
+
Text(
|
|
2397
|
+
text = user.name,
|
|
2398
|
+
style = MaterialTheme.typography.titleMedium
|
|
2399
|
+
)
|
|
2400
|
+
Text(
|
|
2401
|
+
text = user.email,
|
|
2402
|
+
style = MaterialTheme.typography.bodySmall
|
|
2403
|
+
)
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
\`\`\`
|
|
2409
|
+
|
|
2410
|
+
## Navigation
|
|
2411
|
+
|
|
2412
|
+
\`\`\`kotlin
|
|
2413
|
+
@Composable
|
|
2414
|
+
fun AppNavigation() {
|
|
2415
|
+
val navController = rememberNavController()
|
|
2416
|
+
|
|
2417
|
+
NavHost(
|
|
2418
|
+
navController = navController,
|
|
2419
|
+
startDestination = "home"
|
|
2420
|
+
) {
|
|
2421
|
+
composable("home") {
|
|
2422
|
+
HomeScreen(
|
|
2423
|
+
onUserClick = { userId ->
|
|
2424
|
+
navController.navigate("user/$userId")
|
|
2425
|
+
}
|
|
2426
|
+
)
|
|
2427
|
+
}
|
|
2428
|
+
composable(
|
|
2429
|
+
route = "user/{userId}",
|
|
2430
|
+
arguments = listOf(navArgument("userId") { type = NavType.StringType })
|
|
2431
|
+
) { backStackEntry ->
|
|
2432
|
+
val userId = backStackEntry.arguments?.getString("userId")
|
|
2433
|
+
UserDetailScreen(userId = userId)
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
\`\`\`
|
|
2438
|
+
|
|
2439
|
+
## Theming
|
|
2440
|
+
|
|
2441
|
+
\`\`\`kotlin
|
|
2442
|
+
@Composable
|
|
2443
|
+
fun AppTheme(
|
|
2444
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
2445
|
+
content: @Composable () -> Unit
|
|
2446
|
+
) {
|
|
2447
|
+
val colorScheme = if (darkTheme) {
|
|
2448
|
+
darkColorScheme(
|
|
2449
|
+
primary = Purple80,
|
|
2450
|
+
secondary = PurpleGrey80
|
|
2451
|
+
)
|
|
2452
|
+
} else {
|
|
2453
|
+
lightColorScheme(
|
|
2454
|
+
primary = Purple40,
|
|
2455
|
+
secondary = PurpleGrey40
|
|
2456
|
+
)
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
MaterialTheme(
|
|
2460
|
+
colorScheme = colorScheme,
|
|
2461
|
+
typography = Typography,
|
|
2462
|
+
content = content
|
|
2463
|
+
)
|
|
2464
|
+
}
|
|
2465
|
+
\`\`\`
|
|
2466
|
+
|
|
2467
|
+
## Best Practices
|
|
2468
|
+
|
|
2469
|
+
1. **Stateless composables** - Pass state down, events up
|
|
2470
|
+
2. **Remember wisely** - Use \`remember\` for expensive calculations
|
|
2471
|
+
3. **Lifecycle-aware collection** - Use \`collectAsStateWithLifecycle()\`
|
|
2472
|
+
4. **Modifier parameter** - Always accept Modifier as last parameter
|
|
2473
|
+
5. **Preview annotations** - Add @Preview for rapid iteration
|
|
2474
|
+
`,
|
|
2475
|
+
isNew: true
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function generateAndroidViewsSkill() {
|
|
2479
|
+
return {
|
|
2480
|
+
type: "skill",
|
|
2481
|
+
path: ".claude/skills/android-views-patterns.md",
|
|
2482
|
+
content: `---
|
|
2483
|
+
name: android-views-patterns
|
|
2484
|
+
description: Android XML views and traditional patterns
|
|
2485
|
+
globs:
|
|
2486
|
+
- "**/*.kt"
|
|
2487
|
+
- "**/*.xml"
|
|
2488
|
+
---
|
|
2489
|
+
|
|
2490
|
+
# Android Views Patterns
|
|
2491
|
+
|
|
2492
|
+
## Activity Structure
|
|
2493
|
+
|
|
2494
|
+
\`\`\`kotlin
|
|
2495
|
+
class MainActivity : AppCompatActivity() {
|
|
2496
|
+
|
|
2497
|
+
private lateinit var binding: ActivityMainBinding
|
|
2498
|
+
private val viewModel: MainViewModel by viewModels()
|
|
2499
|
+
|
|
2500
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
2501
|
+
super.onCreate(savedInstanceState)
|
|
2502
|
+
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
2503
|
+
setContentView(binding.root)
|
|
2504
|
+
|
|
2505
|
+
setupUI()
|
|
2506
|
+
observeViewModel()
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
private fun setupUI() {
|
|
2510
|
+
binding.recyclerView.apply {
|
|
2511
|
+
layoutManager = LinearLayoutManager(this@MainActivity)
|
|
2512
|
+
adapter = userAdapter
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
binding.swipeRefresh.setOnRefreshListener {
|
|
2516
|
+
viewModel.refresh()
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
private fun observeViewModel() {
|
|
2521
|
+
lifecycleScope.launch {
|
|
2522
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
2523
|
+
viewModel.uiState.collect { state ->
|
|
2524
|
+
updateUI(state)
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
private fun updateUI(state: MainUiState) {
|
|
2531
|
+
binding.swipeRefresh.isRefreshing = state.isLoading
|
|
2532
|
+
userAdapter.submitList(state.users)
|
|
2533
|
+
binding.errorText.isVisible = state.error != null
|
|
2534
|
+
binding.errorText.text = state.error
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
\`\`\`
|
|
2538
|
+
|
|
2539
|
+
## Fragment Pattern
|
|
2540
|
+
|
|
2541
|
+
\`\`\`kotlin
|
|
2542
|
+
class UserFragment : Fragment(R.layout.fragment_user) {
|
|
2543
|
+
|
|
2544
|
+
private var _binding: FragmentUserBinding? = null
|
|
2545
|
+
private val binding get() = _binding!!
|
|
2546
|
+
|
|
2547
|
+
private val viewModel: UserViewModel by viewModels()
|
|
2548
|
+
private val args: UserFragmentArgs by navArgs()
|
|
2549
|
+
|
|
2550
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
2551
|
+
super.onViewCreated(view, savedInstanceState)
|
|
2552
|
+
_binding = FragmentUserBinding.bind(view)
|
|
2553
|
+
|
|
2554
|
+
setupUI()
|
|
2555
|
+
observeViewModel()
|
|
2556
|
+
viewModel.loadUser(args.userId)
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
override fun onDestroyView() {
|
|
2560
|
+
super.onDestroyView()
|
|
2561
|
+
_binding = null
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
\`\`\`
|
|
2565
|
+
|
|
2566
|
+
## RecyclerView Adapter
|
|
2567
|
+
|
|
2568
|
+
\`\`\`kotlin
|
|
2569
|
+
class UserAdapter(
|
|
2570
|
+
private val onItemClick: (User) -> Unit
|
|
2571
|
+
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
|
|
2572
|
+
|
|
2573
|
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
2574
|
+
val binding = ItemUserBinding.inflate(
|
|
2575
|
+
LayoutInflater.from(parent.context),
|
|
2576
|
+
parent,
|
|
2577
|
+
false
|
|
2578
|
+
)
|
|
2579
|
+
return ViewHolder(binding)
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
2583
|
+
holder.bind(getItem(position))
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
inner class ViewHolder(
|
|
2587
|
+
private val binding: ItemUserBinding
|
|
2588
|
+
) : RecyclerView.ViewHolder(binding.root) {
|
|
2589
|
+
|
|
2590
|
+
init {
|
|
2591
|
+
binding.root.setOnClickListener {
|
|
2592
|
+
onItemClick(getItem(adapterPosition))
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
fun bind(user: User) {
|
|
2597
|
+
binding.nameText.text = user.name
|
|
2598
|
+
binding.emailText.text = user.email
|
|
2599
|
+
Glide.with(binding.avatar)
|
|
2600
|
+
.load(user.avatarUrl)
|
|
2601
|
+
.circleCrop()
|
|
2602
|
+
.into(binding.avatar)
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
|
|
2607
|
+
override fun areItemsTheSame(oldItem: User, newItem: User) =
|
|
2608
|
+
oldItem.id == newItem.id
|
|
2609
|
+
|
|
2610
|
+
override fun areContentsTheSame(oldItem: User, newItem: User) =
|
|
2611
|
+
oldItem == newItem
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
\`\`\`
|
|
2615
|
+
|
|
2616
|
+
## XML Layout
|
|
2617
|
+
|
|
2618
|
+
\`\`\`xml
|
|
2619
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2620
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
2621
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2622
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
2623
|
+
android:layout_width="match_parent"
|
|
2624
|
+
android:layout_height="match_parent">
|
|
2625
|
+
|
|
2626
|
+
<com.google.android.material.appbar.MaterialToolbar
|
|
2627
|
+
android:id="@+id/toolbar"
|
|
2628
|
+
android:layout_width="0dp"
|
|
2629
|
+
android:layout_height="?attr/actionBarSize"
|
|
2630
|
+
app:title="Users"
|
|
2631
|
+
app:layout_constraintTop_toTopOf="parent"
|
|
2632
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
2633
|
+
app:layout_constraintEnd_toEndOf="parent" />
|
|
2634
|
+
|
|
2635
|
+
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
2636
|
+
android:id="@+id/swipeRefresh"
|
|
2637
|
+
android:layout_width="0dp"
|
|
2638
|
+
android:layout_height="0dp"
|
|
2639
|
+
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
2640
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
2641
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
2642
|
+
app:layout_constraintEnd_toEndOf="parent">
|
|
2643
|
+
|
|
2644
|
+
<androidx.recyclerview.widget.RecyclerView
|
|
2645
|
+
android:id="@+id/recyclerView"
|
|
2646
|
+
android:layout_width="match_parent"
|
|
2647
|
+
android:layout_height="match_parent" />
|
|
2648
|
+
|
|
2649
|
+
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
2650
|
+
|
|
2651
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
2652
|
+
\`\`\`
|
|
2653
|
+
|
|
2654
|
+
## ViewModel with Repository
|
|
2655
|
+
|
|
2656
|
+
\`\`\`kotlin
|
|
2657
|
+
@HiltViewModel
|
|
2658
|
+
class UserViewModel @Inject constructor(
|
|
2659
|
+
private val repository: UserRepository,
|
|
2660
|
+
private val savedStateHandle: SavedStateHandle
|
|
2661
|
+
) : ViewModel() {
|
|
2662
|
+
|
|
2663
|
+
private val _uiState = MutableStateFlow(UserUiState())
|
|
2664
|
+
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
2665
|
+
|
|
2666
|
+
fun loadUsers() {
|
|
2667
|
+
viewModelScope.launch {
|
|
2668
|
+
_uiState.update { it.copy(isLoading = true) }
|
|
2669
|
+
try {
|
|
2670
|
+
val users = repository.getUsers()
|
|
2671
|
+
_uiState.update { it.copy(users = users, isLoading = false) }
|
|
2672
|
+
} catch (e: Exception) {
|
|
2673
|
+
_uiState.update { it.copy(error = e.message, isLoading = false) }
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
data class UserUiState(
|
|
2680
|
+
val users: List<User> = emptyList(),
|
|
2681
|
+
val isLoading: Boolean = false,
|
|
2682
|
+
val error: String? = null
|
|
2683
|
+
)
|
|
2684
|
+
\`\`\`
|
|
2685
|
+
|
|
2686
|
+
## Best Practices
|
|
2687
|
+
|
|
2688
|
+
1. **View Binding** - Use over findViewById or synthetic imports
|
|
2689
|
+
2. **Lifecycle awareness** - Collect flows in repeatOnLifecycle
|
|
2690
|
+
3. **ListAdapter** - For efficient RecyclerView updates
|
|
2691
|
+
4. **Navigation Component** - For fragment navigation
|
|
2692
|
+
5. **Clean up bindings** - Set to null in onDestroyView
|
|
2693
|
+
`,
|
|
2694
|
+
isNew: true
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function generateIterativeDevelopmentSkill(stack) {
|
|
2698
|
+
const testCmd = getTestCommand(stack);
|
|
2699
|
+
const lintCmd = getLintCommand(stack);
|
|
2700
|
+
return {
|
|
2701
|
+
type: "skill",
|
|
2702
|
+
path: ".claude/skills/iterative-development.md",
|
|
2703
|
+
content: `---
|
|
2704
|
+
name: iterative-development
|
|
2705
|
+
description: TDD-driven iterative loops until tests pass
|
|
2706
|
+
globs:
|
|
2707
|
+
- "**/*.ts"
|
|
2708
|
+
- "**/*.tsx"
|
|
2709
|
+
- "**/*.js"
|
|
2710
|
+
- "**/*.py"
|
|
2711
|
+
- "**/*.go"
|
|
2712
|
+
---
|
|
2713
|
+
|
|
2714
|
+
# Iterative Development (TDD Loops)
|
|
2715
|
+
|
|
2716
|
+
Self-referential development loops where you iterate until completion criteria are met.
|
|
2717
|
+
|
|
2718
|
+
## Core Philosophy
|
|
2719
|
+
|
|
2720
|
+
\`\`\`
|
|
2721
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
2722
|
+
\u2502 ITERATION > PERFECTION \u2502
|
|
2723
|
+
\u2502 Don't aim for perfect on first try. \u2502
|
|
2724
|
+
\u2502 Let the loop refine the work. \u2502
|
|
2725
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
2726
|
+
\u2502 FAILURES ARE DATA \u2502
|
|
2727
|
+
\u2502 Failed tests, lint errors, type mismatches are signals. \u2502
|
|
2728
|
+
\u2502 Use them to guide the next iteration. \u2502
|
|
2729
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
2730
|
+
\u2502 CLEAR COMPLETION CRITERIA \u2502
|
|
2731
|
+
\u2502 Define exactly what "done" looks like. \u2502
|
|
2732
|
+
\u2502 Tests passing. Coverage met. Lint clean. \u2502
|
|
2733
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
2734
|
+
\`\`\`
|
|
2735
|
+
|
|
2736
|
+
## TDD Workflow (Mandatory)
|
|
2737
|
+
|
|
2738
|
+
Every implementation task MUST follow this workflow:
|
|
2739
|
+
|
|
2740
|
+
### 1. RED: Write Tests First
|
|
2741
|
+
\`\`\`bash
|
|
2742
|
+
# Write tests based on requirements
|
|
2743
|
+
# Run tests - they MUST FAIL
|
|
2744
|
+
${testCmd}
|
|
2745
|
+
\`\`\`
|
|
2746
|
+
|
|
2747
|
+
### 2. GREEN: Implement Feature
|
|
2748
|
+
\`\`\`bash
|
|
2749
|
+
# Write minimum code to pass tests
|
|
2750
|
+
# Run tests - they MUST PASS
|
|
2751
|
+
${testCmd}
|
|
2752
|
+
\`\`\`
|
|
2753
|
+
|
|
2754
|
+
### 3. VALIDATE: Quality Gates
|
|
2755
|
+
\`\`\`bash
|
|
2756
|
+
# Full quality check
|
|
2757
|
+
${lintCmd ? `${lintCmd} && ` : ""}${testCmd}
|
|
2758
|
+
\`\`\`
|
|
2759
|
+
|
|
2760
|
+
## Completion Criteria Template
|
|
2761
|
+
|
|
2762
|
+
For any implementation task, define:
|
|
2763
|
+
|
|
2764
|
+
\`\`\`markdown
|
|
2765
|
+
### Completion Criteria
|
|
2766
|
+
- [ ] All tests passing
|
|
2767
|
+
- [ ] Coverage >= 80% (on new code)
|
|
2768
|
+
- [ ] Lint clean (no errors)
|
|
2769
|
+
- [ ] Type check passing
|
|
2770
|
+
\`\`\`
|
|
2771
|
+
|
|
2772
|
+
## When to Use This Workflow
|
|
2773
|
+
|
|
2774
|
+
| Task Type | Use TDD Loop? |
|
|
2775
|
+
|-----------|---------------|
|
|
2776
|
+
| New feature | \u2705 Always |
|
|
2777
|
+
| Bug fix | \u2705 Always (write test that reproduces bug first) |
|
|
2778
|
+
| Refactoring | \u2705 Always (existing tests must stay green) |
|
|
2779
|
+
| Spike/exploration | \u274C Skip (but document findings) |
|
|
2780
|
+
| Documentation | \u274C Skip |
|
|
2781
|
+
|
|
2782
|
+
## Anti-Patterns
|
|
2783
|
+
|
|
2784
|
+
- \u274C Writing code before tests
|
|
2785
|
+
- \u274C Skipping the RED phase (tests that never fail are useless)
|
|
2786
|
+
- \u274C Moving on when tests fail
|
|
2787
|
+
- \u274C Large batches (prefer small, focused iterations)
|
|
2788
|
+
`,
|
|
2789
|
+
isNew: true
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function generateCommitHygieneSkill() {
|
|
2793
|
+
return {
|
|
2794
|
+
type: "skill",
|
|
2795
|
+
path: ".claude/skills/commit-hygiene.md",
|
|
2796
|
+
content: `---
|
|
2797
|
+
name: commit-hygiene
|
|
2798
|
+
description: Atomic commits, PR size limits, commit thresholds
|
|
2799
|
+
globs:
|
|
2800
|
+
- "**/*"
|
|
2801
|
+
---
|
|
2802
|
+
|
|
2803
|
+
# Commit Hygiene
|
|
2804
|
+
|
|
2805
|
+
Keep commits atomic, PRs reviewable, and git history clean.
|
|
2806
|
+
|
|
2807
|
+
## Size Thresholds
|
|
2808
|
+
|
|
2809
|
+
| Metric | \u{1F7E2} Good | \u{1F7E1} Warning | \u{1F534} Commit Now |
|
|
2810
|
+
|--------|---------|------------|---------------|
|
|
2811
|
+
| Files changed | 1-5 | 6-10 | > 10 |
|
|
2812
|
+
| Lines added | < 150 | 150-300 | > 300 |
|
|
2813
|
+
| Total changes | < 250 | 250-400 | > 400 |
|
|
2814
|
+
|
|
2815
|
+
**Research shows:** PRs > 400 lines have 40%+ defect rates vs 15% for smaller changes.
|
|
2816
|
+
|
|
2817
|
+
## When to Commit
|
|
2818
|
+
|
|
2819
|
+
### Commit Triggers (Any = Commit)
|
|
2820
|
+
|
|
2821
|
+
| Trigger | Action |
|
|
2822
|
+
|---------|--------|
|
|
2823
|
+
| Test passes | Just got a test green \u2192 commit |
|
|
2824
|
+
| Feature complete | Finished a function \u2192 commit |
|
|
2825
|
+
| Refactor done | Renamed across files \u2192 commit |
|
|
2826
|
+
| Bug fixed | Fixed the issue \u2192 commit |
|
|
2827
|
+
| Threshold hit | > 5 files or > 200 lines \u2192 commit |
|
|
2828
|
+
|
|
2829
|
+
### Commit Immediately If
|
|
2830
|
+
|
|
2831
|
+
- \u2705 Tests are passing after being red
|
|
2832
|
+
- \u2705 You're about to make a "big change"
|
|
2833
|
+
- \u2705 You've been coding for 30+ minutes
|
|
2834
|
+
- \u2705 You're about to try something risky
|
|
2835
|
+
- \u2705 The current state is "working"
|
|
2836
|
+
|
|
2837
|
+
## Atomic Commit Patterns
|
|
2838
|
+
|
|
2839
|
+
### Good Commits \u2705
|
|
2840
|
+
|
|
2841
|
+
\`\`\`
|
|
2842
|
+
"Add email validation to signup form"
|
|
2843
|
+
- 3 files: validator.ts, signup.tsx, signup.test.ts
|
|
2844
|
+
- 120 lines changed
|
|
2845
|
+
- Single purpose: email validation
|
|
2846
|
+
|
|
2847
|
+
"Fix null pointer in user lookup"
|
|
2848
|
+
- 2 files: userService.ts, userService.test.ts
|
|
2849
|
+
- 25 lines changed
|
|
2850
|
+
- Single purpose: fix one bug
|
|
2851
|
+
\`\`\`
|
|
2852
|
+
|
|
2853
|
+
### Bad Commits \u274C
|
|
2854
|
+
|
|
2855
|
+
\`\`\`
|
|
2856
|
+
"Add authentication, fix bugs, update styles"
|
|
2857
|
+
- 25 files changed, 800 lines
|
|
2858
|
+
- Multiple unrelated purposes
|
|
2859
|
+
|
|
2860
|
+
"WIP" / "Updates" / "Fix stuff"
|
|
2861
|
+
- Unknown scope, no clear purpose
|
|
2862
|
+
\`\`\`
|
|
2863
|
+
|
|
2864
|
+
## Quick Status Check
|
|
2865
|
+
|
|
2866
|
+
Run frequently to check current state:
|
|
2867
|
+
|
|
2868
|
+
\`\`\`bash
|
|
2869
|
+
# See what's changed
|
|
2870
|
+
git status --short
|
|
2871
|
+
|
|
2872
|
+
# Count changes
|
|
2873
|
+
git diff --shortstat
|
|
2874
|
+
|
|
2875
|
+
# Full summary
|
|
2876
|
+
git diff --stat HEAD
|
|
2877
|
+
\`\`\`
|
|
2878
|
+
|
|
2879
|
+
## PR Size Rules
|
|
2880
|
+
|
|
2881
|
+
| PR Size | Review Time | Quality |
|
|
2882
|
+
|---------|-------------|---------|
|
|
2883
|
+
| < 200 lines | < 30 min | High confidence |
|
|
2884
|
+
| 200-400 lines | 30-60 min | Good confidence |
|
|
2885
|
+
| 400-1000 lines | 1-2 hours | Declining quality |
|
|
2886
|
+
| > 1000 lines | Often skipped | Rubber-stamped |
|
|
2887
|
+
|
|
2888
|
+
**Best practice:** If a PR will be > 400 lines, split into stacked PRs.
|
|
2889
|
+
`,
|
|
2890
|
+
isNew: true
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function generateCodeDeduplicationSkill() {
|
|
2894
|
+
return {
|
|
2895
|
+
type: "skill",
|
|
2896
|
+
path: ".claude/skills/code-deduplication.md",
|
|
2897
|
+
content: `---
|
|
2898
|
+
name: code-deduplication
|
|
2899
|
+
description: Prevent semantic code duplication with capability index
|
|
2900
|
+
globs:
|
|
2901
|
+
- "**/*.ts"
|
|
2902
|
+
- "**/*.tsx"
|
|
2903
|
+
- "**/*.js"
|
|
2904
|
+
- "**/*.py"
|
|
2905
|
+
---
|
|
2906
|
+
|
|
2907
|
+
# Code Deduplication
|
|
2908
|
+
|
|
2909
|
+
Prevent semantic duplication by maintaining awareness of existing capabilities.
|
|
2910
|
+
|
|
2911
|
+
## Core Principle
|
|
2912
|
+
|
|
2913
|
+
\`\`\`
|
|
2914
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
2915
|
+
\u2502 CHECK BEFORE YOU WRITE \u2502
|
|
2916
|
+
\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502
|
|
2917
|
+
\u2502 AI doesn't copy/paste - it reimplements. \u2502
|
|
2918
|
+
\u2502 The problem isn't duplicate code, it's duplicate PURPOSE. \u2502
|
|
2919
|
+
\u2502 \u2502
|
|
2920
|
+
\u2502 Before writing ANY new function: \u2502
|
|
2921
|
+
\u2502 1. Search codebase for similar functionality \u2502
|
|
2922
|
+
\u2502 2. Check utils/, helpers/, lib/ for existing implementations \u2502
|
|
2923
|
+
\u2502 3. Extend existing code if possible \u2502
|
|
2924
|
+
\u2502 4. Only create new if nothing suitable exists \u2502
|
|
2925
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
2926
|
+
\`\`\`
|
|
2927
|
+
|
|
2928
|
+
## Before Writing New Code
|
|
2929
|
+
|
|
2930
|
+
### Search Checklist
|
|
2931
|
+
|
|
2932
|
+
1. **Search by purpose**: "format date", "validate email", "fetch user"
|
|
2933
|
+
2. **Search common locations**:
|
|
2934
|
+
- \`src/utils/\` or \`lib/\`
|
|
2935
|
+
- \`src/helpers/\`
|
|
2936
|
+
- \`src/common/\`
|
|
2937
|
+
- \`src/shared/\`
|
|
2938
|
+
3. **Search by function signature**: Similar inputs/outputs
|
|
2939
|
+
|
|
2940
|
+
### Common Duplicate Candidates
|
|
2941
|
+
|
|
2942
|
+
| Category | Look For |
|
|
2943
|
+
|----------|----------|
|
|
2944
|
+
| Date/Time | formatDate, parseDate, isExpired, addDays |
|
|
2945
|
+
| Validation | isEmail, isPhone, isURL, isUUID |
|
|
2946
|
+
| Strings | slugify, truncate, capitalize, pluralize |
|
|
2947
|
+
| API | fetchUser, createItem, handleError |
|
|
2948
|
+
| Auth | validateToken, requireAuth, getCurrentUser |
|
|
2949
|
+
|
|
2950
|
+
## If Similar Code Exists
|
|
2951
|
+
|
|
2952
|
+
### Option 1: Reuse directly
|
|
2953
|
+
\`\`\`typescript
|
|
2954
|
+
// Import and use existing function
|
|
2955
|
+
import { formatDate } from '@/utils/dates';
|
|
2956
|
+
\`\`\`
|
|
2957
|
+
|
|
2958
|
+
### Option 2: Extend with options
|
|
2959
|
+
\`\`\`typescript
|
|
2960
|
+
// Add optional parameter to existing function
|
|
2961
|
+
export function formatDate(
|
|
2962
|
+
date: Date,
|
|
2963
|
+
format: string = 'short',
|
|
2964
|
+
locale?: string // NEW: added locale support
|
|
2965
|
+
): string { ... }
|
|
2966
|
+
\`\`\`
|
|
2967
|
+
|
|
2968
|
+
### Option 3: Compose from existing
|
|
2969
|
+
\`\`\`typescript
|
|
2970
|
+
// Build on existing utilities
|
|
2971
|
+
export function formatDateRange(start: Date, end: Date) {
|
|
2972
|
+
return \`\${formatDate(start)} - \${formatDate(end)}\`;
|
|
2973
|
+
}
|
|
2974
|
+
\`\`\`
|
|
2975
|
+
|
|
2976
|
+
## File Header Pattern
|
|
2977
|
+
|
|
2978
|
+
Document what each file provides:
|
|
2979
|
+
|
|
2980
|
+
\`\`\`typescript
|
|
2981
|
+
/**
|
|
2982
|
+
* @file User validation utilities
|
|
2983
|
+
* @description Email, phone, and identity validation functions.
|
|
2984
|
+
*
|
|
2985
|
+
* Key exports:
|
|
2986
|
+
* - isEmail(email) - Validates email format
|
|
2987
|
+
* - isPhone(phone, country?) - Validates phone with country
|
|
2988
|
+
* - isValidUsername(username) - Checks username rules
|
|
2989
|
+
*/
|
|
2990
|
+
\`\`\`
|
|
2991
|
+
|
|
2992
|
+
## Anti-Patterns
|
|
2993
|
+
|
|
2994
|
+
- \u274C Writing date formatter without checking utils/
|
|
2995
|
+
- \u274C Creating new API client when one exists
|
|
2996
|
+
- \u274C Duplicating validation logic across files
|
|
2997
|
+
- \u274C Copy-pasting functions between files
|
|
2998
|
+
- \u274C "I'll refactor later" (you won't)
|
|
2999
|
+
`,
|
|
3000
|
+
isNew: true
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
function generateSimplicityRulesSkill() {
|
|
3004
|
+
return {
|
|
3005
|
+
type: "skill",
|
|
3006
|
+
path: ".claude/skills/simplicity-rules.md",
|
|
3007
|
+
content: `---
|
|
3008
|
+
name: simplicity-rules
|
|
3009
|
+
description: Enforced code complexity constraints
|
|
3010
|
+
globs:
|
|
3011
|
+
- "**/*.ts"
|
|
3012
|
+
- "**/*.tsx"
|
|
3013
|
+
- "**/*.js"
|
|
3014
|
+
- "**/*.py"
|
|
3015
|
+
- "**/*.go"
|
|
3016
|
+
---
|
|
3017
|
+
|
|
3018
|
+
# Simplicity Rules
|
|
3019
|
+
|
|
3020
|
+
Complexity is the enemy. Every line of code is a liability.
|
|
3021
|
+
|
|
3022
|
+
## Enforced Limits
|
|
3023
|
+
|
|
3024
|
+
**CRITICAL: These limits are non-negotiable. Check and enforce for EVERY file.**
|
|
3025
|
+
|
|
3026
|
+
### Function Level
|
|
3027
|
+
|
|
3028
|
+
| Constraint | Limit | Action if Exceeded |
|
|
3029
|
+
|------------|-------|-------------------|
|
|
3030
|
+
| Lines per function | 20 max | Decompose immediately |
|
|
3031
|
+
| Parameters | 3 max | Use options object |
|
|
3032
|
+
| Nesting levels | 2 max | Flatten with early returns |
|
|
3033
|
+
|
|
3034
|
+
### File Level
|
|
3035
|
+
|
|
3036
|
+
| Constraint | Limit | Action if Exceeded |
|
|
3037
|
+
|------------|-------|-------------------|
|
|
3038
|
+
| Lines per file | 200 max | Split by responsibility |
|
|
3039
|
+
| Functions per file | 10 max | Split into modules |
|
|
3040
|
+
|
|
3041
|
+
### Module Level
|
|
3042
|
+
|
|
3043
|
+
| Constraint | Limit | Reason |
|
|
3044
|
+
|------------|-------|--------|
|
|
3045
|
+
| Directory nesting | 3 levels max | Flat is better |
|
|
3046
|
+
| Circular deps | 0 | Never acceptable |
|
|
3047
|
+
|
|
3048
|
+
## Enforcement Protocol
|
|
3049
|
+
|
|
3050
|
+
**Before completing ANY file:**
|
|
3051
|
+
|
|
3052
|
+
\`\`\`
|
|
3053
|
+
1. Count total lines \u2192 if > 200, STOP and split
|
|
3054
|
+
2. Count functions \u2192 if > 10, STOP and split
|
|
3055
|
+
3. Check function length \u2192 if any > 20 lines, decompose
|
|
3056
|
+
4. Check parameters \u2192 if any > 3, refactor to options object
|
|
3057
|
+
\`\`\`
|
|
3058
|
+
|
|
3059
|
+
## Violation Response
|
|
3060
|
+
|
|
3061
|
+
When limits are exceeded:
|
|
3062
|
+
|
|
3063
|
+
\`\`\`
|
|
3064
|
+
\u26A0\uFE0F FILE SIZE VIOLATION DETECTED
|
|
3065
|
+
|
|
3066
|
+
[filename] has [X] lines (limit: 200)
|
|
3067
|
+
|
|
3068
|
+
Splitting into:
|
|
3069
|
+
- [filename-a].ts - [responsibility A]
|
|
3070
|
+
- [filename-b].ts - [responsibility B]
|
|
3071
|
+
\`\`\`
|
|
3072
|
+
|
|
3073
|
+
**Never defer refactoring.** Fix violations immediately.
|
|
3074
|
+
|
|
3075
|
+
## Decomposition Patterns
|
|
3076
|
+
|
|
3077
|
+
### Long Function \u2192 Multiple Functions
|
|
3078
|
+
|
|
3079
|
+
\`\`\`typescript
|
|
3080
|
+
// BEFORE: 40 lines
|
|
3081
|
+
function processOrder(order) {
|
|
3082
|
+
// validate... 10 lines
|
|
3083
|
+
// calculate totals... 15 lines
|
|
3084
|
+
// apply discounts... 10 lines
|
|
3085
|
+
// save... 5 lines
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
// AFTER: 4 functions, each < 15 lines
|
|
3089
|
+
function processOrder(order) {
|
|
3090
|
+
validateOrder(order);
|
|
3091
|
+
const totals = calculateTotals(order);
|
|
3092
|
+
const finalPrice = applyDiscounts(totals, order.coupons);
|
|
3093
|
+
return saveOrder({ ...order, finalPrice });
|
|
3094
|
+
}
|
|
3095
|
+
\`\`\`
|
|
3096
|
+
|
|
3097
|
+
### Many Parameters \u2192 Options Object
|
|
3098
|
+
|
|
3099
|
+
\`\`\`typescript
|
|
3100
|
+
// BEFORE: 6 parameters
|
|
3101
|
+
function createUser(name, email, password, role, team, settings) { }
|
|
3102
|
+
|
|
3103
|
+
// AFTER: 1 options object
|
|
3104
|
+
interface CreateUserOptions {
|
|
3105
|
+
name: string;
|
|
3106
|
+
email: string;
|
|
3107
|
+
password: string;
|
|
3108
|
+
role?: string;
|
|
3109
|
+
team?: string;
|
|
3110
|
+
settings?: UserSettings;
|
|
3111
|
+
}
|
|
3112
|
+
function createUser(options: CreateUserOptions) { }
|
|
3113
|
+
\`\`\`
|
|
3114
|
+
|
|
3115
|
+
### Deep Nesting \u2192 Early Returns
|
|
3116
|
+
|
|
3117
|
+
\`\`\`typescript
|
|
3118
|
+
// BEFORE: 4 levels deep
|
|
3119
|
+
function process(data) {
|
|
3120
|
+
if (data) {
|
|
3121
|
+
if (data.valid) {
|
|
3122
|
+
if (data.items) {
|
|
3123
|
+
for (const item of data.items) {
|
|
3124
|
+
// actual logic here
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// AFTER: 1 level deep
|
|
3132
|
+
function process(data) {
|
|
3133
|
+
if (!data?.valid || !data.items) return;
|
|
3134
|
+
|
|
3135
|
+
for (const item of data.items) {
|
|
3136
|
+
// actual logic here
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
\`\`\`
|
|
3140
|
+
|
|
3141
|
+
## Anti-Patterns
|
|
3142
|
+
|
|
3143
|
+
- \u274C God objects/files (do everything)
|
|
3144
|
+
- \u274C "Just one more line" (compound violations)
|
|
3145
|
+
- \u274C "I'll split it later" (you won't)
|
|
3146
|
+
- \u274C Deep inheritance hierarchies
|
|
3147
|
+
- \u274C Complex conditionals without extraction
|
|
3148
|
+
`,
|
|
3149
|
+
isNew: true
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
function generateSecuritySkill(stack) {
|
|
3153
|
+
const isJS = stack.languages.includes("typescript") || stack.languages.includes("javascript");
|
|
3154
|
+
const isPython = stack.languages.includes("python");
|
|
3155
|
+
return {
|
|
3156
|
+
type: "skill",
|
|
3157
|
+
path: ".claude/skills/security.md",
|
|
3158
|
+
content: `---
|
|
3159
|
+
name: security
|
|
3160
|
+
description: Security best practices, secrets management, OWASP patterns
|
|
3161
|
+
globs:
|
|
3162
|
+
- "**/*"
|
|
3163
|
+
---
|
|
3164
|
+
|
|
3165
|
+
# Security Best Practices
|
|
3166
|
+
|
|
3167
|
+
Security is not optional. Every project must pass security checks.
|
|
3168
|
+
|
|
3169
|
+
## Required .gitignore Entries
|
|
3170
|
+
|
|
3171
|
+
**NEVER commit these:**
|
|
3172
|
+
|
|
3173
|
+
\`\`\`gitignore
|
|
3174
|
+
# Environment files
|
|
3175
|
+
.env
|
|
3176
|
+
.env.*
|
|
3177
|
+
!.env.example
|
|
3178
|
+
|
|
3179
|
+
# Secrets and credentials
|
|
3180
|
+
*.pem
|
|
3181
|
+
*.key
|
|
3182
|
+
*.p12
|
|
3183
|
+
credentials.json
|
|
3184
|
+
secrets.json
|
|
3185
|
+
*-credentials.json
|
|
3186
|
+
service-account*.json
|
|
3187
|
+
|
|
3188
|
+
# IDE secrets
|
|
3189
|
+
.idea/
|
|
3190
|
+
.vscode/settings.json
|
|
3191
|
+
\`\`\`
|
|
3192
|
+
|
|
3193
|
+
## Environment Variables
|
|
3194
|
+
|
|
3195
|
+
### Create .env.example
|
|
3196
|
+
|
|
3197
|
+
Document required vars without values:
|
|
3198
|
+
|
|
3199
|
+
\`\`\`bash
|
|
3200
|
+
# Server-side only (never expose to client)
|
|
3201
|
+
DATABASE_URL=
|
|
3202
|
+
API_SECRET_KEY=
|
|
3203
|
+
ANTHROPIC_API_KEY=
|
|
3204
|
+
|
|
3205
|
+
# Client-side safe (public, non-sensitive)
|
|
3206
|
+
${isJS ? "VITE_API_URL=\nNEXT_PUBLIC_SITE_URL=" : "API_BASE_URL="}
|
|
3207
|
+
\`\`\`
|
|
3208
|
+
|
|
3209
|
+
${isJS ? `### Frontend Exposure Rules
|
|
3210
|
+
|
|
3211
|
+
| Framework | Client-Exposed Prefix | Server-Only |
|
|
3212
|
+
|-----------|----------------------|-------------|
|
|
3213
|
+
| Vite | \`VITE_*\` | No prefix |
|
|
3214
|
+
| Next.js | \`NEXT_PUBLIC_*\` | No prefix |
|
|
3215
|
+
| CRA | \`REACT_APP_*\` | N/A |
|
|
3216
|
+
|
|
3217
|
+
**CRITICAL:** Never put secrets in client-exposed env vars!
|
|
3218
|
+
|
|
3219
|
+
\`\`\`typescript
|
|
3220
|
+
// \u274C WRONG - Secret exposed to browser
|
|
3221
|
+
const key = import.meta.env.VITE_API_SECRET;
|
|
3222
|
+
|
|
3223
|
+
// \u2705 CORRECT - Secret stays server-side
|
|
3224
|
+
const key = process.env.API_SECRET; // in API route only
|
|
3225
|
+
\`\`\`
|
|
3226
|
+
` : ""}
|
|
3227
|
+
|
|
3228
|
+
### Validate at Startup
|
|
3229
|
+
|
|
3230
|
+
${isJS ? `\`\`\`typescript
|
|
3231
|
+
import { z } from 'zod';
|
|
3232
|
+
|
|
3233
|
+
const envSchema = z.object({
|
|
3234
|
+
DATABASE_URL: z.string().url(),
|
|
3235
|
+
API_SECRET_KEY: z.string().min(32),
|
|
3236
|
+
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
export const env = envSchema.parse(process.env);
|
|
3240
|
+
\`\`\`` : ""}
|
|
3241
|
+
${isPython ? `\`\`\`python
|
|
3242
|
+
from pydantic_settings import BaseSettings
|
|
3243
|
+
|
|
3244
|
+
class Settings(BaseSettings):
|
|
3245
|
+
database_url: str
|
|
3246
|
+
api_secret_key: str
|
|
3247
|
+
environment: str = "development"
|
|
3248
|
+
|
|
3249
|
+
class Config:
|
|
3250
|
+
env_file = ".env"
|
|
3251
|
+
|
|
3252
|
+
settings = Settings()
|
|
3253
|
+
\`\`\`` : ""}
|
|
3254
|
+
|
|
3255
|
+
## OWASP Top 10 Checklist
|
|
3256
|
+
|
|
3257
|
+
| Vulnerability | Prevention |
|
|
3258
|
+
|---------------|------------|
|
|
3259
|
+
| Injection (SQL, NoSQL, Command) | Parameterized queries, input validation |
|
|
3260
|
+
| Broken Auth | Secure session management, MFA |
|
|
3261
|
+
| Sensitive Data Exposure | Encryption at rest and in transit |
|
|
3262
|
+
| XXE | Disable external entity processing |
|
|
3263
|
+
| Broken Access Control | Verify permissions on every request |
|
|
3264
|
+
| Security Misconfiguration | Secure defaults, minimal permissions |
|
|
3265
|
+
| XSS | Output encoding, CSP headers |
|
|
3266
|
+
| Insecure Deserialization | Validate all serialized data |
|
|
3267
|
+
| Using Vulnerable Components | Keep dependencies updated |
|
|
3268
|
+
| Insufficient Logging | Log security events, monitor |
|
|
3269
|
+
|
|
3270
|
+
## Input Validation
|
|
3271
|
+
|
|
3272
|
+
\`\`\`
|
|
3273
|
+
RULE: Never trust user input. Validate everything.
|
|
3274
|
+
|
|
3275
|
+
- Validate type, length, format, range
|
|
3276
|
+
- Sanitize before storage
|
|
3277
|
+
- Encode before output
|
|
3278
|
+
- Use allowlists over denylists
|
|
3279
|
+
\`\`\`
|
|
3280
|
+
|
|
3281
|
+
## Secrets Detection
|
|
3282
|
+
|
|
3283
|
+
Before committing, check for:
|
|
3284
|
+
|
|
3285
|
+
- API keys (usually 32+ chars, specific patterns)
|
|
3286
|
+
- Passwords in code
|
|
3287
|
+
- Connection strings with credentials
|
|
3288
|
+
- Private keys (BEGIN RSA/EC/PRIVATE KEY)
|
|
3289
|
+
- Tokens (jwt, bearer, oauth)
|
|
3290
|
+
|
|
3291
|
+
## Security Review Checklist
|
|
3292
|
+
|
|
3293
|
+
Before PR merge:
|
|
3294
|
+
|
|
3295
|
+
- [ ] No secrets in code or config
|
|
3296
|
+
- [ ] Input validation on all user data
|
|
3297
|
+
- [ ] Output encoding where displayed
|
|
3298
|
+
- [ ] Authentication checked on protected routes
|
|
3299
|
+
- [ ] Authorization verified for resources
|
|
3300
|
+
- [ ] Dependencies scanned for vulnerabilities
|
|
3301
|
+
- [ ] Error messages don't leak internals
|
|
3302
|
+
`,
|
|
3303
|
+
isNew: true
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
function getAgentsForStack(stack) {
|
|
3307
|
+
const agents = [
|
|
3308
|
+
{ name: "code-reviewer", description: "Reviews code for quality and security" },
|
|
3309
|
+
{ name: "test-writer", description: "Generates tests for code" }
|
|
3310
|
+
];
|
|
3311
|
+
if (stack.hasDocker) {
|
|
3312
|
+
agents.push({ name: "docker-helper", description: "Helps with Docker and containerization" });
|
|
3313
|
+
}
|
|
3314
|
+
return agents;
|
|
3315
|
+
}
|
|
3316
|
+
function generateAgents(stack) {
|
|
3317
|
+
const artifacts = [];
|
|
3318
|
+
artifacts.push(generateCodeReviewerAgent(stack));
|
|
3319
|
+
artifacts.push(generateTestWriterAgent(stack));
|
|
3320
|
+
return artifacts;
|
|
3321
|
+
}
|
|
3322
|
+
function generateCodeReviewerAgent(stack) {
|
|
3323
|
+
const lintCommand = getLintCommand(stack);
|
|
3324
|
+
return {
|
|
3325
|
+
type: "agent",
|
|
3326
|
+
path: ".claude/agents/code-reviewer.md",
|
|
3327
|
+
content: `---
|
|
3328
|
+
name: code-reviewer
|
|
3329
|
+
description: Reviews code for quality, security issues, and best practices
|
|
3330
|
+
tools: Read, Grep, Glob${lintCommand ? `, Bash(${lintCommand})` : ""}
|
|
3331
|
+
disallowedTools: Write, Edit
|
|
3332
|
+
model: sonnet
|
|
3333
|
+
---
|
|
3334
|
+
|
|
3335
|
+
You are a senior code reviewer with expertise in security and performance.
|
|
3336
|
+
|
|
3337
|
+
## Code Style Reference
|
|
3338
|
+
|
|
3339
|
+
Read these files to understand project conventions:
|
|
3340
|
+
${stack.linter === "eslint" ? "- `eslint.config.js` or `.eslintrc.*`" : ""}
|
|
3341
|
+
${stack.formatter === "prettier" ? "- `.prettierrc`" : ""}
|
|
3342
|
+
${stack.languages.includes("typescript") ? "- `tsconfig.json`" : ""}
|
|
3343
|
+
${stack.languages.includes("python") ? "- `pyproject.toml` or `setup.cfg`" : ""}
|
|
3344
|
+
|
|
3345
|
+
${lintCommand ? `Run \`${lintCommand}\` to check violations programmatically.` : ""}
|
|
3346
|
+
|
|
3347
|
+
## Review Process
|
|
3348
|
+
|
|
3349
|
+
1. Run \`git diff\` to identify changed files
|
|
3350
|
+
2. Analyze each change for:
|
|
3351
|
+
- Security vulnerabilities (OWASP Top 10)
|
|
3352
|
+
- Performance issues
|
|
3353
|
+
- Code style violations
|
|
3354
|
+
- Missing error handling
|
|
3355
|
+
- Test coverage gaps
|
|
3356
|
+
|
|
3357
|
+
## Output Format
|
|
3358
|
+
|
|
3359
|
+
For each finding:
|
|
3360
|
+
|
|
3361
|
+
- **Critical**: Must fix before merge
|
|
3362
|
+
- **Warning**: Should address
|
|
3363
|
+
- **Suggestion**: Consider improving
|
|
3364
|
+
|
|
3365
|
+
Include file:line references for each issue.
|
|
3366
|
+
`,
|
|
3367
|
+
isNew: true
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
function generateTestWriterAgent(stack) {
|
|
3371
|
+
const testCommand = getTestCommand(stack);
|
|
3372
|
+
return {
|
|
3373
|
+
type: "agent",
|
|
3374
|
+
path: ".claude/agents/test-writer.md",
|
|
3375
|
+
content: `---
|
|
3376
|
+
name: test-writer
|
|
3377
|
+
description: Generates comprehensive tests for code
|
|
3378
|
+
tools: Read, Grep, Glob, Write, Edit, Bash(${testCommand})
|
|
3379
|
+
model: sonnet
|
|
3380
|
+
---
|
|
3381
|
+
|
|
3382
|
+
You are a testing expert who writes thorough, maintainable tests.
|
|
3383
|
+
|
|
3384
|
+
## Testing Framework
|
|
3385
|
+
|
|
3386
|
+
This project uses: **${stack.testingFramework || "unknown"}**
|
|
3387
|
+
|
|
3388
|
+
## Your Process
|
|
3389
|
+
|
|
3390
|
+
1. Read the code to be tested
|
|
3391
|
+
2. Identify test cases:
|
|
3392
|
+
- Happy path scenarios
|
|
3393
|
+
- Edge cases
|
|
3394
|
+
- Error conditions
|
|
3395
|
+
- Boundary values
|
|
3396
|
+
3. Write tests following project patterns
|
|
3397
|
+
4. Run tests to verify they pass
|
|
3398
|
+
|
|
3399
|
+
## Test Structure
|
|
3400
|
+
|
|
3401
|
+
Follow the AAA pattern:
|
|
3402
|
+
- **Arrange**: Set up test data
|
|
3403
|
+
- **Act**: Execute the code
|
|
3404
|
+
- **Assert**: Verify results
|
|
3405
|
+
|
|
3406
|
+
## Guidelines
|
|
3407
|
+
|
|
3408
|
+
- One assertion focus per test
|
|
3409
|
+
- Descriptive test names
|
|
3410
|
+
- Mock external dependencies
|
|
3411
|
+
- Don't test implementation details
|
|
3412
|
+
- Aim for behavior coverage
|
|
3413
|
+
|
|
3414
|
+
## Run Tests
|
|
3415
|
+
|
|
3416
|
+
\`\`\`bash
|
|
3417
|
+
${testCommand}
|
|
3418
|
+
\`\`\`
|
|
3419
|
+
`,
|
|
3420
|
+
isNew: true
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
function getLintCommand(stack) {
|
|
3424
|
+
switch (stack.linter) {
|
|
3425
|
+
case "eslint":
|
|
3426
|
+
return `${stack.packageManager === "bun" ? "bun" : "npx"} eslint .`;
|
|
3427
|
+
case "biome":
|
|
3428
|
+
return `${stack.packageManager === "bun" ? "bun" : "npx"} biome check .`;
|
|
3429
|
+
case "ruff":
|
|
3430
|
+
return "ruff check .";
|
|
3431
|
+
default:
|
|
3432
|
+
return "";
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function getTestCommand(stack) {
|
|
3436
|
+
switch (stack.testingFramework) {
|
|
3437
|
+
case "vitest":
|
|
3438
|
+
return `${stack.packageManager || "npm"} ${stack.packageManager === "npm" ? "run " : ""}test`;
|
|
3439
|
+
case "jest":
|
|
3440
|
+
return `${stack.packageManager || "npm"} ${stack.packageManager === "npm" ? "run " : ""}test`;
|
|
3441
|
+
case "bun-test":
|
|
3442
|
+
return "bun test";
|
|
3443
|
+
case "pytest":
|
|
3444
|
+
return "pytest";
|
|
3445
|
+
case "go-test":
|
|
3446
|
+
return "go test ./...";
|
|
3447
|
+
case "rust-test":
|
|
3448
|
+
return "cargo test";
|
|
3449
|
+
default:
|
|
3450
|
+
return `${stack.packageManager || "npm"} test`;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
function generateRules(stack) {
|
|
3454
|
+
const artifacts = [];
|
|
3455
|
+
if (stack.languages.includes("typescript")) {
|
|
3456
|
+
artifacts.push(generateTypeScriptRules());
|
|
3457
|
+
}
|
|
3458
|
+
if (stack.languages.includes("python")) {
|
|
3459
|
+
artifacts.push(generatePythonRules());
|
|
3460
|
+
}
|
|
3461
|
+
artifacts.push(generateCodeStyleRule(stack));
|
|
3462
|
+
return artifacts;
|
|
3463
|
+
}
|
|
3464
|
+
function generateTypeScriptRules() {
|
|
3465
|
+
return {
|
|
3466
|
+
type: "rule",
|
|
3467
|
+
path: ".claude/rules/typescript.md",
|
|
3468
|
+
content: `---
|
|
3469
|
+
paths:
|
|
3470
|
+
- "**/*.ts"
|
|
3471
|
+
- "**/*.tsx"
|
|
3472
|
+
---
|
|
3473
|
+
|
|
3474
|
+
# TypeScript Rules
|
|
3475
|
+
|
|
3476
|
+
## Type Safety
|
|
3477
|
+
|
|
3478
|
+
- Avoid \`any\` - use \`unknown\` and narrow types
|
|
3479
|
+
- Prefer interfaces for objects, types for unions/intersections
|
|
3480
|
+
- Use strict mode (\`strict: true\` in tsconfig)
|
|
3481
|
+
- Enable \`noUncheckedIndexedAccess\` for safer array access
|
|
3482
|
+
|
|
3483
|
+
## Patterns
|
|
3484
|
+
|
|
3485
|
+
\`\`\`typescript
|
|
3486
|
+
// Prefer
|
|
3487
|
+
const user: User | undefined = users.find(u => u.id === id);
|
|
3488
|
+
if (user) { /* use user */ }
|
|
3489
|
+
|
|
3490
|
+
// Avoid
|
|
3491
|
+
const user = users.find(u => u.id === id) as User;
|
|
3492
|
+
\`\`\`
|
|
3493
|
+
|
|
3494
|
+
## Naming
|
|
3495
|
+
|
|
3496
|
+
- Interfaces: PascalCase (e.g., \`UserProfile\`)
|
|
3497
|
+
- Types: PascalCase (e.g., \`ApiResponse\`)
|
|
3498
|
+
- Functions: camelCase (e.g., \`getUserById\`)
|
|
3499
|
+
- Constants: SCREAMING_SNAKE_CASE for true constants
|
|
3500
|
+
|
|
3501
|
+
## Imports
|
|
3502
|
+
|
|
3503
|
+
- Group imports: external, internal, relative
|
|
3504
|
+
- Use path aliases when configured
|
|
3505
|
+
- Prefer named exports over default exports
|
|
3506
|
+
`,
|
|
3507
|
+
isNew: true
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
function generatePythonRules() {
|
|
3511
|
+
return {
|
|
3512
|
+
type: "rule",
|
|
3513
|
+
path: ".claude/rules/python.md",
|
|
3514
|
+
content: `---
|
|
3515
|
+
paths:
|
|
3516
|
+
- "**/*.py"
|
|
3517
|
+
---
|
|
3518
|
+
|
|
3519
|
+
# Python Rules
|
|
3520
|
+
|
|
3521
|
+
## Style
|
|
3522
|
+
|
|
3523
|
+
- Follow PEP 8
|
|
3524
|
+
- Use type hints for function signatures
|
|
3525
|
+
- Docstrings for public functions (Google style)
|
|
3526
|
+
- Max line length: 88 (Black default)
|
|
3527
|
+
|
|
3528
|
+
## Patterns
|
|
3529
|
+
|
|
3530
|
+
\`\`\`python
|
|
3531
|
+
# Prefer
|
|
3532
|
+
def get_user(user_id: int) -> User | None:
|
|
3533
|
+
"""Fetch user by ID.
|
|
3534
|
+
|
|
3535
|
+
Args:
|
|
1867
3536
|
user_id: The user's unique identifier.
|
|
1868
3537
|
|
|
1869
3538
|
Returns:
|
|
@@ -1948,7 +3617,8 @@ function generateCommands() {
|
|
|
1948
3617
|
generateTaskCommand(),
|
|
1949
3618
|
generateStatusCommand(),
|
|
1950
3619
|
generateDoneCommand(),
|
|
1951
|
-
generateAnalyzeCommand()
|
|
3620
|
+
generateAnalyzeCommand(),
|
|
3621
|
+
generateCodeReviewCommand()
|
|
1952
3622
|
];
|
|
1953
3623
|
}
|
|
1954
3624
|
function generateTaskCommand() {
|
|
@@ -2092,6 +3762,71 @@ Any improvements or concerns noted.
|
|
|
2092
3762
|
isNew: true
|
|
2093
3763
|
};
|
|
2094
3764
|
}
|
|
3765
|
+
function generateCodeReviewCommand() {
|
|
3766
|
+
return {
|
|
3767
|
+
type: "command",
|
|
3768
|
+
path: ".claude/commands/code-review.md",
|
|
3769
|
+
content: `---
|
|
3770
|
+
allowed-tools: Read, Glob, Grep, Bash(git diff), Bash(git status), Bash(git log)
|
|
3771
|
+
description: Review code changes for quality, security, and best practices
|
|
3772
|
+
---
|
|
3773
|
+
|
|
3774
|
+
# Code Review
|
|
3775
|
+
|
|
3776
|
+
## Changes to Review
|
|
3777
|
+
|
|
3778
|
+
!git diff --stat HEAD~1 2>/dev/null || git diff --stat
|
|
3779
|
+
|
|
3780
|
+
## Review Process
|
|
3781
|
+
|
|
3782
|
+
Analyze all changes for:
|
|
3783
|
+
|
|
3784
|
+
### 1. Security (Critical)
|
|
3785
|
+
- [ ] No secrets/credentials in code
|
|
3786
|
+
- [ ] Input validation present
|
|
3787
|
+
- [ ] Output encoding where needed
|
|
3788
|
+
- [ ] Auth/authz checks on protected routes
|
|
3789
|
+
|
|
3790
|
+
### 2. Quality
|
|
3791
|
+
- [ ] Functions \u2264 20 lines
|
|
3792
|
+
- [ ] Files \u2264 200 lines
|
|
3793
|
+
- [ ] No code duplication
|
|
3794
|
+
- [ ] Clear naming
|
|
3795
|
+
- [ ] Proper error handling
|
|
3796
|
+
|
|
3797
|
+
### 3. Testing
|
|
3798
|
+
- [ ] Tests exist for new code
|
|
3799
|
+
- [ ] Edge cases covered
|
|
3800
|
+
- [ ] Tests are meaningful (not just for coverage)
|
|
3801
|
+
|
|
3802
|
+
### 4. Style
|
|
3803
|
+
- [ ] Matches existing patterns
|
|
3804
|
+
- [ ] Consistent formatting
|
|
3805
|
+
- [ ] No commented-out code
|
|
3806
|
+
|
|
3807
|
+
## Output Format
|
|
3808
|
+
|
|
3809
|
+
For each finding, include file:line reference:
|
|
3810
|
+
|
|
3811
|
+
### Critical (Must Fix)
|
|
3812
|
+
Issues that block merge
|
|
3813
|
+
|
|
3814
|
+
### Warning (Should Fix)
|
|
3815
|
+
Issues that should be addressed
|
|
3816
|
+
|
|
3817
|
+
### Suggestion (Consider)
|
|
3818
|
+
Optional improvements
|
|
3819
|
+
|
|
3820
|
+
## Summary
|
|
3821
|
+
|
|
3822
|
+
Provide:
|
|
3823
|
+
1. Overall assessment (Ready / Changes Needed / Not Ready)
|
|
3824
|
+
2. Count of findings by severity
|
|
3825
|
+
3. Top priorities to address
|
|
3826
|
+
`,
|
|
3827
|
+
isNew: true
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
2095
3830
|
function formatLanguage(lang) {
|
|
2096
3831
|
const names = {
|
|
2097
3832
|
typescript: "TypeScript",
|
|
@@ -2111,6 +3846,7 @@ function formatLanguage(lang) {
|
|
|
2111
3846
|
}
|
|
2112
3847
|
function formatFramework(fw) {
|
|
2113
3848
|
const names = {
|
|
3849
|
+
// JavaScript/TypeScript Frontend
|
|
2114
3850
|
nextjs: "Next.js",
|
|
2115
3851
|
react: "React",
|
|
2116
3852
|
vue: "Vue.js",
|
|
@@ -2122,30 +3858,50 @@ function formatFramework(fw) {
|
|
|
2122
3858
|
remix: "Remix",
|
|
2123
3859
|
gatsby: "Gatsby",
|
|
2124
3860
|
solid: "Solid.js",
|
|
3861
|
+
// JavaScript/TypeScript Backend
|
|
2125
3862
|
express: "Express",
|
|
2126
3863
|
nestjs: "NestJS",
|
|
2127
3864
|
fastify: "Fastify",
|
|
2128
3865
|
hono: "Hono",
|
|
2129
3866
|
elysia: "Elysia",
|
|
2130
3867
|
koa: "Koa",
|
|
3868
|
+
// Python
|
|
2131
3869
|
fastapi: "FastAPI",
|
|
2132
3870
|
django: "Django",
|
|
2133
3871
|
flask: "Flask",
|
|
2134
3872
|
starlette: "Starlette",
|
|
3873
|
+
// Go
|
|
2135
3874
|
gin: "Gin",
|
|
2136
3875
|
echo: "Echo",
|
|
2137
3876
|
fiber: "Fiber",
|
|
3877
|
+
// Rust
|
|
2138
3878
|
actix: "Actix",
|
|
2139
3879
|
axum: "Axum",
|
|
2140
3880
|
rocket: "Rocket",
|
|
3881
|
+
// Ruby
|
|
2141
3882
|
rails: "Rails",
|
|
2142
3883
|
sinatra: "Sinatra",
|
|
3884
|
+
// Java/Kotlin
|
|
2143
3885
|
spring: "Spring",
|
|
2144
3886
|
quarkus: "Quarkus",
|
|
3887
|
+
// Android
|
|
3888
|
+
"jetpack-compose": "Jetpack Compose",
|
|
3889
|
+
"android-views": "Android Views",
|
|
3890
|
+
room: "Room",
|
|
3891
|
+
hilt: "Hilt",
|
|
3892
|
+
"ktor-android": "Ktor",
|
|
3893
|
+
// Swift/iOS
|
|
3894
|
+
swiftui: "SwiftUI",
|
|
3895
|
+
uikit: "UIKit",
|
|
3896
|
+
vapor: "Vapor",
|
|
3897
|
+
swiftdata: "SwiftData",
|
|
3898
|
+
combine: "Combine",
|
|
3899
|
+
// CSS/UI
|
|
2145
3900
|
tailwind: "Tailwind CSS",
|
|
2146
3901
|
shadcn: "shadcn/ui",
|
|
2147
3902
|
chakra: "Chakra UI",
|
|
2148
3903
|
mui: "Material UI",
|
|
3904
|
+
// Database/ORM
|
|
2149
3905
|
prisma: "Prisma",
|
|
2150
3906
|
drizzle: "Drizzle",
|
|
2151
3907
|
typeorm: "TypeORM",
|
|
@@ -2203,15 +3959,14 @@ ${pc.bold("MORE INFO")}
|
|
|
2203
3959
|
`);
|
|
2204
3960
|
}
|
|
2205
3961
|
function showBanner() {
|
|
2206
|
-
console.log(
|
|
2207
|
-
console.log(pc.
|
|
2208
|
-
console.log(pc.
|
|
2209
|
-
console.log(pc.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
3962
|
+
console.log();
|
|
3963
|
+
console.log(pc.bold("Claude Code Starter") + pc.gray(` v${VERSION}`));
|
|
3964
|
+
console.log(pc.gray("Intelligent AI-Assisted Development Setup"));
|
|
2210
3965
|
console.log();
|
|
2211
3966
|
}
|
|
2212
3967
|
function showTechStack(projectInfo, verbose) {
|
|
2213
3968
|
const { techStack } = projectInfo;
|
|
2214
|
-
console.log(pc.
|
|
3969
|
+
console.log(pc.bold("Tech Stack"));
|
|
2215
3970
|
console.log();
|
|
2216
3971
|
if (techStack.primaryLanguage) {
|
|
2217
3972
|
console.log(` ${pc.bold("Language:")} ${formatLanguage2(techStack.primaryLanguage)}`);
|
|
@@ -2251,7 +4006,7 @@ async function promptNewProject(args) {
|
|
|
2251
4006
|
if (!args.interactive) {
|
|
2252
4007
|
return null;
|
|
2253
4008
|
}
|
|
2254
|
-
console.log(pc.yellow("
|
|
4009
|
+
console.log(pc.yellow("New project detected - let's set it up!"));
|
|
2255
4010
|
console.log();
|
|
2256
4011
|
const response = await prompts([
|
|
2257
4012
|
{
|
|
@@ -2441,7 +4196,7 @@ async function main() {
|
|
|
2441
4196
|
}
|
|
2442
4197
|
showBanner();
|
|
2443
4198
|
const projectDir = process.cwd();
|
|
2444
|
-
console.log(pc.
|
|
4199
|
+
console.log(pc.gray("Analyzing repository..."));
|
|
2445
4200
|
console.log();
|
|
2446
4201
|
const projectInfo = analyzeRepository(projectDir);
|
|
2447
4202
|
showTechStack(projectInfo, args.verbose);
|
|
@@ -2457,11 +4212,11 @@ async function main() {
|
|
|
2457
4212
|
projectInfo.description = preferences.description;
|
|
2458
4213
|
}
|
|
2459
4214
|
} else {
|
|
2460
|
-
console.log(pc.
|
|
4215
|
+
console.log(pc.gray(`Existing project with ${projectInfo.fileCount} source files`));
|
|
2461
4216
|
console.log();
|
|
2462
4217
|
}
|
|
2463
4218
|
if (projectInfo.techStack.hasClaudeConfig && !args.force) {
|
|
2464
|
-
console.log(pc.yellow("
|
|
4219
|
+
console.log(pc.yellow("Existing .claude/ configuration detected"));
|
|
2465
4220
|
console.log();
|
|
2466
4221
|
if (args.interactive) {
|
|
2467
4222
|
const { proceed } = await prompts({
|
|
@@ -2477,32 +4232,32 @@ async function main() {
|
|
|
2477
4232
|
}
|
|
2478
4233
|
console.log();
|
|
2479
4234
|
}
|
|
2480
|
-
console.log(pc.
|
|
4235
|
+
console.log(pc.gray("Generating configuration..."));
|
|
2481
4236
|
console.log();
|
|
2482
4237
|
const result = generateArtifacts(projectInfo);
|
|
2483
4238
|
const { created, updated, skipped } = writeArtifacts(result.artifacts, projectDir, args.force);
|
|
2484
4239
|
if (created.length > 0) {
|
|
2485
|
-
console.log(pc.green("
|
|
4240
|
+
console.log(pc.green("Created:"));
|
|
2486
4241
|
for (const file of created) {
|
|
2487
|
-
console.log(pc.green(`
|
|
4242
|
+
console.log(pc.green(` + ${file}`));
|
|
2488
4243
|
}
|
|
2489
4244
|
}
|
|
2490
4245
|
if (updated.length > 0) {
|
|
2491
|
-
console.log(pc.blue("
|
|
4246
|
+
console.log(pc.blue("Updated:"));
|
|
2492
4247
|
for (const file of updated) {
|
|
2493
|
-
console.log(pc.blue(`
|
|
4248
|
+
console.log(pc.blue(` ~ ${file}`));
|
|
2494
4249
|
}
|
|
2495
4250
|
}
|
|
2496
4251
|
if (skipped.length > 0 && args.verbose) {
|
|
2497
|
-
console.log(pc.gray("
|
|
4252
|
+
console.log(pc.gray("Preserved:"));
|
|
2498
4253
|
for (const file of skipped) {
|
|
2499
|
-
console.log(pc.gray(`
|
|
4254
|
+
console.log(pc.gray(` - ${file}`));
|
|
2500
4255
|
}
|
|
2501
4256
|
}
|
|
2502
4257
|
console.log();
|
|
2503
4258
|
createTaskFile(projectInfo, preferences);
|
|
2504
4259
|
const totalFiles = created.length + updated.length;
|
|
2505
|
-
console.log(pc.green(
|
|
4260
|
+
console.log(pc.green(`Done! (${totalFiles} files)`));
|
|
2506
4261
|
console.log();
|
|
2507
4262
|
console.log(pc.bold("Generated for your stack:"));
|
|
2508
4263
|
const skills = result.artifacts.filter((a) => a.type === "skill");
|
|
@@ -2510,24 +4265,24 @@ async function main() {
|
|
|
2510
4265
|
const rules = result.artifacts.filter((a) => a.type === "rule");
|
|
2511
4266
|
if (skills.length > 0) {
|
|
2512
4267
|
console.log(
|
|
2513
|
-
`
|
|
4268
|
+
` ${skills.length} skills (${skills.map((s) => path3.basename(s.path, ".md")).join(", ")})`
|
|
2514
4269
|
);
|
|
2515
4270
|
}
|
|
2516
4271
|
if (agents.length > 0) {
|
|
2517
4272
|
console.log(
|
|
2518
|
-
`
|
|
4273
|
+
` ${agents.length} agents (${agents.map((a) => path3.basename(a.path, ".md")).join(", ")})`
|
|
2519
4274
|
);
|
|
2520
4275
|
}
|
|
2521
4276
|
if (rules.length > 0) {
|
|
2522
|
-
console.log(`
|
|
4277
|
+
console.log(` ${rules.length} rules`);
|
|
2523
4278
|
}
|
|
2524
4279
|
console.log();
|
|
2525
4280
|
console.log(`${pc.cyan("Next step:")} Run ${pc.bold("claude")} to start working!`);
|
|
2526
4281
|
console.log();
|
|
2527
4282
|
if (!projectInfo.isExisting) {
|
|
2528
|
-
console.log(pc.gray("
|
|
4283
|
+
console.log(pc.gray("Tip: Use /task to define your first task"));
|
|
2529
4284
|
} else {
|
|
2530
|
-
console.log(pc.gray("
|
|
4285
|
+
console.log(pc.gray("Tip: Use /analyze to explore specific areas of your codebase"));
|
|
2531
4286
|
}
|
|
2532
4287
|
}
|
|
2533
4288
|
main().catch((err) => {
|
|
@@ -2538,6 +4293,13 @@ main().catch((err) => {
|
|
|
2538
4293
|
process.exit(1);
|
|
2539
4294
|
});
|
|
2540
4295
|
export {
|
|
4296
|
+
createTaskFile,
|
|
4297
|
+
formatFramework2 as formatFramework,
|
|
4298
|
+
formatLanguage2 as formatLanguage,
|
|
2541
4299
|
getVersion,
|
|
2542
|
-
parseArgs
|
|
4300
|
+
parseArgs,
|
|
4301
|
+
promptNewProject,
|
|
4302
|
+
showBanner,
|
|
4303
|
+
showHelp,
|
|
4304
|
+
showTechStack
|
|
2543
4305
|
};
|