@craft-native/ios 0.0.91 → 0.0.94
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 +26 -12
- package/dist/cli.js +751 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +674 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +681 -57
- package/LICENSE.md +0 -21
package/dist/cli.js
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
+
import { dirname, join, resolve } from "path";
|
|
10
|
+
var {$ } = globalThis.Bun;
|
|
11
|
+
var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
|
|
12
|
+
var DEFAULT_CONFIG = {
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
buildNumber: "1",
|
|
15
|
+
darkMode: true,
|
|
16
|
+
backgroundColor: "#0b1712",
|
|
17
|
+
enableSpeechRecognition: false,
|
|
18
|
+
enableHaptics: false,
|
|
19
|
+
enableShare: false,
|
|
20
|
+
enableCamera: false,
|
|
21
|
+
enableBiometric: false,
|
|
22
|
+
enablePushNotifications: false,
|
|
23
|
+
enableSecureStorage: false,
|
|
24
|
+
enableGeolocation: false,
|
|
25
|
+
enableClipboard: false,
|
|
26
|
+
enableContacts: false,
|
|
27
|
+
enableCalendar: false,
|
|
28
|
+
enableLocalNotifications: false,
|
|
29
|
+
enableInAppPurchase: false,
|
|
30
|
+
enableKeepAwake: false,
|
|
31
|
+
enableOrientationLock: false,
|
|
32
|
+
enableDeepLinks: false,
|
|
33
|
+
enableQRScanner: false,
|
|
34
|
+
enableFilePicker: false,
|
|
35
|
+
enableFileDownload: false,
|
|
36
|
+
enableSocialAuth: false,
|
|
37
|
+
enableAudioRecording: false,
|
|
38
|
+
enableVideoRecording: false,
|
|
39
|
+
enableMotionSensors: false,
|
|
40
|
+
enableLocalDatabase: false,
|
|
41
|
+
enableBluetooth: false,
|
|
42
|
+
enableNFC: false,
|
|
43
|
+
enableHealthKit: false,
|
|
44
|
+
enableLiveActivities: false,
|
|
45
|
+
enableWatchApp: false,
|
|
46
|
+
enableBackgroundLocation: false,
|
|
47
|
+
enableBackgroundTasks: false,
|
|
48
|
+
enableScreenCapture: false,
|
|
49
|
+
enablePDFViewer: false,
|
|
50
|
+
enableAR: false,
|
|
51
|
+
enableMLKit: false,
|
|
52
|
+
iosVersion: "16.0",
|
|
53
|
+
watchosVersion: "9.0",
|
|
54
|
+
teamId: "",
|
|
55
|
+
trustedOrigins: [],
|
|
56
|
+
associatedDomains: [],
|
|
57
|
+
appGroups: [],
|
|
58
|
+
orientations: ["portrait"],
|
|
59
|
+
deviceFamilies: ["iphone", "ipad"]
|
|
60
|
+
};
|
|
61
|
+
function renderDeviceFamilies(config) {
|
|
62
|
+
const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
|
|
63
|
+
return [...values].join(",");
|
|
64
|
+
}
|
|
65
|
+
function xmlEscape(value) {
|
|
66
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll(/\u0027/g, "'");
|
|
67
|
+
}
|
|
68
|
+
function plistString(key, value) {
|
|
69
|
+
return ` <key>${key}</key>
|
|
70
|
+
<string>${xmlEscape(value)}</string>`;
|
|
71
|
+
}
|
|
72
|
+
function renderUsageDescriptions(config) {
|
|
73
|
+
const entries = [
|
|
74
|
+
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
75
|
+
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
76
|
+
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
77
|
+
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
78
|
+
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
79
|
+
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
80
|
+
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
81
|
+
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
82
|
+
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
83
|
+
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
84
|
+
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
85
|
+
[config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
|
|
86
|
+
[config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
|
|
87
|
+
[config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
|
|
88
|
+
];
|
|
89
|
+
return entries.filter(([enabled]) => enabled).map(([, key, value]) => plistString(key, value)).join(`
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
function renderOrientations(config) {
|
|
93
|
+
const names = {
|
|
94
|
+
portrait: "UIInterfaceOrientationPortrait",
|
|
95
|
+
"landscape-left": "UIInterfaceOrientationLandscapeLeft",
|
|
96
|
+
"landscape-right": "UIInterfaceOrientationLandscapeRight",
|
|
97
|
+
"portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
|
|
98
|
+
};
|
|
99
|
+
const values = config.orientations?.length ? config.orientations : ["portrait"];
|
|
100
|
+
return values.map((value) => ` <string>${names[value]}</string>`).join(`
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
function renderUrlTypes(config) {
|
|
104
|
+
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
|
|
105
|
+
if (!schemes.length)
|
|
106
|
+
return "";
|
|
107
|
+
return ` <key>CFBundleURLTypes</key>
|
|
108
|
+
<array>
|
|
109
|
+
<dict>
|
|
110
|
+
<key>CFBundleURLSchemes</key>
|
|
111
|
+
<array>
|
|
112
|
+
${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
|
|
113
|
+
`)}
|
|
114
|
+
</array>
|
|
115
|
+
</dict>
|
|
116
|
+
</array>`;
|
|
117
|
+
}
|
|
118
|
+
function plistArray(values, indent = 2) {
|
|
119
|
+
const padding = " ".repeat(indent);
|
|
120
|
+
return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
function renderBackgroundModes(config) {
|
|
124
|
+
const modes = new Set;
|
|
125
|
+
if (config.enableBackgroundLocation)
|
|
126
|
+
modes.add("location");
|
|
127
|
+
if (config.enableBackgroundTasks)
|
|
128
|
+
modes.add("processing");
|
|
129
|
+
if (config.enablePushNotifications)
|
|
130
|
+
modes.add("remote-notification");
|
|
131
|
+
if (!modes.size)
|
|
132
|
+
return "";
|
|
133
|
+
return ` <key>UIBackgroundModes</key>
|
|
134
|
+
<array>
|
|
135
|
+
${plistArray([...modes])}
|
|
136
|
+
</array>`;
|
|
137
|
+
}
|
|
138
|
+
function renderEntitlements(config) {
|
|
139
|
+
const entries = [];
|
|
140
|
+
if (config.associatedDomains?.length) {
|
|
141
|
+
entries.push(` <key>com.apple.developer.associated-domains</key>
|
|
142
|
+
<array>
|
|
143
|
+
${plistArray(config.associatedDomains)}
|
|
144
|
+
</array>`);
|
|
145
|
+
}
|
|
146
|
+
if (config.appGroups?.length) {
|
|
147
|
+
entries.push(` <key>com.apple.security.application-groups</key>
|
|
148
|
+
<array>
|
|
149
|
+
${plistArray(config.appGroups)}
|
|
150
|
+
</array>`);
|
|
151
|
+
}
|
|
152
|
+
if (config.enableHealthKit) {
|
|
153
|
+
entries.push(` <key>com.apple.developer.healthkit</key>
|
|
154
|
+
<true/>`);
|
|
155
|
+
}
|
|
156
|
+
if (config.enablePushNotifications) {
|
|
157
|
+
entries.push(` <key>aps-environment</key>
|
|
158
|
+
<string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
|
|
159
|
+
}
|
|
160
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
161
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
162
|
+
<plist version="1.0">
|
|
163
|
+
<dict>
|
|
164
|
+
${entries.join(`
|
|
165
|
+
`)}
|
|
166
|
+
</dict>
|
|
167
|
+
</plist>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
function renderWatchEntitlements(config) {
|
|
171
|
+
const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
|
|
172
|
+
<array>
|
|
173
|
+
${plistArray(config.appGroups)}
|
|
174
|
+
</array>
|
|
175
|
+
` : "";
|
|
176
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
177
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
178
|
+
<plist version="1.0">
|
|
179
|
+
<dict>
|
|
180
|
+
${appGroups}</dict>
|
|
181
|
+
</plist>
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
function renderRuntimeSettings() {
|
|
185
|
+
return [
|
|
186
|
+
' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
|
|
187
|
+
' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
|
|
188
|
+
' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
|
|
189
|
+
].join(`
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
function renderPrivacyManifest(config) {
|
|
193
|
+
const privacy = config.privacy ?? {};
|
|
194
|
+
const collected = privacy.collectedDataTypes ?? [];
|
|
195
|
+
const accessed = privacy.accessedApiTypes ?? [];
|
|
196
|
+
const collectedXml = collected.map((item) => ` <dict>
|
|
197
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
198
|
+
<string>${xmlEscape(item.type)}</string>
|
|
199
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
200
|
+
<${item.linked ? "true" : "false"}/>
|
|
201
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
202
|
+
<${item.tracking ? "true" : "false"}/>
|
|
203
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
204
|
+
<array>
|
|
205
|
+
${plistArray(item.purposes, 4)}
|
|
206
|
+
</array>
|
|
207
|
+
</dict>`).join(`
|
|
208
|
+
`);
|
|
209
|
+
const accessedXml = accessed.map((item) => ` <dict>
|
|
210
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
211
|
+
<string>${xmlEscape(item.type)}</string>
|
|
212
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
213
|
+
<array>
|
|
214
|
+
${plistArray(item.reasons, 4)}
|
|
215
|
+
</array>
|
|
216
|
+
</dict>`).join(`
|
|
217
|
+
`);
|
|
218
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
219
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
220
|
+
<plist version="1.0">
|
|
221
|
+
<dict>
|
|
222
|
+
<key>NSPrivacyTracking</key>
|
|
223
|
+
<${privacy.tracking ? "true" : "false"}/>
|
|
224
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
225
|
+
<array>
|
|
226
|
+
${plistArray(privacy.trackingDomains ?? [])}
|
|
227
|
+
</array>
|
|
228
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
229
|
+
<array>
|
|
230
|
+
${collectedXml}
|
|
231
|
+
</array>
|
|
232
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
233
|
+
<array>
|
|
234
|
+
${accessedXml}
|
|
235
|
+
</array>
|
|
236
|
+
</dict>
|
|
237
|
+
</plist>
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
function renderAssetCatalog(output, config) {
|
|
241
|
+
const catalog = join(output, "Assets.xcassets");
|
|
242
|
+
const appIcon = join(catalog, "AppIcon.appiconset");
|
|
243
|
+
const launchBackground = join(catalog, "LaunchBackground.colorset");
|
|
244
|
+
mkdirSync(appIcon, { recursive: true });
|
|
245
|
+
mkdirSync(launchBackground, { recursive: true });
|
|
246
|
+
writeFileSync(join(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
247
|
+
`);
|
|
248
|
+
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
249
|
+
if (config.appIconPath) {
|
|
250
|
+
if (!existsSync(config.appIconPath))
|
|
251
|
+
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
252
|
+
cpSync(config.appIconPath, join(appIcon, iconFilename));
|
|
253
|
+
}
|
|
254
|
+
writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
|
|
255
|
+
images: iconFilename ? [{
|
|
256
|
+
filename: iconFilename,
|
|
257
|
+
idiom: "universal",
|
|
258
|
+
platform: "ios",
|
|
259
|
+
size: "1024x1024"
|
|
260
|
+
}] : [],
|
|
261
|
+
info: { author: "xcode", version: 1 }
|
|
262
|
+
}, null, 2)}
|
|
263
|
+
`);
|
|
264
|
+
const color = config.backgroundColor?.replace(/^#/, "") || "000000";
|
|
265
|
+
const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
|
|
266
|
+
const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
|
|
267
|
+
writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
268
|
+
colors: [{
|
|
269
|
+
color: {
|
|
270
|
+
"color-space": "srgb",
|
|
271
|
+
components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
|
|
272
|
+
},
|
|
273
|
+
idiom: "universal"
|
|
274
|
+
}],
|
|
275
|
+
info: { author: "xcode", version: 1 }
|
|
276
|
+
}, null, 2)}
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
function syncWebAssets(source, output) {
|
|
280
|
+
const sourcePath = resolve(source);
|
|
281
|
+
if (!existsSync(sourcePath))
|
|
282
|
+
throw new Error(`Web asset path not found: ${source}`);
|
|
283
|
+
const distDir = join(output, "dist");
|
|
284
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
285
|
+
mkdirSync(distDir, { recursive: true });
|
|
286
|
+
if (statSync(sourcePath).isDirectory()) {
|
|
287
|
+
cpSync(sourcePath, distDir, { recursive: true });
|
|
288
|
+
} else {
|
|
289
|
+
cpSync(sourcePath, join(distDir, "index.html"));
|
|
290
|
+
}
|
|
291
|
+
if (!existsSync(join(distDir, "index.html"))) {
|
|
292
|
+
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function resolveRuntimeDir(override) {
|
|
296
|
+
if (override === null)
|
|
297
|
+
return null;
|
|
298
|
+
const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
|
|
299
|
+
if (!dir)
|
|
300
|
+
return null;
|
|
301
|
+
if (!existsSync(dir)) {
|
|
302
|
+
const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
|
|
303
|
+
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
304
|
+
}
|
|
305
|
+
return dir;
|
|
306
|
+
}
|
|
307
|
+
var RUNTIME_ARCHIVES = {
|
|
308
|
+
device: ["libcraft-ios.a"],
|
|
309
|
+
simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
|
|
310
|
+
};
|
|
311
|
+
async function installRuntime(output, runtimeDir) {
|
|
312
|
+
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
313
|
+
const present = archives.filter((a) => existsSync(join(runtimeDir, a)));
|
|
314
|
+
if (present.length === 0) {
|
|
315
|
+
throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
|
|
316
|
+
}
|
|
317
|
+
return { sdk, archives, present };
|
|
318
|
+
});
|
|
319
|
+
const dest = join(output, "Runtime");
|
|
320
|
+
rmSync(dest, { recursive: true, force: true });
|
|
321
|
+
for (const { sdk, archives, present } of resolved) {
|
|
322
|
+
const sdkDir = join(dest, sdk);
|
|
323
|
+
mkdirSync(sdkDir, { recursive: true });
|
|
324
|
+
const target = join(sdkDir, "libcraft-ios.a");
|
|
325
|
+
if (present.length === 1) {
|
|
326
|
+
const missing = archives.filter((a) => !present.includes(a));
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
329
|
+
}
|
|
330
|
+
cpSync(join(runtimeDir, present[0]), target);
|
|
331
|
+
} else {
|
|
332
|
+
await $`lipo -create ${present.map((a) => join(runtimeDir, a))} -output ${target}`.quiet();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
async function refreshRuntime(output, override) {
|
|
338
|
+
if (!existsSync(join(output, "Runtime")))
|
|
339
|
+
return;
|
|
340
|
+
const dir = resolveRuntimeDir(override);
|
|
341
|
+
if (!dir) {
|
|
342
|
+
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
await installRuntime(output, dir);
|
|
346
|
+
console.log(" Refreshed the Zig runtime from", dir);
|
|
347
|
+
}
|
|
348
|
+
async function init(options) {
|
|
349
|
+
const { name, bundleId, teamId, output } = options;
|
|
350
|
+
console.log(`
|
|
351
|
+
\u26A1 Initializing Craft iOS project: ${name}`);
|
|
352
|
+
console.log(` Output: ${output}
|
|
353
|
+
`);
|
|
354
|
+
const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
|
|
355
|
+
if (options.config?.enableWatchApp)
|
|
356
|
+
dirs.push(join(output, "WatchApp"), join(output, "WatchExtension"));
|
|
357
|
+
for (const dir of dirs) {
|
|
358
|
+
if (!existsSync(dir)) {
|
|
359
|
+
mkdirSync(dir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const finalBundleId = bundleId || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
|
363
|
+
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
364
|
+
const config = {
|
|
365
|
+
...DEFAULT_CONFIG,
|
|
366
|
+
appName: name,
|
|
367
|
+
bundleId: finalBundleId,
|
|
368
|
+
teamId: teamId || "",
|
|
369
|
+
...options.config
|
|
370
|
+
};
|
|
371
|
+
if (config.enableBackgroundLocation)
|
|
372
|
+
config.enableGeolocation = true;
|
|
373
|
+
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
374
|
+
const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
375
|
+
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
376
|
+
writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
|
|
377
|
+
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
378
|
+
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light").replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
379
|
+
<true/>
|
|
380
|
+
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
381
|
+
<true/>` : "");
|
|
382
|
+
writeFileSync(join(output, "Info.plist"), infoPlist);
|
|
383
|
+
const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
384
|
+
const nativeDependencies = [];
|
|
385
|
+
if (config.enableLiveActivities)
|
|
386
|
+
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
387
|
+
if (config.enableWatchApp)
|
|
388
|
+
nativeDependencies.push(` - target: ${name}Watch
|
|
389
|
+
embed: true`);
|
|
390
|
+
const nativeTargets = [];
|
|
391
|
+
if (config.enableLiveActivities) {
|
|
392
|
+
nativeTargets.push(` ${name}LiveActivity:
|
|
393
|
+
type: app-extension
|
|
394
|
+
platform: iOS
|
|
395
|
+
deploymentTarget: "16.2"
|
|
396
|
+
sources:
|
|
397
|
+
- WidgetExtension
|
|
398
|
+
- Shared
|
|
399
|
+
settings:
|
|
400
|
+
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
401
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
402
|
+
SWIFT_VERSION: "5.0"
|
|
403
|
+
APPLICATION_EXTENSION_API_ONLY: YES
|
|
404
|
+
SKIP_INSTALL: YES`);
|
|
405
|
+
}
|
|
406
|
+
if (config.enableWatchApp) {
|
|
407
|
+
nativeTargets.push(` ${name}Watch:
|
|
408
|
+
type: application.watchapp2
|
|
409
|
+
platform: watchOS
|
|
410
|
+
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
411
|
+
sources:
|
|
412
|
+
- WatchApp
|
|
413
|
+
settings:
|
|
414
|
+
INFOPLIST_FILE: WatchApp/Info.plist
|
|
415
|
+
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
416
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
417
|
+
SWIFT_VERSION: "5.0"
|
|
418
|
+
SKIP_INSTALL: YES
|
|
419
|
+
dependencies:
|
|
420
|
+
- target: ${name}WatchExtension
|
|
421
|
+
${name}WatchExtension:
|
|
422
|
+
type: watchkit2-extension
|
|
423
|
+
platform: watchOS
|
|
424
|
+
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
425
|
+
sources:
|
|
426
|
+
- WatchExtension
|
|
427
|
+
settings:
|
|
428
|
+
INFOPLIST_FILE: WatchExtension/Info.plist
|
|
429
|
+
CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
|
|
430
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
|
|
431
|
+
SWIFT_VERSION: "5.0"
|
|
432
|
+
APPLICATION_EXTENSION_API_ONLY: YES
|
|
433
|
+
SKIP_INSTALL: YES`);
|
|
434
|
+
}
|
|
435
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
436
|
+
const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
|
|
437
|
+
if (hasRuntime) {
|
|
438
|
+
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
439
|
+
} else {
|
|
440
|
+
rmSync(join(output, "Runtime"), { recursive: true, force: true });
|
|
441
|
+
}
|
|
442
|
+
const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
|
|
443
|
+
${nativeDependencies.join(`
|
|
444
|
+
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
445
|
+
`));
|
|
446
|
+
writeFileSync(join(output, "project.yml"), projectYml);
|
|
447
|
+
writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
|
|
448
|
+
writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
449
|
+
renderAssetCatalog(output, config);
|
|
450
|
+
cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
|
|
451
|
+
if (config.enableLiveActivities) {
|
|
452
|
+
mkdirSync(join(output, "WidgetExtension"), { recursive: true });
|
|
453
|
+
const widgetSource = readFileSync(join(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
454
|
+
writeFileSync(join(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
|
|
455
|
+
const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
456
|
+
writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
457
|
+
}
|
|
458
|
+
if (config.enableWatchApp) {
|
|
459
|
+
const watchSource = readFileSync(join(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
460
|
+
writeFileSync(join(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
|
|
461
|
+
const watchInfo = readFileSync(join(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
462
|
+
writeFileSync(join(output, "WatchApp", "Info.plist"), watchInfo);
|
|
463
|
+
writeFileSync(join(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
464
|
+
const watchExtensionInfo = readFileSync(join(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
465
|
+
writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
466
|
+
writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
467
|
+
}
|
|
468
|
+
const placeholderHtml = `<!DOCTYPE html>
|
|
469
|
+
<html>
|
|
470
|
+
<head>
|
|
471
|
+
<meta charset="UTF-8">
|
|
472
|
+
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
473
|
+
<title>${name}</title>
|
|
474
|
+
<style>
|
|
475
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
476
|
+
body {
|
|
477
|
+
font-family: -apple-system, system-ui, sans-serif;
|
|
478
|
+
background: ${config.backgroundColor};
|
|
479
|
+
color: white;
|
|
480
|
+
min-height: 100vh;
|
|
481
|
+
display: flex;
|
|
482
|
+
justify-content: center;
|
|
483
|
+
align-items: center;
|
|
484
|
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
485
|
+
}
|
|
486
|
+
.container { text-align: center; padding: 2rem; }
|
|
487
|
+
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
488
|
+
p { opacity: 0.7; }
|
|
489
|
+
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
490
|
+
</style>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<div class="container">
|
|
494
|
+
<h1>\u26A1 ${name}</h1>
|
|
495
|
+
<p>Built with Craft iOS</p>
|
|
496
|
+
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
497
|
+
</div>
|
|
498
|
+
<script>
|
|
499
|
+
window.addEventListener('craftReady', (e) => {
|
|
500
|
+
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
501
|
+
console.log('Craft capabilities:', e.detail.capabilities);
|
|
502
|
+
});
|
|
503
|
+
</script>
|
|
504
|
+
</body>
|
|
505
|
+
</html>`;
|
|
506
|
+
writeFileSync(join(output, "dist", "index.html"), placeholderHtml);
|
|
507
|
+
console.log("\u2705 Project initialized");
|
|
508
|
+
console.log("");
|
|
509
|
+
console.log("Next steps:");
|
|
510
|
+
console.log(` 1. cd ${output}`);
|
|
511
|
+
console.log(" 2. Add your web content to dist/index.html");
|
|
512
|
+
console.log(" 3. Run: craft ios build");
|
|
513
|
+
console.log(" 4. Run: craft ios open");
|
|
514
|
+
console.log("");
|
|
515
|
+
}
|
|
516
|
+
async function build(options) {
|
|
517
|
+
const { htmlPath, devServer, output, generateProject = true } = options;
|
|
518
|
+
console.log(`
|
|
519
|
+
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
520
|
+
const configPath = join(output, "craft.config.json");
|
|
521
|
+
if (!existsSync(configPath)) {
|
|
522
|
+
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
523
|
+
}
|
|
524
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
525
|
+
if (devServer) {
|
|
526
|
+
config.devServerURL = devServer;
|
|
527
|
+
const origin = new URL(devServer).origin;
|
|
528
|
+
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
529
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
530
|
+
console.log(` Dev server: ${devServer}`);
|
|
531
|
+
}
|
|
532
|
+
if (htmlPath) {
|
|
533
|
+
syncWebAssets(htmlPath, output);
|
|
534
|
+
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
535
|
+
}
|
|
536
|
+
await refreshRuntime(output, options.runtimeDir);
|
|
537
|
+
if (!generateProject)
|
|
538
|
+
return;
|
|
539
|
+
try {
|
|
540
|
+
const result = await $`which xcodegen`.quiet();
|
|
541
|
+
if (result.exitCode === 0) {
|
|
542
|
+
console.log(" Running xcodegen...");
|
|
543
|
+
await $`cd ${output} && xcodegen generate`.quiet();
|
|
544
|
+
console.log(`\u2705 Xcode project created: ${config.appName}.xcodeproj`);
|
|
545
|
+
} else {
|
|
546
|
+
throw new Error("xcodegen not found");
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
|
|
550
|
+
cause: error
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
console.log("");
|
|
554
|
+
}
|
|
555
|
+
async function open(options) {
|
|
556
|
+
const { output } = options;
|
|
557
|
+
const files = readdirSync(output);
|
|
558
|
+
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
559
|
+
if (!xcodeproj) {
|
|
560
|
+
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
561
|
+
}
|
|
562
|
+
const projectPath = join(output, xcodeproj);
|
|
563
|
+
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
564
|
+
await $`open ${projectPath}`;
|
|
565
|
+
}
|
|
566
|
+
function orderSimulators(devices) {
|
|
567
|
+
return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
|
|
568
|
+
const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
|
|
569
|
+
if (booted !== 0)
|
|
570
|
+
return booted;
|
|
571
|
+
const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
|
|
572
|
+
if (phone !== 0)
|
|
573
|
+
return phone;
|
|
574
|
+
return right.runtime.localeCompare(left.runtime);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
async function pickSimulator() {
|
|
578
|
+
const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
|
|
579
|
+
if (listed.exitCode !== 0)
|
|
580
|
+
return null;
|
|
581
|
+
let parsed;
|
|
582
|
+
try {
|
|
583
|
+
parsed = JSON.parse(listed.stdout.toString());
|
|
584
|
+
} catch {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const devices = [];
|
|
588
|
+
for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
|
|
589
|
+
for (const device of list)
|
|
590
|
+
devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
|
|
591
|
+
}
|
|
592
|
+
return orderSimulators(devices)[0] ?? null;
|
|
593
|
+
}
|
|
594
|
+
async function bootSimulator(device) {
|
|
595
|
+
if (device.state !== "Booted")
|
|
596
|
+
await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
|
|
597
|
+
await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
|
|
598
|
+
}
|
|
599
|
+
async function showSimulator() {
|
|
600
|
+
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
601
|
+
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
602
|
+
if (developer) {
|
|
603
|
+
const app = join(developer, "Applications", "Simulator.app");
|
|
604
|
+
if (existsSync(app)) {
|
|
605
|
+
await $`open ${app}`.quiet().nothrow();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await $`open -a Simulator`.quiet().nothrow();
|
|
610
|
+
}
|
|
611
|
+
async function run(options) {
|
|
612
|
+
const { simulator, output } = options;
|
|
613
|
+
await build({ output });
|
|
614
|
+
const files = readdirSync(output);
|
|
615
|
+
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
616
|
+
if (!xcodeproj) {
|
|
617
|
+
throw new Error(`No Xcode project found in ${output}`);
|
|
618
|
+
}
|
|
619
|
+
const projectPath = join(output, xcodeproj);
|
|
620
|
+
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
621
|
+
const configPath = join(output, "craft.config.json");
|
|
622
|
+
if (!existsSync(configPath))
|
|
623
|
+
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
624
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
625
|
+
if (simulator) {
|
|
626
|
+
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
627
|
+
const device = await pickSimulator();
|
|
628
|
+
if (!device) {
|
|
629
|
+
throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
|
|
630
|
+
}
|
|
631
|
+
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
632
|
+
try {
|
|
633
|
+
await bootSimulator(device);
|
|
634
|
+
const derivedData = join(output, "build");
|
|
635
|
+
const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
636
|
+
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
637
|
+
if (!existsSync(product)) {
|
|
638
|
+
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
639
|
+
}
|
|
640
|
+
await $`xcrun simctl install ${device.udid} ${product}`;
|
|
641
|
+
await showSimulator();
|
|
642
|
+
await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
|
|
643
|
+
console.log(`\u2705 ${config.appName} is running on ${device.name}`);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
|
|
649
|
+
await $`open ${projectPath}`;
|
|
650
|
+
console.log("");
|
|
651
|
+
console.log("In Xcode:");
|
|
652
|
+
console.log(" 1. Select your Team in Signing & Capabilities");
|
|
653
|
+
console.log(" 2. Connect your iPhone");
|
|
654
|
+
console.log(" 3. Select your device");
|
|
655
|
+
console.log(" 4. Click Run (\u25B6\uFE0F)");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/cli.ts
|
|
660
|
+
var { values, positionals } = parseArgs({
|
|
661
|
+
args: Bun.argv.slice(2),
|
|
662
|
+
options: {
|
|
663
|
+
help: { type: "boolean", short: "h" },
|
|
664
|
+
version: { type: "boolean", short: "v" },
|
|
665
|
+
name: { type: "string", short: "n" },
|
|
666
|
+
"bundle-id": { type: "string", short: "b" },
|
|
667
|
+
"team-id": { type: "string", short: "t" },
|
|
668
|
+
"html-path": { type: "string" },
|
|
669
|
+
"dev-server": { type: "string", short: "d" },
|
|
670
|
+
output: { type: "string", short: "o" },
|
|
671
|
+
simulator: { type: "boolean", short: "s" }
|
|
672
|
+
},
|
|
673
|
+
allowPositionals: true
|
|
674
|
+
});
|
|
675
|
+
var command = positionals[0];
|
|
676
|
+
if (values.help || !command) {
|
|
677
|
+
console.log(`
|
|
678
|
+
Craft iOS - Build native iOS apps with web technologies
|
|
679
|
+
|
|
680
|
+
Usage:
|
|
681
|
+
craft-ios <command> [options]
|
|
682
|
+
|
|
683
|
+
Commands:
|
|
684
|
+
init <name> Initialize a new iOS project
|
|
685
|
+
build Build web assets and generate Xcode project
|
|
686
|
+
open Open Xcode project
|
|
687
|
+
run Build and run on simulator
|
|
688
|
+
|
|
689
|
+
Options:
|
|
690
|
+
-n, --name <name> App name
|
|
691
|
+
-b, --bundle-id <id> Bundle identifier (e.g., com.example.app)
|
|
692
|
+
-t, --team-id <id> Apple Developer Team ID
|
|
693
|
+
--html-path <path> Path to HTML file or directory
|
|
694
|
+
-d, --dev-server <url> Development server URL
|
|
695
|
+
-o, --output <dir> Output directory (default: ./ios)
|
|
696
|
+
-s, --simulator Run on simulator instead of device
|
|
697
|
+
-h, --help Show help
|
|
698
|
+
-v, --version Show version
|
|
699
|
+
|
|
700
|
+
Examples:
|
|
701
|
+
craft-ios init MyApp
|
|
702
|
+
craft-ios build --html-path ./dist/index.html
|
|
703
|
+
craft-ios build --dev-server http://localhost:3456
|
|
704
|
+
craft-ios run --simulator
|
|
705
|
+
`);
|
|
706
|
+
process.exit(0);
|
|
707
|
+
}
|
|
708
|
+
if (values.version) {
|
|
709
|
+
console.log("craft-ios v0.1.0");
|
|
710
|
+
process.exit(0);
|
|
711
|
+
}
|
|
712
|
+
async function main() {
|
|
713
|
+
try {
|
|
714
|
+
switch (command) {
|
|
715
|
+
case "init":
|
|
716
|
+
await init({
|
|
717
|
+
name: positionals[1] || values.name || "CraftApp",
|
|
718
|
+
bundleId: values["bundle-id"],
|
|
719
|
+
teamId: values["team-id"],
|
|
720
|
+
output: values.output || "./ios"
|
|
721
|
+
});
|
|
722
|
+
break;
|
|
723
|
+
case "build":
|
|
724
|
+
await build({
|
|
725
|
+
htmlPath: values["html-path"],
|
|
726
|
+
devServer: values["dev-server"],
|
|
727
|
+
output: values.output || "./ios"
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
case "open":
|
|
731
|
+
await open({
|
|
732
|
+
output: values.output || "./ios"
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
case "run":
|
|
736
|
+
await run({
|
|
737
|
+
simulator: values.simulator || false,
|
|
738
|
+
output: values.output || "./ios"
|
|
739
|
+
});
|
|
740
|
+
break;
|
|
741
|
+
default:
|
|
742
|
+
console.error(`Unknown command: ${command}`);
|
|
743
|
+
console.log("Run craft-ios --help for usage");
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
main();
|