pakstr 0.8.0 → 0.8.2

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.
@@ -4,20 +4,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.patchAppName = patchAppName;
7
- exports.patchPackageName = patchPackageName;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
10
9
  function patchAppName(androidRoot, appName) {
11
10
  const stringsPath = path_1.default.join(androidRoot, "app", "src", "main", "res", "values", "strings.xml");
12
11
  let xml = fs_1.default.readFileSync(stringsPath, "utf8");
13
- xml = xml.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">${appName}</string>`);
12
+ const appNamePattern = /<string\s+name="app_name"[^>]*>[\s\S]*?<\/string>/g;
13
+ const matches = [...xml.matchAll(appNamePattern)];
14
+ if (matches.length !== 1) {
15
+ throw new Error(`Expected exactly one app_name string resource, found ${matches.length}`);
16
+ }
17
+ appNamePattern.lastIndex = 0;
18
+ xml = xml.replace(appNamePattern, `<string name="app_name">${escapeXmlText(appName)}</string>`);
14
19
  fs_1.default.writeFileSync(stringsPath, xml);
15
20
  console.log("✅ App name:", appName);
16
21
  }
17
- function patchPackageName(androidRoot, packageName) {
18
- const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
19
- let gradle = fs_1.default.readFileSync(gradlePath, "utf8");
20
- gradle = gradle.replace(/applicationId\s*=\s*"[^"]+"/, `applicationId = "${packageName}"`);
21
- fs_1.default.writeFileSync(gradlePath, gradle);
22
- console.log(" ApplicationId:", packageName);
22
+ function escapeXmlText(value) {
23
+ return value
24
+ .replaceAll("&", "&amp;")
25
+ .replaceAll("<", "&lt;")
26
+ .replaceAll(">", "&gt;")
27
+ .replaceAll('"', "&quot;")
28
+ .replaceAll("'", "&apos;");
23
29
  }
@@ -0,0 +1,95 @@
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.verifyGeneratedAndroidProject = verifyGeneratedAndroidProject;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const gradle_1 = require("./gradle");
10
+ const INTERNAL_NAMESPACE = "com.pakstr.app";
11
+ const INTERNAL_PACKAGE_PATTERN = /^com\.pakstr\.app(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
12
+ const SOURCE_EXTENSIONS = new Set([".kt", ".java"]);
13
+ function verifyGeneratedAndroidProject(androidRoot, expected) {
14
+ const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
15
+ const gradle = (0, gradle_1.readAndroidGradleMetadata)(gradlePath);
16
+ assertEqual("applicationId", gradle.applicationId, expected.applicationId);
17
+ assertEqual("versionCode", gradle.versionCode, expected.versionCode);
18
+ assertEqual("versionName", gradle.versionName, expected.versionName);
19
+ assertEqual("namespace", gradle.namespace, INTERNAL_NAMESPACE);
20
+ const javaRoot = path_1.default.join(androidRoot, "app", "src", "main", "java");
21
+ const sourceRoot = path_1.default.join(javaRoot, "com", "pakstr", "app");
22
+ if (!fs_1.default.existsSync(sourceRoot) || !fs_1.default.lstatSync(sourceRoot).isDirectory()) {
23
+ throw new Error(`Expected Pakstr source root is missing: ${sourceRoot}`);
24
+ }
25
+ const sourceFiles = collectSourceFiles(javaRoot);
26
+ if (!sourceFiles.length) {
27
+ throw new Error(`No Kotlin or Java sources found under ${sourceRoot}`);
28
+ }
29
+ for (const sourceFile of sourceFiles) {
30
+ const declaration = readPackageDeclaration(sourceFile);
31
+ if (!INTERNAL_PACKAGE_PATTERN.test(declaration)) {
32
+ throw new Error(`Source package must remain ${INTERNAL_NAMESPACE} or a subpackage: ${sourceFile}`);
33
+ }
34
+ if (expected.applicationId !== INTERNAL_NAMESPACE &&
35
+ (declaration === expected.applicationId ||
36
+ declaration.startsWith(`${expected.applicationId}.`))) {
37
+ throw new Error(`Customer applicationId must not be used as a source package: ${sourceFile}`);
38
+ }
39
+ }
40
+ const stringsPath = path_1.default.join(androidRoot, "app", "src", "main", "res", "values", "strings.xml");
41
+ const appLabel = readSingleAppLabel(stringsPath);
42
+ assertEqual("app label", decodeXmlText(appLabel), expected.appLabel);
43
+ return {
44
+ applicationId: gradle.applicationId,
45
+ namespace: INTERNAL_NAMESPACE,
46
+ versionCode: gradle.versionCode,
47
+ versionName: gradle.versionName,
48
+ appLabel: expected.appLabel,
49
+ sourceFilesChecked: sourceFiles.length,
50
+ };
51
+ }
52
+ function collectSourceFiles(root) {
53
+ const result = [];
54
+ for (const entry of fs_1.default.readdirSync(root, { withFileTypes: true })) {
55
+ const entryPath = path_1.default.join(root, entry.name);
56
+ if (entry.isDirectory()) {
57
+ result.push(...collectSourceFiles(entryPath));
58
+ }
59
+ else if (SOURCE_EXTENSIONS.has(path_1.default.extname(entry.name))) {
60
+ result.push(entryPath);
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ function readPackageDeclaration(sourceFile) {
66
+ const content = fs_1.default.readFileSync(sourceFile, "utf8");
67
+ const matches = [...content.matchAll(/^\s*package\s+([A-Za-z_][\w.]*)/gm)];
68
+ if (matches.length !== 1 || !matches[0][1]) {
69
+ throw new Error(`Expected exactly one package declaration in source file: ${sourceFile}`);
70
+ }
71
+ return matches[0][1];
72
+ }
73
+ function readSingleAppLabel(stringsPath) {
74
+ const xml = fs_1.default.readFileSync(stringsPath, "utf8");
75
+ const matches = [
76
+ ...xml.matchAll(/<string\s+name="app_name"[^>]*>([\s\S]*?)<\/string>/g),
77
+ ];
78
+ if (matches.length !== 1 || matches[0][1] === undefined) {
79
+ throw new Error(`Expected exactly one app_name string resource, found ${matches.length}`);
80
+ }
81
+ return matches[0][1];
82
+ }
83
+ function decodeXmlText(value) {
84
+ return value
85
+ .replaceAll("&lt;", "<")
86
+ .replaceAll("&gt;", ">")
87
+ .replaceAll("&quot;", '"')
88
+ .replaceAll("&apos;", "'")
89
+ .replaceAll("&amp;", "&");
90
+ }
91
+ function assertEqual(field, actual, expected) {
92
+ if (actual !== expected) {
93
+ throw new Error(`Generated Android project ${field} mismatch: expected ${expected}, received ${actual}`);
94
+ }
95
+ }
@@ -4,14 +4,69 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.patchGradle = patchGradle;
7
+ exports.readAndroidGradleMetadata = readAndroidGradleMetadata;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
10
+ const ASSIGNMENTS = {
11
+ applicationId: /\bapplicationId\s*=\s*"([^"]*)"/g,
12
+ namespace: /\bnamespace\s*=\s*"([^"]*)"/g,
13
+ versionCode: /\bversionCode\s*=\s*(\d+)/g,
14
+ versionName: /\bversionName\s*=\s*"([^"]*)"/g,
15
+ };
9
16
  function patchGradle(androidRoot, manifest) {
10
- const gradlePath = path_1.default.join(androidRoot, "app/build.gradle.kts");
11
- let content = fs_1.default.readFileSync(gradlePath, "utf-8");
12
- content = content.replace(/applicationId\s+".*"/, `applicationId "${manifest.appId}"`);
13
- content = content.replace(/versionCode\s+\d+/, `versionCode ${manifest.versionCode}`);
14
- content = content.replace(/versionName\s+".*"/, `versionName "${manifest.versionName}"`);
17
+ const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
18
+ let content = fs_1.default.readFileSync(gradlePath, "utf8");
19
+ content = replaceSingleAssignment(content, "applicationId", ASSIGNMENTS.applicationId, `applicationId = ${quoteKotlinString(manifest.appId)}`);
20
+ content = replaceSingleAssignment(content, "versionCode", ASSIGNMENTS.versionCode, `versionCode = ${manifest.versionCode}`);
21
+ content = replaceSingleAssignment(content, "versionName", ASSIGNMENTS.versionName, `versionName = ${quoteKotlinString(manifest.versionName)}`);
15
22
  fs_1.default.writeFileSync(gradlePath, content);
16
- console.log("⚙️ Gradle patched");
23
+ const metadata = readAndroidGradleMetadata(gradlePath);
24
+ if (metadata.applicationId !== manifest.appId) {
25
+ throw new Error("Generated Gradle applicationId does not match manifest.appId");
26
+ }
27
+ if (metadata.versionCode !== manifest.versionCode) {
28
+ throw new Error("Generated Gradle versionCode does not match manifest.versionCode");
29
+ }
30
+ if (metadata.versionName !== manifest.versionName) {
31
+ throw new Error("Generated Gradle versionName does not match manifest.versionName");
32
+ }
33
+ console.log("⚙️ Gradle identity and version patched");
34
+ return metadata;
35
+ }
36
+ function readAndroidGradleMetadata(gradlePath) {
37
+ const content = fs_1.default.readFileSync(gradlePath, "utf8");
38
+ return {
39
+ applicationId: readSingleAssignment(content, "applicationId", ASSIGNMENTS.applicationId),
40
+ namespace: readSingleAssignment(content, "namespace", ASSIGNMENTS.namespace),
41
+ versionCode: Number(readSingleAssignment(content, "versionCode", ASSIGNMENTS.versionCode)),
42
+ versionName: readSingleAssignment(content, "versionName", ASSIGNMENTS.versionName),
43
+ };
44
+ }
45
+ function replaceSingleAssignment(content, field, pattern, replacement) {
46
+ requireSingleMatch(content, field, pattern);
47
+ pattern.lastIndex = 0;
48
+ return content.replace(pattern, replacement);
49
+ }
50
+ function readSingleAssignment(content, field, pattern) {
51
+ requireSingleMatch(content, field, pattern);
52
+ pattern.lastIndex = 0;
53
+ const match = pattern.exec(content);
54
+ if (!match?.[1]) {
55
+ throw new Error(`Unable to read Gradle ${field} assignment`);
56
+ }
57
+ return match[1];
58
+ }
59
+ function requireSingleMatch(content, field, pattern) {
60
+ pattern.lastIndex = 0;
61
+ const matches = [...content.matchAll(pattern)];
62
+ pattern.lastIndex = 0;
63
+ if (matches.length !== 1) {
64
+ throw new Error(`Expected exactly one Kotlin DSL ${field} assignment, found ${matches.length}`);
65
+ }
66
+ }
67
+ function quoteKotlinString(value) {
68
+ if (/[\r\n"\\]/.test(value)) {
69
+ throw new Error("Gradle string values must not contain line breaks, quotes, or backslashes");
70
+ }
71
+ return `"${value}"`;
17
72
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.verifyReleaseApk = verifyReleaseApk;
4
4
  exports.parseApkSignerFingerprints = parseApkSignerFingerprints;
5
5
  exports.parseKeystoreFingerprints = parseKeystoreFingerprints;
6
+ exports.parseAaptBadging = parseAaptBadging;
6
7
  const crypto_1 = require("crypto");
7
8
  const releaseSigning_1 = require("../core/releaseSigning");
8
9
  function verifyReleaseApk(input) {
@@ -23,40 +24,42 @@ function verifyReleaseApk(input) {
23
24
  "-rfc",
24
25
  "-keystore",
25
26
  input.toolKeystorePath ?? input.signing.keystorePath,
26
- `-storepass:env`,
27
+ "-storepass:env",
27
28
  releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword,
28
29
  ], keytoolEnv);
29
30
  const keystoreFingerprints = parseKeystoreFingerprints(keytoolResult.stdout);
30
- const signerFingerprint = signerFingerprints[0];
31
- const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprint);
31
+ const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprints[0]);
32
32
  if (matchingEntries.length !== 1) {
33
33
  throw new Error(`Release APK signer must match exactly one certificate in the supplied keystore; found ${matchingEntries.length}`);
34
34
  }
