@choochmeque/tauri-apple-extensions 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Choochmeque
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # tauri-apple-extensions
2
+
3
+ [![npm](https://img.shields.io/npm/v/@choochmeque/tauri-apple-extensions.svg)](https://www.npmjs.com/package/@choochmeque/tauri-apple-extensions)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Add iOS extensions to Tauri apps with a single command.
7
+
8
+ ## Features
9
+
10
+ - Automatic Xcode project configuration via XcodeGen
11
+ - Plugin-based template system
12
+ - Idempotent - safe to re-run
13
+ - Share Extension support (more extension types coming soon)
14
+
15
+ ## Prerequisites
16
+
17
+ - [XcodeGen](https://github.com/yonaskolb/XcodeGen) installed (`brew install xcodegen`)
18
+ - Tauri iOS project initialized (`tauri ios init`)
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install -D @choochmeque/tauri-apple-extensions
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### With a plugin (recommended)
29
+
30
+ If you're using a Tauri plugin that provides iOS extension templates:
31
+
32
+ ```bash
33
+ npx @choochmeque/tauri-apple-extensions add share --plugin @choochmeque/tauri-plugin-sharekit-api
34
+ ```
35
+
36
+ ### With custom templates
37
+
38
+ ```bash
39
+ npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/templates
40
+ ```
41
+
42
+ ## Supported Extensions
43
+
44
+ | Type | Status | Description |
45
+ |------|--------|-------------|
46
+ | `share` | Available | Share Extension for receiving shared content |
47
+
48
+ ## For Plugin Developers
49
+
50
+ To make your plugin compatible with this tool, add the following to your `package.json`:
51
+
52
+ ```json
53
+ {
54
+ "tauri-apple-extension": {
55
+ "type": "share",
56
+ "templates": "./ios/templates"
57
+ }
58
+ }
59
+ ```
60
+
61
+ Your templates directory should contain:
62
+ - Swift source files with `{{VARIABLE}}` placeholders
63
+ - `Info.plist` for the extension
64
+
65
+ ### Template Variables
66
+
67
+ | Variable | Description |
68
+ |----------|-------------|
69
+ | `{{APP_GROUP_IDENTIFIER}}` | App Group ID (e.g., `group.com.example.app`) |
70
+ | `{{APP_URL_SCHEME}}` | URL scheme for deep linking |
71
+ | `{{VERSION}}` | App version from tauri.conf.json |
72
+ | `{{BUNDLE_IDENTIFIER}}` | Extension bundle identifier |
73
+ | `{{PRODUCT_NAME}}` | App product name |
74
+
75
+ ## Post-Setup Steps
76
+
77
+ After running the tool:
78
+
79
+ 1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
80
+ 2. Select your Apple Developer Team for both targets
81
+ 3. Enable required capabilities (e.g., App Groups) for both targets
82
+ 4. Configure the capability in Apple Developer Portal
83
+
84
+ ## Contributing
85
+
86
+ PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { execSync } from 'child_process';
7
+
8
+ /**
9
+ * Simple YAML-like parsing for the values we need.
10
+ * This is not a full YAML parser, just extracts top-level key-value pairs.
11
+ */
12
+ function parseYamlSimple(content) {
13
+ const lines = content.split("\n");
14
+ const result = {};
15
+ for (const line of lines) {
16
+ const match = line.match(/^(\w+):\s*(.+)$/);
17
+ if (match) {
18
+ result[match[1]] = match[2].trim();
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+
24
+ function findProjectRoot() {
25
+ let dir = process.cwd();
26
+ while (dir !== path.dirname(dir)) {
27
+ if (fs.existsSync(path.join(dir, "tauri.conf.json")) ||
28
+ fs.existsSync(path.join(dir, "src-tauri", "tauri.conf.json"))) {
29
+ return dir;
30
+ }
31
+ dir = path.dirname(dir);
32
+ }
33
+ return process.cwd();
34
+ }
35
+ function findTauriConfig(projectRoot) {
36
+ const paths = [
37
+ path.join(projectRoot, "src-tauri", "tauri.conf.json"),
38
+ path.join(projectRoot, "tauri.conf.json"),
39
+ ];
40
+ for (const p of paths) {
41
+ if (fs.existsSync(p)) {
42
+ return JSON.parse(fs.readFileSync(p, "utf8"));
43
+ }
44
+ }
45
+ throw new Error("Could not find tauri.conf.json");
46
+ }
47
+ function findAppleProjectDir(projectRoot) {
48
+ const paths = [
49
+ path.join(projectRoot, "src-tauri", "gen", "apple"),
50
+ path.join(projectRoot, "gen", "apple"),
51
+ ];
52
+ for (const p of paths) {
53
+ if (fs.existsSync(p)) {
54
+ return p;
55
+ }
56
+ }
57
+ throw new Error("Could not find iOS project directory. Run 'tauri ios init' first.");
58
+ }
59
+ function getAppInfo(tauriConfig, projectYml) {
60
+ const parsed = parseYamlSimple(projectYml);
61
+ const productName = tauriConfig.productName || tauriConfig.package?.productName || "app";
62
+ const bundleIdPrefix = parsed.bundleIdPrefix || "com.tauri";
63
+ const identifier = tauriConfig.identifier || `${bundleIdPrefix}.${productName}`;
64
+ const version = tauriConfig.version || "1.0.0";
65
+ return {
66
+ productName,
67
+ bundleIdPrefix,
68
+ identifier,
69
+ version,
70
+ };
71
+ }
72
+
73
+ function readProjectYml(appleDir) {
74
+ const projectYmlPath = path.join(appleDir, "project.yml");
75
+ if (!fs.existsSync(projectYmlPath)) {
76
+ throw new Error("project.yml not found. Run 'tauri ios init' first.");
77
+ }
78
+ return fs.readFileSync(projectYmlPath, "utf8");
79
+ }
80
+ function writeProjectYml(appleDir, content) {
81
+ const projectYmlPath = path.join(appleDir, "project.yml");
82
+ fs.writeFileSync(projectYmlPath, content);
83
+ }
84
+ function addExtensionTarget(projectYml, targetConfig) {
85
+ let modified = projectYml;
86
+ // Remove existing target if it exists (to allow re-running the script)
87
+ // Note: Don't use 'm' flag - we need $ to match only at true end of string
88
+ const existingTargetRegex = new RegExp(`\\n ${targetConfig.name}:[\\s\\S]*?(?=\\n [a-zA-Z_]|\\n[a-zA-Z]|$)`);
89
+ if (modified.match(existingTargetRegex)) {
90
+ console.log(`Removing existing ${targetConfig.name} target to recreate it...`);
91
+ modified = modified.replace(existingTargetRegex, "");
92
+ }
93
+ // Find the end of targets section and insert the new target
94
+ if (modified.includes("targets:")) {
95
+ const targetsIndex = modified.indexOf("targets:");
96
+ const afterTargets = modified.slice(targetsIndex);
97
+ const lines = afterTargets.split("\n");
98
+ let inTargets = false;
99
+ let lastTargetEnd = targetsIndex;
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const line = lines[i];
102
+ if (line.match(/^targets:/)) {
103
+ inTargets = true;
104
+ continue;
105
+ }
106
+ if (inTargets) {
107
+ // Check if we've exited the targets section (new top-level key)
108
+ if (line.match(/^[a-zA-Z]/) && !line.startsWith(" ")) {
109
+ break;
110
+ }
111
+ lastTargetEnd = targetsIndex + lines.slice(0, i + 1).join("\n").length;
112
+ }
113
+ }
114
+ modified =
115
+ modified.slice(0, lastTargetEnd) +
116
+ targetConfig.yaml +
117
+ modified.slice(lastTargetEnd);
118
+ }
119
+ return modified;
120
+ }
121
+ function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
122
+ let modified = projectYml;
123
+ // Remove existing dependency if it exists
124
+ const existingDepRegex = new RegExp(`\\n - target: ${dependencyConfig.target}\\n embed: true\\n codeSign: true`, "g");
125
+ modified = modified.replace(existingDepRegex, "");
126
+ // Find the main target's dependencies section and add the extension
127
+ const targetRegex = new RegExp(`(${mainTargetName}:[\\s\\S]*?dependencies:)([\\s\\S]*?)(\\n\\s{4}\\w|$)`, "m");
128
+ const depMatch = modified.match(targetRegex);
129
+ if (depMatch) {
130
+ const depSection = depMatch[2];
131
+ const newDep = `\n - target: ${dependencyConfig.target}
132
+ embed: true
133
+ codeSign: true`;
134
+ if (!depSection.includes(dependencyConfig.target)) {
135
+ modified = modified.replace(targetRegex, `$1${depSection}${newDep}$3`);
136
+ }
137
+ }
138
+ return modified;
139
+ }
140
+
141
+ function updateMainAppEntitlements(appleDir, appInfo) {
142
+ const targetName = `${appInfo.productName}_iOS`;
143
+ const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
144
+ const appGroupId = `group.${appInfo.identifier}`;
145
+ let entitlements;
146
+ if (fs.existsSync(entitlementsPath)) {
147
+ entitlements = fs.readFileSync(entitlementsPath, "utf8");
148
+ }
149
+ else {
150
+ entitlements = `<?xml version="1.0" encoding="UTF-8"?>
151
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
152
+ <plist version="1.0">
153
+ <dict>
154
+ </dict>
155
+ </plist>`;
156
+ }
157
+ // Check if app groups already configured
158
+ if (entitlements.includes("com.apple.security.application-groups")) {
159
+ if (!entitlements.includes(appGroupId)) {
160
+ // Add our group to existing array
161
+ entitlements = entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
162
+ }
163
+ }
164
+ else {
165
+ // Add app groups entitlement
166
+ entitlements = entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
167
+ <key>com.apple.security.application-groups</key>
168
+ <array>
169
+ <string>${appGroupId}</string>
170
+ </array>
171
+ </dict>`);
172
+ }
173
+ fs.writeFileSync(entitlementsPath, entitlements);
174
+ console.log(`Updated main app entitlements: ${entitlementsPath}`);
175
+ }
176
+ function createExtensionEntitlements(extensionDir, appGroupId) {
177
+ const entitlements = `<?xml version="1.0" encoding="UTF-8"?>
178
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
179
+ <plist version="1.0">
180
+ <dict>
181
+ <key>com.apple.security.application-groups</key>
182
+ <array>
183
+ <string>${appGroupId}</string>
184
+ </array>
185
+ </dict>
186
+ </plist>`;
187
+ const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
188
+ fs.writeFileSync(entitlementsPath, entitlements);
189
+ return entitlementsPath;
190
+ }
191
+
192
+ function addUrlSchemeToInfoPlist(appleDir, appInfo) {
193
+ const targetName = `${appInfo.productName}_iOS`;
194
+ const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
195
+ const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
196
+ if (!fs.existsSync(infoPlistPath)) {
197
+ console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
198
+ return;
199
+ }
200
+ let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
201
+ // Check if URL schemes already configured
202
+ if (infoPlist.includes("CFBundleURLSchemes")) {
203
+ if (!infoPlist.includes(urlScheme)) {
204
+ console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
205
+ }
206
+ return;
207
+ }
208
+ // Add URL scheme - need to insert before closing </dict></plist>
209
+ const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
210
+ <array>
211
+ <dict>
212
+ <key>CFBundleURLSchemes</key>
213
+ <array>
214
+ <string>${urlScheme}</string>
215
+ </array>
216
+ <key>CFBundleURLName</key>
217
+ <string>${appInfo.identifier}</string>
218
+ </dict>
219
+ </array>
220
+ `;
221
+ // Insert before the last </dict>
222
+ infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
223
+ fs.writeFileSync(infoPlistPath, infoPlist);
224
+ console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
225
+ }
226
+
227
+ function runXcodeGen(appleDir) {
228
+ try {
229
+ console.log("Running xcodegen to regenerate project...");
230
+ execSync("xcodegen generate", {
231
+ cwd: appleDir,
232
+ stdio: "inherit",
233
+ });
234
+ console.log("Xcode project regenerated successfully!");
235
+ }
236
+ catch (error) {
237
+ console.log("\nNote: xcodegen not found or failed. You may need to run it manually:");
238
+ console.log(` cd ${appleDir} && xcodegen generate`);
239
+ console.log(` Error: ${error.message}`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Replace template variables in content.
245
+ * Variables are in the format {{VARIABLE_NAME}}
246
+ */
247
+ function replaceTemplateVariables(content, variables) {
248
+ let result = content;
249
+ for (const [key, value] of Object.entries(variables)) {
250
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
251
+ result = result.replace(regex, value);
252
+ }
253
+ return result;
254
+ }
255
+ /**
256
+ * Copy a template file to destination, replacing variables.
257
+ */
258
+ function copyTemplateFile(src, dest, variables) {
259
+ let content = fs.readFileSync(src, "utf8");
260
+ content = replaceTemplateVariables(content, variables);
261
+ fs.writeFileSync(dest, content);
262
+ }
263
+
264
+ const shareExtension = {
265
+ type: "share",
266
+ displayName: "Share Extension",
267
+ extensionSuffix: "ShareExtension",
268
+ extensionPointIdentifier: "com.apple.share-services",
269
+ extensionName(appInfo) {
270
+ return `${appInfo.productName}-ShareExtension`;
271
+ },
272
+ createFiles(appleDir, appInfo, templatesDir) {
273
+ const extensionDir = path.join(appleDir, "ShareExtension");
274
+ const appGroupId = `group.${appInfo.identifier}`;
275
+ const urlScheme = appInfo.productName
276
+ .toLowerCase()
277
+ .replace(/[^a-z0-9]/g, "");
278
+ // Create directory
279
+ if (!fs.existsSync(extensionDir)) {
280
+ fs.mkdirSync(extensionDir, { recursive: true });
281
+ }
282
+ const variables = {
283
+ APP_GROUP_IDENTIFIER: appGroupId,
284
+ APP_URL_SCHEME: urlScheme,
285
+ VERSION: appInfo.version,
286
+ BUNDLE_IDENTIFIER: `${appInfo.identifier}.ShareExtension`,
287
+ PRODUCT_NAME: appInfo.productName,
288
+ };
289
+ // Copy ShareViewController.swift
290
+ const viewControllerSrc = path.join(templatesDir, "ShareViewController.swift");
291
+ if (fs.existsSync(viewControllerSrc)) {
292
+ copyTemplateFile(viewControllerSrc, path.join(extensionDir, "ShareViewController.swift"), variables);
293
+ }
294
+ else {
295
+ throw new Error(`Template not found: ${viewControllerSrc}`);
296
+ }
297
+ // Copy Info.plist
298
+ const possibleInfoPlists = [
299
+ path.join(templatesDir, "Info.plist"),
300
+ path.join(templatesDir, "ShareExtension-Info.plist"),
301
+ ];
302
+ let infoPlistFound = false;
303
+ for (const src of possibleInfoPlists) {
304
+ if (fs.existsSync(src)) {
305
+ copyTemplateFile(src, path.join(extensionDir, "Info.plist"), variables);
306
+ infoPlistFound = true;
307
+ break;
308
+ }
309
+ }
310
+ if (!infoPlistFound) {
311
+ // Create a default Info.plist
312
+ const defaultInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
313
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
314
+ <plist version="1.0">
315
+ <dict>
316
+ <key>CFBundleDevelopmentRegion</key>
317
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
318
+ <key>CFBundleDisplayName</key>
319
+ <string>Share</string>
320
+ <key>CFBundleExecutable</key>
321
+ <string>$(EXECUTABLE_NAME)</string>
322
+ <key>CFBundleIdentifier</key>
323
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
324
+ <key>CFBundleInfoDictionaryVersion</key>
325
+ <string>6.0</string>
326
+ <key>CFBundleName</key>
327
+ <string>$(PRODUCT_NAME)</string>
328
+ <key>CFBundlePackageType</key>
329
+ <string>XPC!</string>
330
+ <key>CFBundleShortVersionString</key>
331
+ <string>${appInfo.version}</string>
332
+ <key>CFBundleVersion</key>
333
+ <string>${appInfo.version}</string>
334
+ <key>NSExtension</key>
335
+ <dict>
336
+ <key>NSExtensionAttributes</key>
337
+ <dict>
338
+ <key>NSExtensionActivationRule</key>
339
+ <dict>
340
+ <key>NSExtensionActivationSupportsFileWithMaxCount</key>
341
+ <integer>10</integer>
342
+ <key>NSExtensionActivationSupportsImageWithMaxCount</key>
343
+ <integer>10</integer>
344
+ <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
345
+ <integer>10</integer>
346
+ <key>NSExtensionActivationSupportsText</key>
347
+ <true/>
348
+ <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
349
+ <integer>1</integer>
350
+ </dict>
351
+ </dict>
352
+ <key>NSExtensionPointIdentifier</key>
353
+ <string>com.apple.share-services</string>
354
+ <key>NSExtensionPrincipalClass</key>
355
+ <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
356
+ </dict>
357
+ </dict>
358
+ </plist>`;
359
+ fs.writeFileSync(path.join(extensionDir, "Info.plist"), defaultInfoPlist);
360
+ }
361
+ // Create entitlements
362
+ createExtensionEntitlements(extensionDir, appGroupId);
363
+ console.log(`Created ShareExtension files in ${extensionDir}`);
364
+ },
365
+ updateProjectYml(projectYml, appInfo) {
366
+ const extensionName = this.extensionName(appInfo);
367
+ const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
368
+ const targetName = `${appInfo.productName}_iOS`;
369
+ // Create the extension target YAML
370
+ const extensionTarget = `
371
+ ${extensionName}:
372
+ type: app-extension
373
+ platform: iOS
374
+ deploymentTarget: "14.0"
375
+ sources:
376
+ - path: ShareExtension
377
+ info:
378
+ path: ShareExtension/Info.plist
379
+ properties:
380
+ CFBundleDisplayName: Share
381
+ CFBundleShortVersionString: "${appInfo.version}"
382
+ CFBundleVersion: "${appInfo.version}"
383
+ NSExtension:
384
+ NSExtensionAttributes:
385
+ NSExtensionActivationRule:
386
+ NSExtensionActivationSupportsFileWithMaxCount: 10
387
+ NSExtensionActivationSupportsImageWithMaxCount: 10
388
+ NSExtensionActivationSupportsMovieWithMaxCount: 10
389
+ NSExtensionActivationSupportsText: true
390
+ NSExtensionActivationSupportsWebURLWithMaxCount: 1
391
+ NSExtensionPointIdentifier: com.apple.share-services
392
+ NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController
393
+ settings:
394
+ base:
395
+ PRODUCT_BUNDLE_IDENTIFIER: ${extensionBundleId}
396
+ SKIP_INSTALL: YES
397
+ CODE_SIGN_ENTITLEMENTS: ShareExtension/ShareExtension.entitlements
398
+ `;
399
+ // Add the target
400
+ let modified = addExtensionTarget(projectYml, {
401
+ name: extensionName,
402
+ yaml: extensionTarget,
403
+ });
404
+ // Add dependency to main target
405
+ modified = addDependencyToTarget(modified, targetName, {
406
+ target: extensionName,
407
+ });
408
+ return modified;
409
+ },
410
+ };
411
+
412
+ const __filename$1 = fileURLToPath(import.meta.url);
413
+ const __dirname$1 = path.dirname(__filename$1);
414
+ const EXTENSIONS = {
415
+ share: shareExtension,
416
+ };
417
+ function resolveTemplatesDir(options, extensionType) {
418
+ // Option 1: Explicit templates path
419
+ if (options.templates) {
420
+ const templatesPath = path.resolve(process.cwd(), options.templates);
421
+ if (!fs.existsSync(templatesPath)) {
422
+ throw new Error(`Templates directory not found: ${templatesPath}`);
423
+ }
424
+ return templatesPath;
425
+ }
426
+ // Option 2: Plugin templates
427
+ if (options.plugin) {
428
+ const pluginPath = resolvePluginPath(options.plugin);
429
+ const pluginPkg = JSON.parse(fs.readFileSync(path.join(pluginPath, "package.json"), "utf8"));
430
+ const extensionConfig = pluginPkg["tauri-apple-extension"];
431
+ if (!extensionConfig) {
432
+ throw new Error(`Plugin ${options.plugin} does not have tauri-apple-extension config in package.json`);
433
+ }
434
+ if (extensionConfig.type !== extensionType) {
435
+ throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
436
+ }
437
+ const templatesPath = path.join(pluginPath, extensionConfig.templates);
438
+ if (!fs.existsSync(templatesPath)) {
439
+ throw new Error(`Plugin templates directory not found: ${templatesPath}`);
440
+ }
441
+ return templatesPath;
442
+ }
443
+ // Option 3: Default templates
444
+ const defaultTemplates = path.join(__dirname$1, "../../templates", extensionType);
445
+ if (!fs.existsSync(defaultTemplates)) {
446
+ throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
447
+ }
448
+ return defaultTemplates;
449
+ }
450
+ function resolvePluginPath(pluginName) {
451
+ // Try to find in node_modules
452
+ const possiblePaths = [
453
+ path.join(process.cwd(), "node_modules", pluginName),
454
+ path.join(process.cwd(), "src-tauri", "node_modules", pluginName),
455
+ ];
456
+ for (const p of possiblePaths) {
457
+ if (fs.existsSync(p)) {
458
+ return p;
459
+ }
460
+ }
461
+ throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
462
+ }
463
+ async function addExtension(type, options) {
464
+ console.log(`\nTauri Apple Extensions - Add ${type}\n`);
465
+ try {
466
+ // Validate extension type
467
+ const extension = EXTENSIONS[type];
468
+ if (!extension) {
469
+ const available = Object.keys(EXTENSIONS).join(", ");
470
+ throw new Error(`Unknown extension type: ${type}. Available: ${available}`);
471
+ }
472
+ // Find project
473
+ const projectRoot = findProjectRoot();
474
+ console.log(`Project root: ${projectRoot}`);
475
+ const tauriConfig = findTauriConfig(projectRoot);
476
+ const appleDir = findAppleProjectDir(projectRoot);
477
+ console.log(`Apple project dir: ${appleDir}`);
478
+ // Get app info
479
+ let projectYml = readProjectYml(appleDir);
480
+ const appInfo = getAppInfo(tauriConfig, projectYml);
481
+ console.log(`\nApp Info:`);
482
+ console.log(` Product Name: ${appInfo.productName}`);
483
+ console.log(` Bundle ID: ${appInfo.identifier}`);
484
+ console.log(` Version: ${appInfo.version}`);
485
+ const appGroupId = `group.${appInfo.identifier}`;
486
+ const urlScheme = appInfo.productName
487
+ .toLowerCase()
488
+ .replace(/[^a-z0-9]/g, "");
489
+ console.log(` App Group: ${appGroupId}`);
490
+ console.log(` URL Scheme: ${urlScheme}`);
491
+ // Resolve templates
492
+ const templatesDir = resolveTemplatesDir(options, type);
493
+ console.log(`\nUsing templates from: ${templatesDir}`);
494
+ // Run extension setup
495
+ console.log(`\n1. Creating ${extension.displayName} files...`);
496
+ extension.createFiles(appleDir, appInfo, templatesDir);
497
+ console.log(`\n2. Updating main app entitlements...`);
498
+ updateMainAppEntitlements(appleDir, appInfo);
499
+ console.log(`\n3. Adding URL scheme to Info.plist...`);
500
+ addUrlSchemeToInfoPlist(appleDir, appInfo);
501
+ console.log(`\n4. Updating project.yml...`);
502
+ projectYml = readProjectYml(appleDir);
503
+ projectYml = extension.updateProjectYml(projectYml, appInfo);
504
+ writeProjectYml(appleDir, projectYml);
505
+ console.log(`\n5. Regenerating Xcode project...`);
506
+ runXcodeGen(appleDir);
507
+ console.log(`\n========================================`);
508
+ console.log(`${extension.displayName} setup complete!`);
509
+ console.log(`========================================\n`);
510
+ console.log(`Next steps:`);
511
+ console.log(`1. Open the Xcode project`);
512
+ console.log(`2. Select your team for both targets (main app and ${extension.extensionName(appInfo)})`);
513
+ console.log(`3. Enable 'App Groups' capability for both targets with: ${appGroupId}`);
514
+ console.log(`4. Build and run!\n`);
515
+ console.log(`IMPORTANT: You need to configure App Groups in Apple Developer Portal:`);
516
+ console.log(` - Create App Group: ${appGroupId}`);
517
+ console.log(` - Add it to both App IDs: ${appInfo.identifier} and ${appInfo.identifier}.${extension.extensionSuffix}\n`);
518
+ }
519
+ catch (error) {
520
+ console.error(`\nError: ${error.message}`);
521
+ process.exit(1);
522
+ }
523
+ }
524
+
525
+ const program = new Command();
526
+ program
527
+ .name("tauri-apple-extensions")
528
+ .description("Add iOS extensions to Tauri apps")
529
+ .version("0.1.0");
530
+ program
531
+ .command("add <type>")
532
+ .description("Add an extension (e.g., share)")
533
+ .option("-p, --plugin <name>", "Plugin to use for templates")
534
+ .option("-t, --templates <path>", "Custom templates directory")
535
+ .action(addExtension);
536
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import type { AddOptions } from "../types.js";
2
+ export declare function addExtension(type: string, options: AddOptions): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import type { AppInfo } from "../types.js";
2
+ export declare function updateMainAppEntitlements(appleDir: string, appInfo: AppInfo): void;
3
+ export declare function createExtensionEntitlements(extensionDir: string, appGroupId: string): string;
@@ -0,0 +1,2 @@
1
+ import type { AppInfo } from "../types.js";
2
+ export declare function addUrlSchemeToInfoPlist(appleDir: string, appInfo: AppInfo): void;
@@ -0,0 +1,5 @@
1
+ import type { AppInfo, TauriConfig } from "../types.js";
2
+ export declare function findProjectRoot(): string;
3
+ export declare function findTauriConfig(projectRoot: string): TauriConfig;
4
+ export declare function findAppleProjectDir(projectRoot: string): string;
5
+ export declare function getAppInfo(tauriConfig: TauriConfig, projectYml: string): AppInfo;
@@ -0,0 +1,5 @@
1
+ import type { TargetConfig, DependencyConfig } from "../types.js";
2
+ export declare function readProjectYml(appleDir: string): string;
3
+ export declare function writeProjectYml(appleDir: string, content: string): void;
4
+ export declare function addExtensionTarget(projectYml: string, targetConfig: TargetConfig): string;
5
+ export declare function addDependencyToTarget(projectYml: string, mainTargetName: string, dependencyConfig: DependencyConfig): string;
@@ -0,0 +1 @@
1
+ export declare function runXcodeGen(appleDir: string): void;
@@ -0,0 +1,2 @@
1
+ import type { Extension } from "../types.js";
2
+ export declare const shareExtension: Extension;