morphkit-cli 0.1.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/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "morphkit-cli",
3
+ "version": "0.1.0",
4
+ "description": "Semantic AI agent that converts TypeScript/React web apps to native SwiftUI iOS apps",
5
+ "type": "module",
6
+ "bin": {
7
+ "morphkit": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "dev": "bun run src/index.ts",
17
+ "build": "bun build src/index.ts --outdir dist --target node",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit",
20
+ "analyze": "bun run src/index.ts analyze",
21
+ "generate": "bun run src/index.ts generate",
22
+ "prepublishOnly": "bun build src/index.ts --outdir dist --target node --minify"
23
+ },
24
+ "dependencies": {
25
+ "openai": "^4.0.0",
26
+ "ts-morph": "^24.0.0",
27
+ "commander": "^13.0.0",
28
+ "chalk": "^5.4.0",
29
+ "ora": "^8.0.0",
30
+ "simple-git": "^3.27.0",
31
+ "fast-glob": "^3.3.0",
32
+ "zod": "^3.24.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "typescript": "^5.7.0"
37
+ },
38
+ "keywords": [
39
+ "typescript",
40
+ "swift",
41
+ "swiftui",
42
+ "ios",
43
+ "react",
44
+ "nextjs",
45
+ "code-generation",
46
+ "ai",
47
+ "mobile"
48
+ ],
49
+ "license": "MIT",
50
+ "author": "AshlrAI <hello@ashlr.ai>",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/ashlrai/morphkit.git"
54
+ },
55
+ "homepage": "https://morphkit.dev",
56
+ "bugs": "https://github.com/ashlrai/morphkit/issues",
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }
@@ -0,0 +1,46 @@
1
+ // Template for generating the main App entry point
2
+
3
+ export function generateAppEntry(appName: string): string {
4
+ return `// Generated by Morphkit
5
+ import SwiftUI
6
+
7
+ @main
8
+ struct ${appName}App: App {
9
+ var body: some Scene {
10
+ WindowGroup {
11
+ ContentView()
12
+ }
13
+ }
14
+ }
15
+ `;
16
+ }
17
+
18
+ export function generateAppEntryWithState(appName: string, storeNames: string[]): string {
19
+ const storeDeclarations = storeNames
20
+ .map(name => ` @State private var ${camelCase(name)} = ${name}()`)
21
+ .join('\n');
22
+
23
+ const environmentModifiers = storeNames
24
+ .map(name => ` .environment(${camelCase(name)})`)
25
+ .join('\n');
26
+
27
+ return `// Generated by Morphkit
28
+ import SwiftUI
29
+
30
+ @main
31
+ struct ${appName}App: App {
32
+ ${storeDeclarations}
33
+
34
+ var body: some Scene {
35
+ WindowGroup {
36
+ ContentView()
37
+ ${environmentModifiers}
38
+ }
39
+ }
40
+ }
41
+ `;
42
+ }
43
+
44
+ function camelCase(str: string): string {
45
+ return str.charAt(0).toLowerCase() + str.slice(1);
46
+ }
@@ -0,0 +1,72 @@
1
+ // Template for generating Swift data models
2
+
3
+ export interface SwiftProperty {
4
+ name: string;
5
+ type: string;
6
+ optional: boolean;
7
+ defaultValue?: string;
8
+ }
9
+
10
+ export interface SwiftModelConfig {
11
+ name: string;
12
+ properties: SwiftProperty[];
13
+ protocols: string[];
14
+ hasId: boolean;
15
+ sourceFile: string;
16
+ }
17
+
18
+ export function generateSwiftModel(config: SwiftModelConfig): string {
19
+ const protocols = config.protocols.length > 0
20
+ ? `: ${config.protocols.join(', ')}`
21
+ : '';
22
+
23
+ const properties = config.properties
24
+ .map(p => {
25
+ const type = p.optional ? `${p.type}?` : p.type;
26
+ const defaultVal = p.defaultValue ? ` = ${p.defaultValue}` : '';
27
+ return ` var ${p.name}: ${type}${defaultVal}`;
28
+ })
29
+ .join('\n');
30
+
31
+ // Generate CodingKeys if any property names need mapping
32
+ const needsCodingKeys = config.properties.some(p => p.name.includes('_') || p.name !== camelCase(p.name));
33
+ const codingKeys = needsCodingKeys
34
+ ? generateCodingKeys(config.properties)
35
+ : '';
36
+
37
+ return `// Generated by Morphkit from: ${config.sourceFile}
38
+ import Foundation
39
+
40
+ struct ${config.name}${protocols} {
41
+ ${properties}
42
+ }
43
+ ${codingKeys}`;
44
+ }
45
+
46
+ export function generateSwiftEnum(name: string, cases: string[], sourceFile: string): string {
47
+ const enumCases = cases.map(c => ` case ${camelCase(c)}`).join('\n');
48
+
49
+ return `// Generated by Morphkit from: ${sourceFile}
50
+ import Foundation
51
+
52
+ enum ${name}: String, Codable, CaseIterable {
53
+ ${enumCases}
54
+ }
55
+ `;
56
+ }
57
+
58
+ function generateCodingKeys(properties: SwiftProperty[]): string {
59
+ const keys = properties
60
+ .map(p => ` case ${camelCase(p.name)} = "${p.name}"`)
61
+ .join('\n');
62
+
63
+ return `
64
+ enum CodingKeys: String, CodingKey {
65
+ ${keys}
66
+ }
67
+ `;
68
+ }
69
+
70
+ function camelCase(str: string): string {
71
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
72
+ }
@@ -0,0 +1,118 @@
1
+ // Templates for generating navigation structure
2
+
3
+ export interface RouteDefinition {
4
+ name: string;
5
+ associatedValues?: { name: string; type: string }[];
6
+ }
7
+
8
+ export function generateRouterEnum(routes: RouteDefinition[]): string {
9
+ const cases = routes.map(r => {
10
+ if (r.associatedValues && r.associatedValues.length > 0) {
11
+ const values = r.associatedValues.map(v => `${v.name}: ${v.type}`).join(', ');
12
+ return ` case ${r.name}(${values})`;
13
+ }
14
+ return ` case ${r.name}`;
15
+ }).join('\n');
16
+
17
+ return `// Generated by Morphkit
18
+ import SwiftUI
19
+
20
+ enum AppRoute: Hashable {
21
+ ${cases}
22
+ }
23
+ `;
24
+ }
25
+
26
+ export function generateRouter(routes: RouteDefinition[]): string {
27
+ const destinationCases = routes.map(r => {
28
+ const viewName = pascalCase(r.name) + 'View';
29
+ if (r.associatedValues && r.associatedValues.length > 0) {
30
+ const bindings = r.associatedValues.map(v => `let ${v.name}`).join(', ');
31
+ const args = r.associatedValues.map(v => `${v.name}: ${v.name}`).join(', ');
32
+ return ` case .${r.name}(${bindings}):
33
+ ${viewName}(${args})`;
34
+ }
35
+ return ` case .${r.name}:
36
+ ${viewName}()`;
37
+ }).join('\n');
38
+
39
+ return `// Generated by Morphkit
40
+ import SwiftUI
41
+
42
+ @Observable
43
+ final class Router {
44
+ var path = NavigationPath()
45
+
46
+ func navigate(to route: AppRoute) {
47
+ path.append(route)
48
+ }
49
+
50
+ func goBack() {
51
+ guard !path.isEmpty else { return }
52
+ path.removeLast()
53
+ }
54
+
55
+ func popToRoot() {
56
+ path = NavigationPath()
57
+ }
58
+ }
59
+
60
+ struct RouterDestination: ViewModifier {
61
+ func body(content: Content) -> some View {
62
+ content
63
+ .navigationDestination(for: AppRoute.self) { route in
64
+ switch route {
65
+ ${destinationCases}
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ extension View {
72
+ func withRouterDestinations() -> some View {
73
+ modifier(RouterDestination())
74
+ }
75
+ }
76
+ `;
77
+ }
78
+
79
+ export function generateNavigationStackRoot(rootView: string): string {
80
+ return `NavigationStack(path: $router.path) {
81
+ ${rootView}()
82
+ .withRouterDestinations()
83
+ }`;
84
+ }
85
+
86
+ export function generateTabViewWithNavigation(
87
+ tabs: { name: string; icon: string; view: string }[],
88
+ ): string {
89
+ const tabContent = tabs.map(t => ` Tab("${t.name}", systemImage: "${t.icon}") {
90
+ NavigationStack {
91
+ ${t.view}()
92
+ .withRouterDestinations()
93
+ }
94
+ }`).join('\n');
95
+
96
+ return `// Generated by Morphkit
97
+ import SwiftUI
98
+
99
+ struct ContentView: View {
100
+ @State private var router = Router()
101
+
102
+ var body: some View {
103
+ TabView {
104
+ ${tabContent}
105
+ }
106
+ .environment(router)
107
+ }
108
+ }
109
+
110
+ #Preview {
111
+ ContentView()
112
+ }
113
+ `;
114
+ }
115
+
116
+ function pascalCase(str: string): string {
117
+ return str.charAt(0).toUpperCase() + str.slice(1);
118
+ }
@@ -0,0 +1,145 @@
1
+ // Templates for generating the networking layer
2
+
3
+ export interface EndpointConfig {
4
+ name: string;
5
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
6
+ path: string;
7
+ requestType?: string;
8
+ responseType: string;
9
+ requiresAuth: boolean;
10
+ description: string;
11
+ }
12
+
13
+ export function generateAPIClient(
14
+ baseURL: string,
15
+ endpoints: EndpointConfig[],
16
+ authStrategy: 'bearer' | 'cookie' | 'none',
17
+ ): string {
18
+ const endpointMethods = endpoints.map(generateEndpointMethod).join('\n\n');
19
+
20
+ const authHeader = authStrategy === 'bearer'
21
+ ? `
22
+ var authToken: String? {
23
+ get { UserDefaults.standard.string(forKey: "authToken") }
24
+ set { UserDefaults.standard.set(newValue, forKey: "authToken") }
25
+ }
26
+
27
+ private func authenticatedRequest(_ request: inout URLRequest) {
28
+ if let token = authToken {
29
+ request.setValue("Bearer \\(token)", forHTTPHeaderField: "Authorization")
30
+ }
31
+ }`
32
+ : '';
33
+
34
+ return `// Generated by Morphkit
35
+ import Foundation
36
+
37
+ @Observable
38
+ final class APIClient {
39
+ static let shared = APIClient()
40
+
41
+ private let baseURL = URL(string: "${baseURL}")!
42
+ private let decoder: JSONDecoder = {
43
+ let decoder = JSONDecoder()
44
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
45
+ decoder.dateDecodingStrategy = .iso8601
46
+ return decoder
47
+ }()
48
+ private let encoder: JSONEncoder = {
49
+ let encoder = JSONEncoder()
50
+ encoder.keyEncodingStrategy = .convertToSnakeCase
51
+ encoder.dateEncodingStrategy = .iso8601
52
+ return encoder
53
+ }()
54
+ ${authHeader}
55
+
56
+ private init() {}
57
+
58
+ ${endpointMethods}
59
+
60
+ // MARK: - Private Helpers
61
+
62
+ private func request<T: Decodable>(
63
+ method: String,
64
+ path: String,
65
+ body: (any Encodable)? = nil,
66
+ requiresAuth: Bool = false
67
+ ) async throws -> T {
68
+ let url = baseURL.appendingPathComponent(path)
69
+ var request = URLRequest(url: url)
70
+ request.httpMethod = method
71
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
72
+
73
+ if requiresAuth {
74
+ ${authStrategy === 'bearer' ? 'authenticatedRequest(&request)' : '// TODO: Add auth'}
75
+ }
76
+
77
+ if let body {
78
+ request.httpBody = try encoder.encode(body)
79
+ }
80
+
81
+ let (data, response) = try await URLSession.shared.data(for: request)
82
+
83
+ guard let httpResponse = response as? HTTPURLResponse else {
84
+ throw NetworkError.invalidResponse
85
+ }
86
+
87
+ guard (200...299).contains(httpResponse.statusCode) else {
88
+ throw NetworkError.httpError(statusCode: httpResponse.statusCode, data: data)
89
+ }
90
+
91
+ return try decoder.decode(T.self, from: data)
92
+ }
93
+ }
94
+
95
+ enum NetworkError: LocalizedError {
96
+ case invalidResponse
97
+ case httpError(statusCode: Int, data: Data)
98
+ case decodingError(Error)
99
+
100
+ var errorDescription: String? {
101
+ switch self {
102
+ case .invalidResponse:
103
+ return "Invalid response from server"
104
+ case .httpError(let statusCode, _):
105
+ return "HTTP error: \\(statusCode)"
106
+ case .decodingError(let error):
107
+ return "Failed to decode response: \\(error.localizedDescription)"
108
+ }
109
+ }
110
+ }
111
+ `;
112
+ }
113
+
114
+ function generateEndpointMethod(endpoint: EndpointConfig): string {
115
+ const params: string[] = [];
116
+ const pathWithInterpolation = endpoint.path.replace(/:(\w+)/g, (_, param) => {
117
+ params.push(`${param}: String`);
118
+ return `\\(${param})`;
119
+ });
120
+
121
+ if (endpoint.requestType && ['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
122
+ params.push(`body: ${endpoint.requestType}`);
123
+ }
124
+
125
+ const paramString = params.length > 0 ? params.join(', ') : '';
126
+ const bodyArg = endpoint.requestType && ['POST', 'PUT', 'PATCH'].includes(endpoint.method)
127
+ ? ', body: body'
128
+ : '';
129
+
130
+ const returnType = endpoint.responseType === 'Void' ? '' : ` -> ${endpoint.responseType}`;
131
+ const returnStatement = endpoint.responseType === 'Void'
132
+ ? `let _: EmptyResponse = try await request(method: "${endpoint.method}", path: "${pathWithInterpolation}"${bodyArg}, requiresAuth: ${endpoint.requiresAuth})`
133
+ : `return try await request(method: "${endpoint.method}", path: "${pathWithInterpolation}"${bodyArg}, requiresAuth: ${endpoint.requiresAuth})`;
134
+
135
+ return ` /// ${endpoint.description}
136
+ func ${endpoint.name}(${paramString}) async throws${returnType} {
137
+ ${returnStatement}
138
+ }`;
139
+ }
140
+
141
+ export function generateEmptyResponseType(): string {
142
+ return `
143
+ struct EmptyResponse: Decodable {}
144
+ `;
145
+ }
@@ -0,0 +1,230 @@
1
+ // Templates for generating SwiftUI views
2
+
3
+ export interface ViewConfig {
4
+ name: string;
5
+ sourceFile: string;
6
+ stateProperties: StateProperty[];
7
+ bodyContent: string;
8
+ helperMethods: string[];
9
+ imports: string[];
10
+ }
11
+
12
+ export interface StateProperty {
13
+ wrapper: '@State' | '@Binding' | '@Environment' | '@Observable';
14
+ name: string;
15
+ type: string;
16
+ defaultValue?: string;
17
+ }
18
+
19
+ export function generateView(config: ViewConfig): string {
20
+ const imports = ['SwiftUI', ...config.imports]
21
+ .map(i => `import ${i}`)
22
+ .join('\n');
23
+
24
+ const stateProps = config.stateProperties
25
+ .map(p => {
26
+ if (p.wrapper === '@Environment') {
27
+ return ` @Environment(${p.type}.self) private var ${p.name}`;
28
+ }
29
+ const defaultVal = p.defaultValue ? ` = ${p.defaultValue}` : '';
30
+ return ` ${p.wrapper} private var ${p.name}: ${p.type}${defaultVal}`;
31
+ })
32
+ .join('\n');
33
+
34
+ const helpers = config.helperMethods.length > 0
35
+ ? '\n' + config.helperMethods.map(m => indentBlock(m, 4)).join('\n\n')
36
+ : '';
37
+
38
+ return `// Generated by Morphkit from: ${config.sourceFile}
39
+ ${imports}
40
+
41
+ struct ${config.name}: View {
42
+ ${stateProps}
43
+
44
+ var body: some View {
45
+ ${indentBlock(config.bodyContent, 8)}
46
+ }${helpers}
47
+ }
48
+
49
+ #Preview {
50
+ ${config.name}()
51
+ }
52
+ `;
53
+ }
54
+
55
+ export function generateListView(
56
+ name: string,
57
+ sourceFile: string,
58
+ itemType: string,
59
+ rowContent: string,
60
+ stateProperties: StateProperty[],
61
+ ): string {
62
+ return generateView({
63
+ name,
64
+ sourceFile,
65
+ stateProperties: [
66
+ ...stateProperties,
67
+ { wrapper: '@State', name: 'items', type: `[${itemType}]`, defaultValue: '[]' },
68
+ { wrapper: '@State', name: 'isLoading', type: 'Bool', defaultValue: 'false' },
69
+ ],
70
+ bodyContent: `NavigationStack {
71
+ List(items) { item in
72
+ NavigationLink(value: item) {
73
+ ${indentBlock(rowContent, 12)}
74
+ }
75
+ }
76
+ .navigationTitle("${name}")
77
+ .overlay {
78
+ if isLoading {
79
+ ProgressView()
80
+ }
81
+ }
82
+ .task {
83
+ await loadItems()
84
+ }
85
+ }`,
86
+ helperMethods: [
87
+ `private func loadItems() async {
88
+ isLoading = true
89
+ defer { isLoading = false }
90
+ // TODO: Load items from API
91
+ }`,
92
+ ],
93
+ imports: [],
94
+ });
95
+ }
96
+
97
+ export function generateFormView(
98
+ name: string,
99
+ sourceFile: string,
100
+ fields: { name: string; type: string; label: string }[],
101
+ ): string {
102
+ const stateProperties: StateProperty[] = fields.map(f => ({
103
+ wrapper: '@State' as const,
104
+ name: f.name,
105
+ type: f.type,
106
+ defaultValue: getDefaultValue(f.type),
107
+ }));
108
+
109
+ const formFields = fields.map(f => {
110
+ switch (f.type) {
111
+ case 'String':
112
+ return `TextField("${f.label}", text: $${f.name})`;
113
+ case 'Bool':
114
+ return `Toggle("${f.label}", isOn: $${f.name})`;
115
+ case 'Date':
116
+ return `DatePicker("${f.label}", selection: $${f.name})`;
117
+ case 'Int':
118
+ case 'Double':
119
+ return `TextField("${f.label}", value: $${f.name}, format: .number)`;
120
+ default:
121
+ return `TextField("${f.label}", text: $${f.name})`;
122
+ }
123
+ }).join('\n');
124
+
125
+ return generateView({
126
+ name,
127
+ sourceFile,
128
+ stateProperties: [
129
+ ...stateProperties,
130
+ { wrapper: '@Environment', name: 'dismiss', type: '\\.dismiss' },
131
+ ],
132
+ bodyContent: `NavigationStack {
133
+ Form {
134
+ Section {
135
+ ${indentBlock(formFields, 12)}
136
+ }
137
+
138
+ Section {
139
+ Button("Save") {
140
+ save()
141
+ }
142
+ .frame(maxWidth: .infinity)
143
+ }
144
+ }
145
+ .navigationTitle("${name}")
146
+ .toolbar {
147
+ ToolbarItem(placement: .cancellationAction) {
148
+ Button("Cancel") { dismiss() }
149
+ }
150
+ }
151
+ }`,
152
+ helperMethods: [
153
+ `private func save() {
154
+ // TODO: Implement save
155
+ dismiss()
156
+ }`,
157
+ ],
158
+ imports: [],
159
+ });
160
+ }
161
+
162
+ export function generateDetailView(
163
+ name: string,
164
+ sourceFile: string,
165
+ modelType: string,
166
+ sections: { title: string; content: string }[],
167
+ ): string {
168
+ const sectionContent = sections
169
+ .map(s => `Section("${s.title}") {\n${indentBlock(s.content, 4)}\n}`)
170
+ .join('\n\n');
171
+
172
+ return generateView({
173
+ name,
174
+ sourceFile,
175
+ stateProperties: [],
176
+ bodyContent: `ScrollView {
177
+ VStack(alignment: .leading, spacing: 16) {
178
+ ${indentBlock(sectionContent, 8)}
179
+ }
180
+ .padding()
181
+ }`,
182
+ helperMethods: [],
183
+ imports: [],
184
+ });
185
+ }
186
+
187
+ export function generateTabView(
188
+ tabs: { name: string; icon: string; view: string }[],
189
+ ): string {
190
+ const tabItems = tabs
191
+ .map(t => ` Tab("${t.name}", systemImage: "${t.icon}") {
192
+ ${t.view}()
193
+ }`)
194
+ .join('\n');
195
+
196
+ return `// Generated by Morphkit
197
+ import SwiftUI
198
+
199
+ struct ContentView: View {
200
+ var body: some View {
201
+ TabView {
202
+ ${tabItems}
203
+ }
204
+ }
205
+ }
206
+
207
+ #Preview {
208
+ ContentView()
209
+ }
210
+ `;
211
+ }
212
+
213
+ function getDefaultValue(type: string): string {
214
+ switch (type) {
215
+ case 'String': return '""';
216
+ case 'Bool': return 'false';
217
+ case 'Int': return '0';
218
+ case 'Double': return '0.0';
219
+ case 'Date': return '.now';
220
+ default: return '""';
221
+ }
222
+ }
223
+
224
+ function indentBlock(content: string, spaces: number): string {
225
+ const indent = ' '.repeat(spaces);
226
+ return content
227
+ .split('\n')
228
+ .map(line => line.length > 0 ? `${indent}${line}` : line)
229
+ .join('\n');
230
+ }