35
- const identityResult = input.executor.run("apkanalyzer", [
36
- "manifest",
37
- "application-id",
38
- input.artifactPath,
39
- ]);
40
- const applicationIds = identityResult.stdout
41
- .split(/\r?\n/)
42
- .map(value => value.trim())
43
- .filter(Boolean);
44
- if (applicationIds.length !== 1) {
45
- throw new Error("Unable to derive one unambiguous Application ID from release APK");
46
- }
47
- const actualApplicationId = applicationIds[0];
48
- if (actualApplicationId !== input.expectedApplicationId) {
49
- throw new Error(`Release APK Application ID mismatch: expected ${input.expectedApplicationId}, received ${actualApplicationId}`);
50
- }
35
+ const analyzerMetadata = {
36
+ applicationId: readAnalyzerValue(input, "application-id"),
37
+ versionCode: parseVersionCode(readAnalyzerValue(input, "version-code")),
38
+ versionName: readAnalyzerValue(input, "version-name"),
39
+ };
40
+ const badging = parseAaptBadging(input.executor.run("aapt", ["dump", "badging", input.artifactPath]).stdout);
41
+ assertMetadataField("Application ID", analyzerMetadata.applicationId, input.expectedMetadata.applicationId);
42
+ assertMetadataField("versionCode", analyzerMetadata.versionCode, input.expectedMetadata.versionCode);
43
+ assertMetadataField("versionName", analyzerMetadata.versionName, input.expectedMetadata.versionName);
44
+ assertMetadataField("aapt Application ID", badging.applicationId, analyzerMetadata.applicationId);
45
+ assertMetadataField("aapt versionCode", badging.versionCode, analyzerMetadata.versionCode);
46
+ assertMetadataField("aapt versionName", badging.versionName, analyzerMetadata.versionName);
47
+ assertMetadataField("app label", badging.appLabel, input.expectedMetadata.appLabel);
51
48
  return {
52
49
  signatureVerified: true,
53
50
  signerMatched: true,
54
- expectedApplicationId: input.expectedApplicationId,
55
- actualApplicationId,
51
+ expectedMetadata: input.expectedMetadata,
52
+ actualMetadata: {
53
+ applicationId: analyzerMetadata.applicationId,
54
+ versionCode: analyzerMetadata.versionCode,
55
+ versionName: analyzerMetadata.versionName,
56
+ appLabel: badging.appLabel,
57
+ },
56
58
  tools: {
57
59
  signature: "apksigner",
58
60
  signer: "keytool",
59
- identity: "apkanalyzer",
61
+ manifest: "apkanalyzer",
62
+ badging: "aapt",
60
63
  },
61
64
  };
