mobile-app-builder 0.0.1

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.
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchGradleProperties = patchGradleProperties;
7
+ exports.getGradleVersionCode = getGradleVersionCode;
8
+ exports.syncBuildGradle = syncBuildGradle;
9
+ exports.runGradleBuild = runGradleBuild;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const execa_1 = require("execa");
12
+ const fs_extra_1 = __importDefault(require("fs-extra"));
13
+ const path_1 = __importDefault(require("path"));
14
+ async function patchGradleProperties() {
15
+ console.log(chalk_1.default.cyan("Patching gradle.properties for memory limit..."));
16
+ const propsPath = path_1.default.join(process.cwd(), "android", "gradle.properties");
17
+ if (await fs_extra_1.default.pathExists(propsPath)) {
18
+ let props = await fs_extra_1.default.readFile(propsPath, "utf8");
19
+ props = props.replace(/^org.gradle.jvmargs=.*?$/m, "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseG1GC -Dfile.encoding=UTF-8");
20
+ if (!props.includes("org.gradle.caching")) {
21
+ props +=
22
+ "\norg.gradle.caching=true\norg.gradle.configureondemand=true\norg.gradle.parallel=true\norg.gradle.daemon=true\nkotlin.incremental=true";
23
+ }
24
+ await fs_extra_1.default.writeFile(propsPath, props);
25
+ }
26
+ }
27
+ async function getGradleVersionCode() {
28
+ const gradlePath = path_1.default.join(process.cwd(), "android", "app", "build.gradle");
29
+ if (await fs_extra_1.default.pathExists(gradlePath)) {
30
+ try {
31
+ const content = await fs_extra_1.default.readFile(gradlePath, "utf8");
32
+ const match = content.match(/versionCode\s*=?\s*(\d+)/);
33
+ if (match && match[1]) {
34
+ return parseInt(match[1], 10);
35
+ }
36
+ }
37
+ catch { }
38
+ }
39
+ return 1;
40
+ }
41
+ async function syncBuildGradle(appVersion, versionCode) {
42
+ const gradlePath = path_1.default.join(process.cwd(), "android", "app", "build.gradle");
43
+ if (await fs_extra_1.default.pathExists(gradlePath)) {
44
+ let content = await fs_extra_1.default.readFile(gradlePath, "utf8");
45
+ content = content.replace(/versionName\s*=?\s*".*?"/, `versionName "${appVersion}"`);
46
+ if (versionCode > 0) {
47
+ content = content.replace(/versionCode\s*=?\s*\d+/, `versionCode ${versionCode}`);
48
+ }
49
+ content = content.replace(/(\brelease\s*\{[\s\S]*?)(signingConfig\s+signingConfigs\.debug)/, "$1signingConfig null");
50
+ await fs_extra_1.default.writeFile(gradlePath, content);
51
+ }
52
+ }
53
+ async function runGradleBuild(assembleTask) {
54
+ const isWin = process.platform === "win32";
55
+ const cmd = isWin ? ".\\gradlew.bat" : "./gradlew";
56
+ if (!isWin) {
57
+ try {
58
+ await (0, execa_1.execa)("chmod", ["+x", "gradlew"], { cwd: "android" });
59
+ }
60
+ catch { }
61
+ }
62
+ console.log(chalk_1.default.cyan(`Building (${assembleTask})...`));
63
+ try {
64
+ await (0, execa_1.execa)(cmd, [
65
+ assembleTask,
66
+ "--build-cache",
67
+ "--configure-on-demand",
68
+ "--parallel",
69
+ "--quiet",
70
+ ], { cwd: "android", stdio: "inherit" });
71
+ }
72
+ catch (err) {
73
+ console.error(chalk_1.default.red("Error: Gradle build failed."));
74
+ throw err;
75
+ }
76
+ }
@@ -0,0 +1,22 @@
1
+ export declare function bumpVersion(): Promise<void>;
2
+ export declare function syncAppJsonVersion(): Promise<void>;
3
+ /**
4
+ * Unified project version synchronizer
5
+ */
6
+ export declare function syncProjectVersionConfig(shouldBump: boolean, projectType: "expo" | "react-native"): Promise<void>;
7
+ /**
8
+ * Converts a package name or raw slug into a proper, human-readable display name.
9
+ * e.g., "@scope/my-awesome-app" -> "My Awesome App"
10
+ * "mobile-app-builder" -> "Mobile App Builder"
11
+ * "my_cool_app" -> "My Cool App"
12
+ * "myApp" -> "My App"
13
+ */
14
+ export declare function toProperName(name?: string): string;
15
+ /**
16
+ * Extracts app display name from native strings.xml or package.json
17
+ */
18
+ export declare function getNativeAppName(): Promise<string>;
19
+ /**
20
+ * Extracts package name / applicationId from Gradle or AndroidManifest
21
+ */
22
+ export declare function getNativePackageName(): Promise<string | null>;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.bumpVersion = bumpVersion;
7
+ exports.syncAppJsonVersion = syncAppJsonVersion;
8
+ exports.syncProjectVersionConfig = syncProjectVersionConfig;
9
+ exports.toProperName = toProperName;
10
+ exports.getNativeAppName = getNativeAppName;
11
+ exports.getNativePackageName = getNativePackageName;
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ const execa_1 = require("execa");
14
+ const fs_extra_1 = __importDefault(require("fs-extra"));
15
+ const path_1 = __importDefault(require("path"));
16
+ async function bumpVersion() {
17
+ console.log(chalk_1.default.cyan("Bumping app version string..."));
18
+ try {
19
+ await (0, execa_1.execa)("npm", ["version", "patch", "--no-git-tag-version"]);
20
+ console.log(chalk_1.default.green("Successfully bumped package version."));
21
+ }
22
+ catch (err) {
23
+ console.log(chalk_1.default.yellow("Warning: Failed to bump version via npm."));
24
+ }
25
+ }
26
+ async function syncAppJsonVersion() {
27
+ const appJsonPath = path_1.default.join(process.cwd(), "app.json");
28
+ const pkgPath = path_1.default.join(process.cwd(), "package.json");
29
+ if ((await fs_extra_1.default.pathExists(appJsonPath)) && (await fs_extra_1.default.pathExists(pkgPath))) {
30
+ try {
31
+ const pkg = await fs_extra_1.default.readJson(pkgPath);
32
+ const appJson = await fs_extra_1.default.readJson(appJsonPath);
33
+ if (appJson.expo && appJson.expo.version !== pkg.version) {
34
+ appJson.expo.version = pkg.version;
35
+ await fs_extra_1.default.writeJson(appJsonPath, appJson, { spaces: 2 });
36
+ console.log(chalk_1.default.green(`Synced app.json version to ${pkg.version}.`));
37
+ }
38
+ }
39
+ catch { }
40
+ }
41
+ }
42
+ /**
43
+ * Unified project version synchronizer
44
+ */
45
+ async function syncProjectVersionConfig(shouldBump, projectType) {
46
+ if (shouldBump) {
47
+ console.log(chalk_1.default.cyan("\nStep 02: Bumping app version in package.json..."));
48
+ await bumpVersion();
49
+ }
50
+ else {
51
+ console.log(chalk_1.default.gray("\nStep 02: Preserving existing app version in package.json..."));
52
+ }
53
+ if (projectType === "expo") {
54
+ await syncAppJsonVersion();
55
+ }
56
+ }
57
+ /**
58
+ * Converts a package name or raw slug into a proper, human-readable display name.
59
+ * e.g., "@scope/my-awesome-app" -> "My Awesome App"
60
+ * "mobile-app-builder" -> "Mobile App Builder"
61
+ * "my_cool_app" -> "My Cool App"
62
+ * "myApp" -> "My App"
63
+ */
64
+ function toProperName(name) {
65
+ if (!name || typeof name !== "string")
66
+ return "App";
67
+ // Remove npm scope if present: @scope/my-app -> my-app
68
+ let cleaned = name.replace(/^@[^/]+\//, "");
69
+ // Insert space between lower/number and upper case in camelCase (e.g. "myApp" -> "my App")
70
+ cleaned = cleaned.replace(/([a-z0-9])([A-Z])/g, (_m, p1, p2) => `${p1} ${p2}`);
71
+ // Replace delimiters (hyphens, underscores, dots, slashes) with spaces
72
+ cleaned = cleaned.replace(/[-_./]+/g, " ");
73
+ // Remove any characters that could disrupt dname format or names
74
+ cleaned = cleaned.replace(/[,="+\\]/g, "");
75
+ // Capitalize each word
76
+ const words = cleaned
77
+ .trim()
78
+ .split(/\s+/)
79
+ .filter(Boolean)
80
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1));
81
+ return words.join(" ") || "App";
82
+ }
83
+ /**
84
+ * Extracts app display name from native strings.xml or package.json
85
+ */
86
+ async function getNativeAppName() {
87
+ const cwd = process.cwd();
88
+ // Try android strings.xml
89
+ const stringsPath = path_1.default.join(cwd, "android", "app", "src", "main", "res", "values", "strings.xml");
90
+ if (await fs_extra_1.default.pathExists(stringsPath)) {
91
+ try {
92
+ const content = await fs_extra_1.default.readFile(stringsPath, "utf8");
93
+ const match = content.match(/<string\s+name=["']app_name["']>([^<]+)<\/string>/);
94
+ if (match && match[1]) {
95
+ return match[1].trim();
96
+ }
97
+ }
98
+ catch { }
99
+ }
100
+ // Try package.json
101
+ const pkgPath = path_1.default.join(cwd, "package.json");
102
+ if (await fs_extra_1.default.pathExists(pkgPath)) {
103
+ try {
104
+ const pkg = await fs_extra_1.default.readJson(pkgPath);
105
+ if (pkg.displayName)
106
+ return pkg.displayName;
107
+ if (pkg.productName)
108
+ return pkg.productName;
109
+ if (pkg.name)
110
+ return toProperName(pkg.name);
111
+ }
112
+ catch { }
113
+ }
114
+ return "App";
115
+ }
116
+ /**
117
+ * Extracts package name / applicationId from Gradle or AndroidManifest
118
+ */
119
+ async function getNativePackageName() {
120
+ const cwd = process.cwd();
121
+ const candidateFiles = [
122
+ path_1.default.join(cwd, "android", "app", "build.gradle"),
123
+ path_1.default.join(cwd, "android", "app", "build.gradle.kts"),
124
+ path_1.default.join(cwd, "android", "app", "src", "main", "AndroidManifest.xml"),
125
+ ];
126
+ for (const file of candidateFiles) {
127
+ if (await fs_extra_1.default.pathExists(file)) {
128
+ try {
129
+ const content = await fs_extra_1.default.readFile(file, "utf8");
130
+ const appIdMatch = content.match(/(?:applicationId|namespace)\s*=?\s*["']([a-zA-Z0-9_.]+)["']/);
131
+ if (appIdMatch && appIdMatch[1]) {
132
+ return appIdMatch[1];
133
+ }
134
+ const manifestMatch = content.match(/package=["']([a-zA-Z0-9_.]+)["']/);
135
+ if (manifestMatch && manifestMatch[1]) {
136
+ return manifestMatch[1];
137
+ }
138
+ }
139
+ catch { }
140
+ }
141
+ }
142
+ return null;
143
+ }
@@ -0,0 +1,11 @@
1
+ export interface KeystoreConfig {
2
+ keystorePath: string;
3
+ ksAlias: string;
4
+ ksPass: string;
5
+ keyPass: string;
6
+ }
7
+ export declare function updateEnvLocal(vars: Record<string, string>): Promise<void>;
8
+ export declare function printKeystoreWarning(keystorePath: string): void;
9
+ export declare function ensureOrPromptKeystore(requestedKeystore?: string, requestedAlias?: string, requestedStorePass?: string, requestedKeyPass?: string, appName?: string, outDir?: string): Promise<KeystoreConfig>;
10
+ export declare function stripExistingSignatures(archivePath: string): Promise<void>;
11
+ export declare function signArtifact(outputName: string, outDir: string, isAab: boolean, appName: string): Promise<void>;
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateEnvLocal = updateEnvLocal;
7
+ exports.printKeystoreWarning = printKeystoreWarning;
8
+ exports.ensureOrPromptKeystore = ensureOrPromptKeystore;
9
+ exports.stripExistingSignatures = stripExistingSignatures;
10
+ exports.signArtifact = signArtifact;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const execa_1 = require("execa");
13
+ const fs_extra_1 = __importDefault(require("fs-extra"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const promises_1 = __importDefault(require("node:readline/promises"));
16
+ const node_process_1 = require("node:process");
17
+ const node_crypto_1 = __importDefault(require("node:crypto"));
18
+ const project_js_1 = require("./project.js");
19
+ async function promptQuestion(rl, query, defaultValue = "") {
20
+ const displayQuery = defaultValue
21
+ ? `${query} [${defaultValue}]: `
22
+ : `${query}: `;
23
+ const answer = (await rl.question(displayQuery)).trim();
24
+ return answer || defaultValue;
25
+ }
26
+ async function updateEnvLocal(vars) {
27
+ const envLocalPath = path_1.default.join(process.cwd(), ".env.local");
28
+ let content = "";
29
+ if (await fs_extra_1.default.pathExists(envLocalPath)) {
30
+ content = await fs_extra_1.default.readFile(envLocalPath, "utf8");
31
+ }
32
+ for (const [key, value] of Object.entries(vars)) {
33
+ const regex = new RegExp(`^${key}=.*$`, "m");
34
+ if (regex.test(content)) {
35
+ content = content.replace(regex, `${key}=${value}`);
36
+ }
37
+ else {
38
+ content = content
39
+ ? `${content.trimEnd()}\n${key}=${value}\n`
40
+ : `${key}=${value}\n`;
41
+ }
42
+ process.env[key] = value;
43
+ }
44
+ await fs_extra_1.default.writeFile(envLocalPath, content, "utf8");
45
+ }
46
+ function printKeystoreWarning(keystorePath) {
47
+ console.log(chalk_1.default.yellow("\n========================================================"));
48
+ console.log(chalk_1.default.red("IMPORTANT KEYSTORE BACKUP WARNING"));
49
+ console.log(chalk_1.default.yellow(` Generated new release keystore at: ${keystorePath}`));
50
+ console.log(chalk_1.default.green(" Configuration saved to: .env.local"));
51
+ console.log(chalk_1.default.white(" PLEASE BACK UP THIS KEYSTORE FILE!"));
52
+ console.log(chalk_1.default.white(" Losing this keystore means you CANNOT update your app"));
53
+ console.log(chalk_1.default.white(" on the Google Play Store in the future."));
54
+ console.log(chalk_1.default.yellow("========================================================\n"));
55
+ }
56
+ async function ensureOrPromptKeystore(requestedKeystore, requestedAlias, requestedStorePass, requestedKeyPass, appName = "Mobile App", outDir = "build") {
57
+ let ksFileName = requestedKeystore || process.env.KEYSTORE_PATH;
58
+ let keystorePath = "";
59
+ if (ksFileName) {
60
+ const p1 = path_1.default.resolve(process.cwd(), ksFileName);
61
+ const p2 = path_1.default.resolve(outDir, ksFileName);
62
+ if (await fs_extra_1.default.pathExists(p1)) {
63
+ keystorePath = p1;
64
+ }
65
+ else if (await fs_extra_1.default.pathExists(p2)) {
66
+ keystorePath = p2;
67
+ }
68
+ }
69
+ else {
70
+ const candidates = [
71
+ path_1.default.join(outDir, "release.jks"),
72
+ path_1.default.join(process.cwd(), "release.jks"),
73
+ path_1.default.join(outDir, "release.keystore"),
74
+ path_1.default.join(process.cwd(), "release.keystore"),
75
+ path_1.default.join(process.cwd(), "android", "app", "release.keystore"),
76
+ path_1.default.join(process.cwd(), "android", "app", "release.jks"),
77
+ path_1.default.join(process.cwd(), "android", "app", "my-upload-key.keystore"),
78
+ ];
79
+ for (const cand of candidates) {
80
+ if (await fs_extra_1.default.pathExists(cand)) {
81
+ keystorePath = cand;
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ // If found on disk, return existing config
87
+ if (keystorePath && (await fs_extra_1.default.pathExists(keystorePath))) {
88
+ const ksAlias = requestedAlias || process.env.KEYSTORE_ALIAS || "release";
89
+ const ksPass = requestedStorePass || process.env.KEYSTORE_PASSWORD || "Password@1";
90
+ const keyPass = requestedKeyPass || process.env.KEY_PASSWORD || ksPass;
91
+ return { keystorePath, ksAlias, ksPass, keyPass };
92
+ }
93
+ // Keystore does NOT exist -> Interactively prompt or auto-generate
94
+ const targetKsRelative = requestedKeystore || path_1.default.join(outDir, "release.jks");
95
+ const targetKsPath = path_1.default.resolve(process.cwd(), targetKsRelative);
96
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(targetKsPath));
97
+ const isInteractive = Boolean(process.stdin.isTTY);
98
+ let chosenAlias = requestedAlias || process.env.KEYSTORE_ALIAS || "";
99
+ let chosenStorePass = requestedStorePass || process.env.KEYSTORE_PASSWORD || "";
100
+ let chosenKeyPass = requestedKeyPass || process.env.KEY_PASSWORD || "";
101
+ let chosenPath = targetKsPath;
102
+ const properName = (0, project_js_1.toProperName)(appName || "Mobile App");
103
+ let cn = properName;
104
+ let ou = "Mobile App";
105
+ let org = properName;
106
+ let city = "Nairobi";
107
+ let state = "Nairobi";
108
+ let country = "KE";
109
+ if (isInteractive) {
110
+ console.log(chalk_1.default.yellow(`\nKeystore not found at '${targetKsRelative}'.`));
111
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
112
+ try {
113
+ const shouldCreate = await promptQuestion(rl, "Would you like to interactively create a new keystore now? (Y/n)", "Y");
114
+ if (shouldCreate.toLowerCase().startsWith("n")) {
115
+ console.error(chalk_1.default.red("Keystore creation cancelled. Cannot sign without a keystore."));
116
+ process.exit(1);
117
+ }
118
+ console.log(chalk_1.default.cyan("\n--- Keystore Configuration ---"));
119
+ const p = await promptQuestion(rl, "Keystore file path", path_1.default.relative(process.cwd(), targetKsPath).replace(/\\/g, "/"));
120
+ chosenPath = path_1.default.resolve(process.cwd(), p);
121
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(chosenPath));
122
+ const defaultAlias = chosenAlias || node_crypto_1.default.randomBytes(16).toString("hex");
123
+ chosenAlias = await promptQuestion(rl, "Key alias", defaultAlias);
124
+ const defaultPass = chosenStorePass || node_crypto_1.default.randomBytes(16).toString("hex");
125
+ chosenStorePass = await promptQuestion(rl, "Keystore password", defaultPass);
126
+ // In modern PKCS12 keystores, key password must match store password
127
+ chosenKeyPass = chosenStorePass;
128
+ console.log(chalk_1.default.cyan("\n--- Certificate Details (Press Enter to accept defaults) ---"));
129
+ cn = await promptQuestion(rl, "Common Name (CN)", cn);
130
+ org = await promptQuestion(rl, "Organization (O)", org);
131
+ ou = await promptQuestion(rl, "Organizational Unit (OU)", ou);
132
+ city = await promptQuestion(rl, "Locality (L)", city);
133
+ state = await promptQuestion(rl, "State (ST)", state);
134
+ country = await promptQuestion(rl, "Country Code (C, 2-letter)", country);
135
+ }
136
+ finally {
137
+ rl.close();
138
+ }
139
+ }
140
+ else {
141
+ // Non-interactive / CI fallback
142
+ console.log(chalk_1.default.yellow(`Keystore not found. Generating new keystore at ${targetKsPath}...`));
143
+ if (!chosenAlias)
144
+ chosenAlias = node_crypto_1.default.randomBytes(16).toString("hex");
145
+ if (!chosenStorePass)
146
+ chosenStorePass = node_crypto_1.default.randomBytes(16).toString("hex");
147
+ chosenKeyPass = chosenStorePass;
148
+ }
149
+ // Generate the keystore with keytool
150
+ console.log(chalk_1.default.cyan(`\nGenerating keystore with keytool at ${chosenPath}...`));
151
+ await (0, execa_1.execa)("keytool", [
152
+ "-genkeypair",
153
+ "-v",
154
+ "-keystore",
155
+ chosenPath,
156
+ "-alias",
157
+ chosenAlias,
158
+ "-keyalg",
159
+ "RSA",
160
+ "-keysize",
161
+ "2048",
162
+ "-validity",
163
+ "10000",
164
+ "-storepass",
165
+ chosenStorePass,
166
+ "-keypass",
167
+ chosenKeyPass,
168
+ "-dname",
169
+ `CN=${cn}, OU=${ou}, O=${org}, L=${city}, ST=${state}, C=${country}`,
170
+ ]);
171
+ // Save generated credentials to .env.local
172
+ const relPath = path_1.default.relative(process.cwd(), chosenPath).replace(/\\/g, "/");
173
+ await updateEnvLocal({
174
+ KEYSTORE_PATH: relPath,
175
+ KEYSTORE_ALIAS: chosenAlias,
176
+ KEYSTORE_PASSWORD: chosenStorePass,
177
+ KEY_PASSWORD: chosenKeyPass,
178
+ });
179
+ printKeystoreWarning(chosenPath);
180
+ return {
181
+ keystorePath: chosenPath,
182
+ ksAlias: chosenAlias,
183
+ ksPass: chosenStorePass,
184
+ keyPass: chosenKeyPass,
185
+ };
186
+ }
187
+ async function stripExistingSignatures(archivePath) {
188
+ console.log(chalk_1.default.gray("Stripping existing signatures from bundle to ensure single certificate chain..."));
189
+ if (process.platform === "win32") {
190
+ const escapedPath = archivePath.replace(/'/g, "''");
191
+ await (0, execa_1.execa)("powershell", [
192
+ "-Command",
193
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem; " +
194
+ "$zip = [System.IO.Compression.ZipFile]::Open('" +
195
+ escapedPath +
196
+ "', 'Update'); " +
197
+ "$entries = @($zip.Entries | Where-Object { $_.FullName -match '^META-INF/.*\\.(RSA|DSA|EC|SF)$' }); " +
198
+ "foreach ($e in $entries) { $e.Delete() }; " +
199
+ "$zip.Dispose()",
200
+ ]);
201
+ }
202
+ else {
203
+ try {
204
+ await (0, execa_1.execa)("zip", [
205
+ "-d",
206
+ archivePath,
207
+ "META-INF/*.SF",
208
+ "META-INF/*.RSA",
209
+ "META-INF/*.DSA",
210
+ "META-INF/*.EC",
211
+ ]);
212
+ }
213
+ catch {
214
+ // Ignored if no matching signature files found
215
+ }
216
+ }
217
+ // Clean up any detached idsig file for APKs
218
+ const idsigPath = `${archivePath}.idsig`;
219
+ if (await fs_extra_1.default.pathExists(idsigPath)) {
220
+ await fs_extra_1.default.remove(idsigPath);
221
+ }
222
+ }
223
+ async function signArtifact(outputName, outDir, isAab, appName) {
224
+ const { keystorePath, ksAlias, ksPass, keyPass } = await ensureOrPromptKeystore(undefined, undefined, undefined, undefined, appName, outDir);
225
+ const targetPath = path_1.default.join(outDir, outputName);
226
+ if (isAab) {
227
+ await stripExistingSignatures(targetPath);
228
+ console.log(chalk_1.default.cyan(`Signing ${outputName} with jarsigner...`));
229
+ try {
230
+ await (0, execa_1.execa)("jarsigner", [
231
+ "-sigalg",
232
+ "SHA256withRSA",
233
+ "-digestalg",
234
+ "SHA-256",
235
+ "-keystore",
236
+ keystorePath,
237
+ "-storepass",
238
+ ksPass,
239
+ "-keypass",
240
+ keyPass,
241
+ targetPath,
242
+ ksAlias,
243
+ ]);
244
+ console.log(chalk_1.default.green("Successfully signed AAB."));
245
+ }
246
+ catch (err) {
247
+ console.error(chalk_1.default.red(`jarsigner failed:\n${err.stderr || err.message}`));
248
+ }
249
+ }
250
+ else {
251
+ console.log(chalk_1.default.cyan(`Signing ${outputName} with apksigner...`));
252
+ let apksignerJar = "";
253
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || "C:/Android";
254
+ const buildToolsDir = path_1.default.join(androidHome, "build-tools");
255
+ if (await fs_extra_1.default.pathExists(buildToolsDir)) {
256
+ const versions = await fs_extra_1.default.readdir(buildToolsDir);
257
+ if (versions.length > 0) {
258
+ versions.sort((a, b) => b.localeCompare(a));
259
+ apksignerJar = path_1.default.join(buildToolsDir, versions[0], "lib", "apksigner.jar");
260
+ }
261
+ }
262
+ if (await fs_extra_1.default.pathExists(apksignerJar)) {
263
+ try {
264
+ await (0, execa_1.execa)("java", [
265
+ "-jar",
266
+ apksignerJar,
267
+ "sign",
268
+ "--ks",
269
+ keystorePath,
270
+ "--ks-key-alias",
271
+ ksAlias,
272
+ "--ks-pass",
273
+ `pass:${ksPass}`,
274
+ "--key-pass",
275
+ `pass:${keyPass}`,
276
+ "--v4-signing-enabled",
277
+ "false",
278
+ targetPath,
279
+ ]);
280
+ console.log(chalk_1.default.green("Successfully signed APK."));
281
+ }
282
+ catch (err) {
283
+ console.log(chalk_1.default.yellow("Warning: apksigner failed."));
284
+ console.error(chalk_1.default.red(err.stderr || err.message));
285
+ }
286
+ }
287
+ else {
288
+ console.log(chalk_1.default.yellow("apksigner not found. Falling back to jarsigner for APK..."));
289
+ try {
290
+ await (0, execa_1.execa)("jarsigner", [
291
+ "-sigalg",
292
+ "SHA256withRSA",
293
+ "-digestalg",
294
+ "SHA-256",
295
+ "-keystore",
296
+ keystorePath,
297
+ "-storepass",
298
+ ksPass,
299
+ "-keypass",
300
+ keyPass,
301
+ targetPath,
302
+ ksAlias,
303
+ ]);
304
+ console.log(chalk_1.default.green("Successfully signed APK with jarsigner!"));
305
+ }
306
+ catch (err) {
307
+ console.error(chalk_1.default.red(`jarsigner failed:\n${err.stderr || err.message}`));
308
+ }
309
+ }
310
+ }
311
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "mobile-app-builder",
3
+ "version": "0.0.1",
4
+ "description": "Cross-platform CLI builder and signer for mobile applications (Expo, React Native, and Android projects)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mobile-builder": "./dist/cli.js",
9
+ "mobile-app-builder": "./dist/cli.js",
10
+ "app-builder": "./dist/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build",
15
+ "dev": "tsc --watch",
16
+ "format": "prettier --write ."
17
+ },
18
+ "keywords": [
19
+ "mobile",
20
+ "mobile-app",
21
+ "react-native",
22
+ "expo",
23
+ "android",
24
+ "builder",
25
+ "cli",
26
+ "signing",
27
+ "keystore",
28
+ "apksigner",
29
+ "jarsigner",
30
+ "aab",
31
+ "apk"
32
+ ],
33
+ "author": "George Chitechi",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "chalk": "^6.0.0",
37
+ "commander": "^15.0.0",
38
+ "dotenv": "^17.4.2",
39
+ "execa": "^10.0.1",
40
+ "fs-extra": "^11.4.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/fs-extra": "^11.0.4",
44
+ "@types/node": "^22.20.3",
45
+ "prettier": "^3.9.7",
46
+ "typescript": "^7.0.2"
47
+ },
48
+ "engines": {
49
+ "node": ">=22.0.0"
50
+ },
51
+ "files": [
52
+ "dist"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/georgechitechi/mobile-app-builder.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/georgechitechi/mobile-app-builder/issues"
60
+ },
61
+ "homepage": "https://github.com/georgechitechi/mobile-app-builder#readme"
62
+ }