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.
- package/README.md +191 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +113 -0
- package/dist/commands/buildAndroid.d.ts +15 -0
- package/dist/commands/buildAndroid.js +160 -0
- package/dist/commands/buildIos.d.ts +1 -0
- package/dist/commands/buildIos.js +11 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +28 -0
- package/dist/commands/sign.d.ts +10 -0
- package/dist/commands/sign.js +185 -0
- package/dist/commands/upload.d.ts +10 -0
- package/dist/commands/upload.js +114 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +175 -0
- package/dist/utils/dependencies.d.ts +13 -0
- package/dist/utils/dependencies.js +388 -0
- package/dist/utils/github.d.ts +5 -0
- package/dist/utils/github.js +216 -0
- package/dist/utils/gradle.d.ts +4 -0
- package/dist/utils/gradle.js +76 -0
- package/dist/utils/project.d.ts +22 -0
- package/dist/utils/project.js +143 -0
- package/dist/utils/signing.d.ts +11 -0
- package/dist/utils/signing.js +311 -0
- package/package.json +62 -0
|
@@ -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
|
+
}
|