62
65
  }
@@ -78,6 +81,84 @@ function parseKeystoreFingerprints(output) {
78
81
  return (0, crypto_1.createHash)("sha256").update(certificate.raw).digest("hex");
79
82
  });
80
83
  }
84
+ function parseAaptBadging(output) {
85
+ const lines = output.split(/\r?\n/);
86
+ const packageLines = lines.filter(line => line.startsWith("package: "));
87
+ const labelLines = lines.filter(line => line.startsWith("application-label:"));
88
+ if (packageLines.length !== 1) {
89
+ throw new Error(`Expected exactly one aapt package line, found ${packageLines.length}`);
90
+ }
91
+ if (labelLines.length !== 1) {
92
+ throw new Error(`Expected exactly one default aapt application label, found ${labelLines.length}`);
93
+ }
94
+ const packageLine = packageLines[0];
95
+ const applicationId = readAaptAttribute(packageLine, "name");
96
+ const versionCode = parseVersionCode(readAaptAttribute(packageLine, "versionCode"));
97
+ const versionName = readAaptAttribute(packageLine, "versionName");
98
+ const appLabel = parseQuotedAaptValue(labelLines[0], "application-label");
99
+ return {
100
+ applicationId,
101
+ versionCode,
102
+ versionName,
103
+ appLabel,
104
+ };
105
+ }
106
+ function readAnalyzerValue(input, verb) {
107
+ const result = input.executor.run("apkanalyzer", [
108
+ "manifest",
109
+ verb,
110
+ input.artifactPath,
111
+ ]);
112
+ const values = result.stdout
113
+ .split(/\r?\n/)
114
+ .map(value => value.trim())
115
+ .filter(Boolean);
116
+ if (values.length !== 1) {
117
+ throw new Error(`Unable to derive one unambiguous ${verb} from release APK`);
118
+ }
119
+ return values[0];
120
+ }
121
+ function parseQuotedAaptValue(line, field) {
122
+ const pattern = new RegExp(`^${field}:'((?:\\\\.|[^'])*)'$`);
123
+ const match = pattern.exec(line);
124
+ if (!match || match[1] === undefined) {
125
+ throw new Error(`Unable to derive one unambiguous aapt ${field}`);
126
+ }
127
+ return decodeAaptEscapes(match[1]);
128
+ }
129
+ function readAaptAttribute(line, name) {
130
+ const pattern = new RegExp(`${name}='((?:\\\\.|[^'])*)'`);
131
+ const match = pattern.exec(line);
132
+ if (!match || match[1] === undefined) {
133
+ throw new Error(`Unable to derive one unambiguous aapt ${name}`);
134
+ }
135
+ return decodeAaptEscapes(match[1]);
136
+ }
137
+ function decodeAaptEscapes(value) {
138
+ return value.replace(/\\(.)/gs, (_match, escaped) => {
139
+ switch (escaped) {
140
+ case "n":
141
+ return "\n";
142
+ case "r":
143
+ return "\r";
144
+ case "t":
145
+ return "\t";
146
+ default:
147
+ return escaped;
148
+ }
149
+ });
150
+ }
151
+ function parseVersionCode(value) {
152
+ if (!/^\d+$/.test(value)) {
153
+ throw new Error(`APK versionCode is not an integer: ${value}`);
154
+ }
155
+ return Number(value);
156
+ }
157
+ function assertMetadataField(field, actual, expected) {
158
+ if (actual !== expected) {
159
+ throw new Error(`Release APK ${field} mismatch: expected ${expected}, received ${actual}`);
160
+ }
161
+ }
81
162
  function normalizeFingerprint(value) {
82
163
  return value.replace(/[^0-9a-fA-F]/g, "").toLowerCase();
83
164
  }
