claude-code-starter 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +1064 -1
  2. 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
@@ -796,6 +873,21 @@ function getSkillsForStack(stack) {
796
873
  if (stack.frameworks.includes("prisma") || stack.frameworks.includes("drizzle")) {
797
874
  skills.push({ name: "database-patterns", description: "Database and ORM patterns" });
798
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
+ }
799
891
  return skills;
800
892
  }
801
893
  function generateSkills(stack) {
@@ -820,6 +912,21 @@ function generateSkills(stack) {
820
912
  if (stack.frameworks.includes("nestjs")) {
821
913
  artifacts.push(generateNestJSSkill());
822
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
+ }
823
930
  return artifacts;
824
931
  }
825
932
  function generatePatternDiscoverySkill() {
@@ -1659,6 +1766,934 @@ describe('UsersService', () => {
1659
1766
  isNew: true
1660
1767
  };
1661
1768
  }
1769
+ function generateSwiftUISkill() {
1770
+ return {
1771
+ type: "skill",
1772
+ path: ".claude/skills/swiftui-patterns.md",
1773
+ content: `---
1774
+ name: swiftui-patterns
1775
+ description: SwiftUI declarative UI patterns and best practices
1776
+ globs:
1777
+ - "**/*.swift"
1778
+ ---
1779
+
1780
+ # SwiftUI Patterns
1781
+
1782
+ ## View Structure
1783
+
1784
+ \`\`\`swift
1785
+ import SwiftUI
1786
+
1787
+ struct ContentView: View {
1788
+ @State private var count = 0
1789
+ @StateObject private var viewModel = ContentViewModel()
1790
+
1791
+ var body: some View {
1792
+ VStack(spacing: 16) {
1793
+ Text("Count: \\(count)")
1794
+ .font(.title)
1795
+
1796
+ Button("Increment") {
1797
+ count += 1
1798
+ }
1799
+ .buttonStyle(.borderedProminent)
1800
+ }
1801
+ .padding()
1802
+ }
1803
+ }
1804
+
1805
+ #Preview {
1806
+ ContentView()
1807
+ }
1808
+ \`\`\`
1809
+
1810
+ ## Property Wrappers
1811
+
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 |
1820
+
1821
+ ## MVVM Pattern
1822
+
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?
1830
+
1831
+ private let service: UserService
1832
+
1833
+ init(service: UserService = .shared) {
1834
+ self.service = service
1835
+ }
1836
+
1837
+ func fetchUsers() async {
1838
+ isLoading = true
1839
+ defer { isLoading = false }
1840
+
1841
+ do {
1842
+ users = try await service.getUsers()
1843
+ } catch {
1844
+ self.error = error
1845
+ }
1846
+ }
1847
+ }
1848
+
1849
+ // View
1850
+ struct UsersView: View {
1851
+ @StateObject private var viewModel = UserViewModel()
1852
+
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
+ \`\`\`
1868
+
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
+ \`\`\`
1888
+
1889
+ ## Async/Await Patterns
1890
+
1891
+ \`\`\`swift
1892
+ // Task modifier for view lifecycle
1893
+ .task {
1894
+ await loadData()
1895
+ }
1896
+
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
+ }
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
1916
+ `,
1917
+ isNew: true
1918
+ };
1919
+ }
1920
+ function generateUIKitSkill() {
1921
+ return {
1922
+ type: "skill",
1923
+ path: ".claude/skills/uikit-patterns.md",
1924
+ content: `---
1925
+ name: uikit-patterns
1926
+ description: UIKit view controller patterns and best practices
1927
+ globs:
1928
+ - "**/*.swift"
1929
+ ---
1930
+
1931
+ # UIKit Patterns
1932
+
1933
+ ## View Controller Structure
1934
+
1935
+ \`\`\`swift
1936
+ import UIKit
1937
+
1938
+ class UserViewController: UIViewController {
1939
+ // MARK: - Properties
1940
+ private let viewModel: UserViewModel
1941
+ private var cancellables = Set<AnyCancellable>()
1942
+
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
+ }()
1952
+
1953
+ // MARK: - Lifecycle
1954
+ init(viewModel: UserViewModel) {
1955
+ self.viewModel = viewModel
1956
+ super.init(nibName: nil, bundle: nil)
1957
+ }
1958
+
1959
+ required init?(coder: NSCoder) {
1960
+ fatalError("init(coder:) has not been implemented")
1961
+ }
1962
+
1963
+ override func viewDidLoad() {
1964
+ super.viewDidLoad()
1965
+ setupUI()
1966
+ setupBindings()
1967
+ viewModel.fetchUsers()
1968
+ }
1969
+
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
+ }
1982
+
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
+ }
1991
+ }
1992
+ \`\`\`
1993
+
1994
+ ## Coordinator Pattern
1995
+
1996
+ \`\`\`swift
1997
+ protocol Coordinator: AnyObject {
1998
+ var childCoordinators: [Coordinator] { get set }
1999
+ var navigationController: UINavigationController { get set }
2000
+ func start()
2001
+ }
2002
+
2003
+ class AppCoordinator: Coordinator {
2004
+ var childCoordinators: [Coordinator] = []
2005
+ var navigationController: UINavigationController
2006
+
2007
+ init(navigationController: UINavigationController) {
2008
+ self.navigationController = navigationController
2009
+ }
2010
+
2011
+ func start() {
2012
+ let vc = HomeViewController()
2013
+ vc.coordinator = self
2014
+ navigationController.pushViewController(vc, animated: false)
2015
+ }
2016
+
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
+ }
1662
2697
  function generateIterativeDevelopmentSkill(stack) {
1663
2698
  const testCmd = getTestCommand(stack);
1664
2699
  const lintCmd = getLintCommand(stack);
@@ -2811,6 +3846,7 @@ function formatLanguage(lang) {
2811
3846
  }
2812
3847
  function formatFramework(fw) {
2813
3848
  const names = {
3849
+ // JavaScript/TypeScript Frontend
2814
3850
  nextjs: "Next.js",
2815
3851
  react: "React",
2816
3852
  vue: "Vue.js",
@@ -2822,30 +3858,50 @@ function formatFramework(fw) {
2822
3858
  remix: "Remix",
2823
3859
  gatsby: "Gatsby",
2824
3860
  solid: "Solid.js",
3861
+ // JavaScript/TypeScript Backend
2825
3862
  express: "Express",
2826
3863
  nestjs: "NestJS",
2827
3864
  fastify: "Fastify",
2828
3865
  hono: "Hono",
2829
3866
  elysia: "Elysia",
2830
3867
  koa: "Koa",
3868
+ // Python
2831
3869
  fastapi: "FastAPI",
2832
3870
  django: "Django",
2833
3871
  flask: "Flask",
2834
3872
  starlette: "Starlette",
3873
+ // Go
2835
3874
  gin: "Gin",
2836
3875
  echo: "Echo",
2837
3876
  fiber: "Fiber",
3877
+ // Rust
2838
3878
  actix: "Actix",
2839
3879
  axum: "Axum",
2840
3880
  rocket: "Rocket",
3881
+ // Ruby
2841
3882
  rails: "Rails",
2842
3883
  sinatra: "Sinatra",
3884
+ // Java/Kotlin
2843
3885
  spring: "Spring",
2844
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
2845
3900
  tailwind: "Tailwind CSS",
2846
3901
  shadcn: "shadcn/ui",
2847
3902
  chakra: "Chakra UI",
2848
3903
  mui: "Material UI",
3904
+ // Database/ORM
2849
3905
  prisma: "Prisma",
2850
3906
  drizzle: "Drizzle",
2851
3907
  typeorm: "TypeORM",
@@ -3237,6 +4293,13 @@ main().catch((err) => {
3237
4293
  process.exit(1);
3238
4294
  });
3239
4295
  export {
4296
+ createTaskFile,
4297
+ formatFramework2 as formatFramework,
4298
+ formatLanguage2 as formatLanguage,
3240
4299
  getVersion,
3241
- parseArgs
4300
+ parseArgs,
4301
+ promptNewProject,
4302
+ showBanner,
4303
+ showHelp,
4304
+ showTechStack
3242
4305
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-starter",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A lightweight starter kit for AI-assisted development with Claude Code",
5
5
  "keywords": [
6
6
  "claude",