@@ -7,6 +7,7 @@ exports.buildCommand = buildCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const child_process_1 = require("child_process");
10
+ const generatedProjectVerification_1 = require("../android/generatedProjectVerification");
10
11
  const gradle_1 = require("../android/gradle");
11
12
  const branding_1 = require("../android/branding");
12
13
  const icon_1 = require("../android/icon");
@@ -14,7 +15,6 @@ const permissions_1 = require("../android/permissions");
14
15
  const releaseWorkspace_1 = require("../android/releaseWorkspace");
15
16
  const splash_1 = require("../android/splash");
16
17
  const template_1 = require("../android/template");
17
- const version_1 = require("../android/version");
18
18
  const buildContext_1 = require("../core/buildContext");
19
19
  const config_1 = require("../core/config");
20
20
  const releaseSigning_1 = require("../core/releaseSigning");
@@ -26,7 +26,7 @@ async function buildCommand(args) {
26
26
  const config = (0, config_1.loadConfig)(process.cwd());
27
27
  const manifest = ctx.manifest;
28
28
  const mode = ctx.mode;
29
- const appName = manifest?.app?.name || manifest?.appName || "Pakstr App";
29
+ const appName = manifest.appName;
30
30
  const finalPath = path_1.default.resolve(ctx.out);
31
31
  console.log("\n🚀 Pakstr CLI");
32
32
  console.log("📦 App:", appName);
@@ -60,7 +60,12 @@ async function buildCommand(args) {
60
60
  const result = await (0, createRunner_1.createRunner)(config).build({
61
61
  androidRoot,
62
62
  mode,
63
- expectedApplicationId: manifest.appId,
63
+ expectedMetadata: {
64
+ applicationId: manifest.appId,
65
+ versionCode: manifest.versionCode,
66
+ versionName: manifest.versionName,
67
+ appLabel: appName,
68
+ },
64
69
  signing,
65
70
  });
66
71
  if (mode === "release" && !result.verification) {
@@ -115,8 +120,6 @@ async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
115
120
  (0, runtimeConfig_1.generateRuntimeConfig)(assetsTarget, manifest);
116
121
  (0, gradle_1.patchGradle)(androidRoot, manifest);
117
122
  (0, branding_1.patchAppName)(androidRoot, appName);
118
- (0, branding_1.patchPackageName)(androidRoot, manifest.appId);
119
- (0, version_1.patchVersion)(androidRoot, manifest.versionCode, manifest.versionName);
120
123
  if (manifest.ui?.icon) {
121
124
  await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF");
122
125
  }
@@ -124,6 +127,12 @@ async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
124
127
  await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF");
125
128
  }
126
129
  (0, permissions_1.patchPermissions)(androidRoot, manifest);
130
+ (0, generatedProjectVerification_1.verifyGeneratedAndroidProject)(androidRoot, {
131
+ applicationId: manifest.appId,
132
+ versionCode: manifest.versionCode,
133
+ versionName: manifest.versionName,
134
+ appLabel: appName,
135
+ });
127
136
  }
128
137
  function validateOutputParent(finalPath) {
129
138
  const parent = path_1.default.dirname(finalPath);
@@ -28,10 +28,19 @@ function validateManifest(m) {
28
28
  }
29
29
  if (!m.appName)
30
30
  throw new Error("❌ appName is required");
31
+ if (/[\r\n]/.test(m.appName)) {
32
+ throw new Error("❌ appName must not contain line breaks");
33
+ }
31
34
  if (!m.versionName)
32
35
  throw new Error("❌ versionName is required");
36
+ if (/[\r\n"\\]/.test(m.versionName)) {
37
+ throw new Error("❌ versionName must not contain line breaks, quotes, or backslashes");
38
+ }
33
39
  if (m.versionCode === undefined)
34
40
  throw new Error("❌ versionCode is required");
41
+ if (!Number.isInteger(m.versionCode) || m.versionCode <= 0) {
42
+ throw new Error("❌ versionCode must be a positive integer");
43
+ }
35
44
  if (m.ui?.splash && !m.ui.splash.image) {
36
45
  throw new Error("❌ splash.image is required");
37
46
  }
@@ -106,7 +106,7 @@ class DockerGradleRunner {
106
106
  });
107
107
  const verification = (0, releaseVerification_1.verifyReleaseApk)({
108
108
  artifactPath: CONTAINER_RELEASE_APK,
109
- expectedApplicationId: request.expectedApplicationId,
109
+ expectedMetadata: request.expectedMetadata,
110
110
  signing,
111
111
  toolKeystorePath: CONTAINER_KEYSTORE_PATH,
112
112
  executor: this.createVerificationExecutor(storage, signing),
@@ -37,13 +37,26 @@ class LocalGradleRunner {
37
37
  streamOutput: true,
38
38
  });
39
39
  const artifactPath = requireApk(androidRoot, "release");
40
- const executor = createLocalVerificationExecutor(signing);
41
- const verification = (0, releaseVerification_1.verifyReleaseApk)({
42
- artifactPath,
43
- expectedApplicationId: request.expectedApplicationId,
44
- signing,
45
- executor,
46
- });
40
+ let verification;
41
+ try {
42
+ const executor = createLocalVerificationExecutor(signing);
43
+ verification = (0, releaseVerification_1.verifyReleaseApk)({
44
+ artifactPath,
45
+ expectedMetadata: request.expectedMetadata,
46
+ signing,
47
+ executor,
48
+ });
49
+ }
50
+ catch (error) {
51
+ if (error instanceof Error &&
52
+ error.message.startsWith("Required Android verification tool not found")) {
53
+ console.warn("⚠️ Local release verification skipped: Android verification tools are missing.");
54
+ console.warn("ℹ️ Production releases should use CI Docker verification.");
55
+ }
56
+ else {
57
+ throw error;
58
+ }
59
+ }
47
60
  return { artifactPath, verification };
48
61
  }
49
62
  }
@@ -67,9 +80,18 @@ function resolveTool(tool, androidHome) {
67
80
  if (tool === "keytool")
68
81
  return "keytool";
69
82
  if (tool === "apkanalyzer") {
70
- const analyzer = path_1.default.join(androidHome, "cmdline-tools", "latest", "bin", "apkanalyzer");
71
- if (!fs_1.default.existsSync(analyzer)) {
72
- throw new Error(`Required Android verification tool not found: ${analyzer}`);
83
+ const cmdlineTools = path_1.default.join(androidHome, "cmdline-tools");
84
+ if (!fs_1.default.existsSync(cmdlineTools)) {
85
+ throw new Error(`Android cmdline-tools directory not found: ${cmdlineTools}`);
86
+ }
87
+ const versions = fs_1.default
88
+ .readdirSync(cmdlineTools)
89
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
90
+ const analyzer = versions
91
+ .map(version => path_1.default.join(cmdlineTools, version, "bin", "apkanalyzer"))
92
+ .find(candidate => fs_1.default.existsSync(candidate));
93
+ if (!analyzer) {
94
+ throw new Error(`Required Android verification tool not found under ${cmdlineTools}`);
73
95
  }
74
96
  return analyzer;
75
97
  }
@@ -80,13 +102,14 @@ function resolveTool(tool, androidHome) {
80
102
  const versions = fs_1.default
81
103
  .readdirSync(buildToolsDir)
82
104
  .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
83
- const signer = versions
84
- .map(version => path_1.default.join(buildToolsDir, version, "apksigner"))
105
+ const executable = tool === "aapt" ? "aapt" : "apksigner";
106
+ const resolved = versions
107
+ .map(version => path_1.default.join(buildToolsDir, version, executable))
85
108
  .find(candidate => fs_1.default.existsSync(candidate));
86
- if (!signer) {
87
- throw new Error(`Required Android verification tool apksigner not found under ${buildToolsDir}`);
109
+ if (!resolved) {
110
+ throw new Error(`Required Android verification tool ${executable} not found under ${buildToolsDir}`);
88
111
  }
89
- return signer;
112
+ return resolved;
90
113
  }
91
114
  function requireApk(androidRoot, mode) {
92
115
  const artifactPath = path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pakstr",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "CLI for packaging Nostr web apps into Android APKs",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -1,16 +0,0 @@
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.patchVersion = patchVersion;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
- function patchVersion(androidRoot, versionCode, versionName) {
10
- const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
11
- let gradle = fs_1.default.readFileSync(gradlePath, "utf8");
12
- gradle = gradle.replace(/versionCode\s*=\s*[^\r\n]+/, `versionCode = ${versionCode}`);
13
- gradle = gradle.replace(/versionName\s*=\s*"[^"]+"/, `versionName = "${versionName}"`);
14
- fs_1.default.writeFileSync(gradlePath, gradle);
15
- console.log("✅ Version patched:", versionCode, versionName);
16
- }