pakstr 0.8.6 → 0.9.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.
Files changed (38) hide show
  1. package/android-template/app/build.gradle.kts +11 -3
  2. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +2 -0
  3. package/android-template/app/src/main/java/com/pakstr/app/debug/DebugReportExporter.kt +1 -1
  4. package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +1 -1
  5. package/android-template/app/src/main/java/com/pakstr/app/debug/DeviceDeveloperState.kt +23 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/debug/WebViewDebugManager.kt +31 -4
  7. package/android-template/app/src/test/java/com/pakstr/app/debug/DeviceDeveloperStateTest.kt +21 -0
  8. package/android-template/app/src/test/java/com/pakstr/app/debug/WebViewDebugManagerTest.kt +84 -0
  9. package/bin/cli.js +11 -6
  10. package/dist/cli.js +53 -16
  11. package/dist/commands/build.js +42 -114
  12. package/dist/commands/init.js +191 -0
  13. package/dist/commands/publish.js +137 -0
  14. package/dist/commands/run.js +95 -0
  15. package/dist/commands/sign.js +41 -0
  16. package/dist/core/androidProject.js +70 -0
  17. package/dist/core/appIdentity.js +7 -0
  18. package/dist/core/blossom.js +122 -0
  19. package/dist/core/dotenv.js +51 -0
  20. package/dist/core/nostr.js +153 -0
  21. package/dist/core/pakstrConfig.js +206 -0
  22. package/dist/core/zapStore.js +221 -0
  23. package/dist/runners/DockerGradleRunner.js +38 -128
  24. package/dist/runners/DockerSignRunner.js +182 -0
  25. package/dist/runners/LocalGradleRunner.js +26 -129
  26. package/dist/runners/createRunner.js +4 -4
  27. package/package.json +7 -1
  28. package/dist/android/releaseVerification.js +0 -164
  29. package/dist/config.js +0 -4
  30. package/dist/core/buildContext.js +0 -102
  31. package/dist/core/config.js +0 -24
  32. package/dist/core/copyFile.js +0 -17
  33. package/dist/core/manifest.js +0 -47
  34. package/dist/core/releaseSigning.js +0 -134
  35. package/dist/core/resolveProjectInputs.js +0 -20
  36. package/dist/core/validateProject.js +0 -21
  37. package/dist/core/zeroConfig.js +0 -97
  38. package/dist/runtime/runtimeConfig.js +0 -22
@@ -21,8 +21,11 @@ android {
21
21
 
22
22
  signingConfigs {
23
23
  create("release") {
24
- if (releaseRequested) {
25
- val keystorePath = requireReleaseSigningValue("PAKSTR_ANDROID_KEYSTORE_PATH")
24
+ // Signing is optional: pakstr build produces an UNSIGNED release APK and
25
+ // pakstr sign signs it with apksigner afterwards. Only wire up the
26
+ // Gradle signingConfig when PAKSTR_ANDROID_KEYSTORE_PATH is present.
27
+ val keystorePath = System.getenv("PAKSTR_ANDROID_KEYSTORE_PATH")
28
+ if (releaseRequested && keystorePath != null && keystorePath.isNotEmpty()) {
26
29
  val keystoreFile = File(keystorePath)
27
30
  if (!keystoreFile.isFile) {
28
31
  throw GradleException("Release signing keystore path is not a file")
@@ -53,7 +56,12 @@ android {
53
56
  }
54
57
 
55
58
  release {
56
- signingConfig = signingConfigs.getByName("release")
59
+ // Only attach the signing config when a keystore was provided;
60
+ // otherwise emit an unsigned release APK for pakstr sign to sign.
61
+ val keystorePath = System.getenv("PAKSTR_ANDROID_KEYSTORE_PATH")
62
+ if (keystorePath != null && keystorePath.isNotEmpty()) {
63
+ signingConfig = signingConfigs.getByName("release")
64
+ }
57
65
  isMinifyEnabled = true
58
66
  isShrinkResources = true
59
67
  proguardFiles(
@@ -209,6 +209,7 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
209
209
  private fun setupSupportUnlockGesture() {
210
210
  supportUnlockGestureLayout.onUnlock = {
211
211
  SupportUnlock.unlock(this)
212
+ WebViewDebugManager.enable(this)
212
213
  Toast.makeText(
213
214
  this,
214
215
  R.string.diagnostics_mode_enabled,
@@ -289,6 +290,7 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
289
290
  override fun onResume() {
290
291
  super.onResume()
291
292
 
293
+ WebViewDebugManager.enable(this)
292
294
  if (::fab.isInitialized) {
293
295
  showDebugFab()
294
296
  }
@@ -106,7 +106,7 @@ object DebugReportExporter {
106
106
  ${RuntimeConfig.apiEnabled(context)}
107
107
 
108
108
  WebView Debug:
109
- ${BuildConfig.DEBUG || SupportUnlock.isActive(context)}
109
+ ${WebViewDebugManager.isInspectionAllowed(context)}
110
110
 
111
111
  Logs:
112
112
  -------------------
@@ -129,7 +129,7 @@ class DeveloperToolsActivity : AppCompatActivity() {
129
129
  }
130
130
 
131
131
  private fun isWebViewDebugEnabled(): Boolean {
132
- return BuildConfig.DEBUG || SupportUnlock.isActive(this)
132
+ return WebViewDebugManager.isInspectionAllowed(this)
133
133
  }
134
134
 
135
135
  private fun setupLogActions() {
@@ -0,0 +1,23 @@
1
+ package com.pakstr.app.debug
2
+
3
+ import android.content.Context
4
+ import android.provider.Settings
5
+
6
+ object DeviceDeveloperState {
7
+
8
+ fun isAdbEnabled(context: Context): Boolean {
9
+ return isAdbEnabled {
10
+ Settings.Global.getInt(
11
+ context.contentResolver,
12
+ Settings.Global.ADB_ENABLED,
13
+ 0
14
+ )
15
+ }
16
+ }
17
+
18
+ internal fun isAdbEnabled(
19
+ readSetting: () -> Int
20
+ ): Boolean {
21
+ return readSetting() == 1
22
+ }
23
+ }
@@ -6,15 +6,36 @@ import com.pakstr.app.BuildConfig
6
6
 
7
7
  object WebViewDebugManager {
8
8
 
9
+ internal var debuggingStateApplier: (Boolean) -> Unit =
10
+ WebView::setWebContentsDebuggingEnabled
11
+
12
+
13
+ fun isInspectionAllowed(
14
+ context: Context
15
+ ): Boolean {
16
+ return isInspectionAllowed(
17
+ isDebugBuild = BuildConfig.DEBUG,
18
+ isSupportUnlocked = SupportUnlock.isActive(context),
19
+ isAdbEnabled = DeviceDeveloperState.isAdbEnabled(context)
20
+ )
21
+ }
22
+
23
+ internal fun isInspectionAllowed(
24
+ isDebugBuild: Boolean,
25
+ isSupportUnlocked: Boolean,
26
+ isAdbEnabled: Boolean
27
+ ): Boolean {
28
+ return isDebugBuild ||
29
+ (isSupportUnlocked && isAdbEnabled)
30
+ }
9
31
 
10
32
  fun enable(
11
33
  context: Context
12
34
  ) {
13
35
 
14
- val enabled = BuildConfig.DEBUG ||
15
- SupportUnlock.isActive(context)
36
+ val enabled = isInspectionAllowed(context)
16
37
 
17
- WebView.setWebContentsDebuggingEnabled(enabled)
38
+ setWebContentsDebuggingEnabled(enabled)
18
39
 
19
40
  AppDebugLogger.log(
20
41
  context,
@@ -22,4 +43,10 @@ object WebViewDebugManager {
22
43
  "WebView debugging enabled: $enabled"
23
44
  )
24
45
  }
25
- }
46
+
47
+ internal fun setWebContentsDebuggingEnabled(
48
+ enabled: Boolean
49
+ ) {
50
+ debuggingStateApplier(enabled)
51
+ }
52
+ }
@@ -0,0 +1,21 @@
1
+ package com.pakstr.app.debug
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class DeviceDeveloperStateTest {
8
+
9
+ @Test
10
+ fun adb_is_enabled_only_for_setting_value_one() {
11
+ assertTrue(
12
+ DeviceDeveloperState.isAdbEnabled { 1 }
13
+ )
14
+ assertFalse(
15
+ DeviceDeveloperState.isAdbEnabled { 0 }
16
+ )
17
+ assertFalse(
18
+ DeviceDeveloperState.isAdbEnabled { 2 }
19
+ )
20
+ }
21
+ }
@@ -0,0 +1,84 @@
1
+ package com.pakstr.app.debug
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class WebViewDebugManagerTest {
8
+
9
+ @Test
10
+ fun debug_build_allows_inspection_without_release_prerequisites() {
11
+ assertTrue(
12
+ WebViewDebugManager.isInspectionAllowed(
13
+ isDebugBuild = true,
14
+ isSupportUnlocked = false,
15
+ isAdbEnabled = false
16
+ )
17
+ )
18
+ }
19
+
20
+ @Test
21
+ fun release_allows_inspection_with_support_unlock_and_adb() {
22
+ assertTrue(
23
+ WebViewDebugManager.isInspectionAllowed(
24
+ isDebugBuild = false,
25
+ isSupportUnlocked = true,
26
+ isAdbEnabled = true
27
+ )
28
+ )
29
+ }
30
+
31
+ @Test
32
+ fun release_rejects_support_unlock_without_adb() {
33
+ assertFalse(
34
+ WebViewDebugManager.isInspectionAllowed(
35
+ isDebugBuild = false,
36
+ isSupportUnlocked = true,
37
+ isAdbEnabled = false
38
+ )
39
+ )
40
+ }
41
+
42
+ @Test
43
+ fun release_rejects_adb_without_support_unlock() {
44
+ assertFalse(
45
+ WebViewDebugManager.isInspectionAllowed(
46
+ isDebugBuild = false,
47
+ isSupportUnlocked = false,
48
+ isAdbEnabled = true
49
+ )
50
+ )
51
+ }
52
+
53
+ @Test
54
+ fun release_rejects_inspection_without_either_prerequisite() {
55
+ assertFalse(
56
+ WebViewDebugManager.isInspectionAllowed(
57
+ isDebugBuild = false,
58
+ isSupportUnlocked = false,
59
+ isAdbEnabled = false
60
+ )
61
+ )
62
+ }
63
+
64
+ @Test
65
+ fun allowed_release_state_is_applied_to_webview_debugging() {
66
+ val appliedStates = mutableListOf<Boolean>()
67
+ val previous = WebViewDebugManager.debuggingStateApplier
68
+ WebViewDebugManager.debuggingStateApplier = appliedStates::add
69
+
70
+ try {
71
+ val allowed =
72
+ WebViewDebugManager.isInspectionAllowed(
73
+ isDebugBuild = false,
74
+ isSupportUnlocked = true,
75
+ isAdbEnabled = true
76
+ )
77
+ WebViewDebugManager.setWebContentsDebuggingEnabled(allowed)
78
+ } finally {
79
+ WebViewDebugManager.debuggingStateApplier = previous
80
+ }
81
+
82
+ assertTrue(appliedStates.single())
83
+ }
84
+ }
package/bin/cli.js CHANGED
@@ -3,11 +3,16 @@
3
3
  const { runCLI } = require("../dist/cli.js");
4
4
 
5
5
  runCLI(process.argv.slice(2))
6
-
6
+ .then(() => {
7
+ // runCLI sets process.exitCode on user-facing errors; nothing to do here.
8
+ })
7
9
  .catch((err) => {
8
-
9
- console.error("❌ CLI crashed:", err);
10
-
10
+ // Known, human-readable errors: print just the message.
11
+ const name = err && err.name;
12
+ if (name === "ConfigError" || name === "NsecError" || err && err.envVar !== undefined) {
13
+ console.error("Error:", err.message);
14
+ } else {
15
+ console.error("Error:", err && err.message ? err.message : err);
16
+ }
11
17
  process.exit(1);
12
-
13
- });
18
+ });
package/dist/cli.js CHANGED
@@ -5,8 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runCLI = runCLI;
7
7
  const build_1 = require("./commands/build");
8
+ const sign_1 = require("./commands/sign");
9
+ const publish_1 = require("./commands/publish");
10
+ const run_1 = require("./commands/run");
11
+ const init_1 = require("./commands/init");
12
+ const dotenv_1 = require("./core/dotenv");
8
13
  const package_json_1 = __importDefault(require("../package.json"));
9
14
  async function runCLI(argv) {
15
+ // Auto-load a local `.env` (gitignored) so `pakstr run` works out of the box
16
+ // after `pakstr init`. Existing env vars (e.g. CI secrets) always win.
17
+ (0, dotenv_1.loadDotEnv)();
10
18
  if (argv.includes("--help") || argv.includes("-h")) {
11
19
  printHelp();
12
20
  return;
@@ -16,41 +24,70 @@ async function runCLI(argv) {
16
24
  return;
17
25
  }
18
26
  const command = argv[0];
19
- const subcommand = argv[1];
20
- const args = argv.slice(2);
27
+ const args = argv.slice(1);
21
28
  if (!command) {
22
29
  printHelp();
23
30
  return;
24
31
  }
32
+ const configFlag = extractFlag(args, "--config");
25
33
  switch (command) {
34
+ case "init":
35
+ (0, init_1.initCommand)({ force: args.includes("--force"), out: configFlag });
36
+ return;
26
37
  case "build":
27
- if (subcommand === "apk") {
28
- console.log("🚀 Running: build apk");
29
- return await (0, build_1.buildCommand)(args);
30
- }
31
- console.error("❌ Missing subcommand. Use: build apk");
32
- process.exitCode = 1;
38
+ await (0, build_1.buildCommand)(configFlag ?? undefined);
39
+ return;
40
+ case "sign":
41
+ await (0, sign_1.signCommand)(configFlag ?? undefined);
42
+ return;
43
+ case "publish":
44
+ await (0, publish_1.publishCommand)(configFlag ?? undefined, {
45
+ dryRun: args.includes("--dry-run"),
46
+ verify: args.includes("--verify"),
47
+ outJson: extractFlag(args, "--out-json"),
48
+ });
49
+ return;
50
+ case "run":
51
+ await (0, run_1.runCommand)(configFlag ?? undefined, {
52
+ verifyPublish: args.includes("--verify-publish"),
53
+ publishOutJson: extractFlag(args, "--publish-out-json"),
54
+ });
33
55
  return;
34
56
  default:
35
- console.error(`❌ Unknown command: ${command}`);
57
+ console.error(`Unknown command: ${command}`);
36
58
  printHelp();
37
59
  process.exitCode = 1;
38
60
  return;
39
61
  }
40
62
  }
63
+ function extractFlag(args, flag) {
64
+ const i = args.indexOf(flag);
65
+ if (i === -1)
66
+ return undefined;
67
+ return args[i + 1];
68
+ }
41
69
  function printHelp() {
42
70
  console.log(`
43
- 🚀 Pakstr CLI
71
+ Pakstr — one-step nsec -> build -> sign -> publish.
44
72
 
45
73
  Usage:
74
+ pakstr init [--force] [--config <path>]
75
+ pakstr build [--config <path>]
76
+ pakstr sign [--config <path>]
77
+ pakstr publish [--config <path>] [--dry-run] [--verify] [--out-json <path>]
78
+ pakstr run [--config <path>] [--verify-publish] [--publish-out-json <path>]
46
79
 
47
- pakstr build apk --manifest <path> --web <path> --out <path>
80
+ pakstr --version
81
+ pakstr --help
48
82
 
49
- Example:
83
+ Config:
84
+ pakstr.yaml is the single source of truth (see docs/SPEC.md §3).
85
+ Secrets live in env vars, never in the config file:
86
+ PAKSTR_NSEC nsec that derives the APK signing key (and publish auth by default)
87
+ PAKSTR_PUBLISH_NSEC optional separate publish nsec (see publish.publishKey)
50
88
 
51
- pakstr build apk \
52
- --manifest app.manifest.json \
53
- --web ./dist \
54
- --out ./build/app.apk
89
+ Example:
90
+ pakstr init
91
+ PAKSTR_NSEC=nsec1... pakstr run
55
92
  `);
56
93
  }
@@ -6,95 +6,70 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildCommand = buildCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- const child_process_1 = require("child_process");
10
- const generatedProjectVerification_1 = require("../android/generatedProjectVerification");
11
- const gradle_1 = require("../android/gradle");
12
- const branding_1 = require("../android/branding");
13
- const icon_1 = require("../android/icon");
14
- const permissions_1 = require("../android/permissions");
15
- const releaseWorkspace_1 = require("../android/releaseWorkspace");
16
- const splash_1 = require("../android/splash");
17
9
  const template_1 = require("../android/template");
18
- const buildContext_1 = require("../core/buildContext");
19
- const config_1 = require("../core/config");
20
- const releaseSigning_1 = require("../core/releaseSigning");
21
- const validateProject_1 = require("../core/validateProject");
10
+ const releaseWorkspace_1 = require("../android/releaseWorkspace");
11
+ const pakstrConfig_1 = require("../core/pakstrConfig");
12
+ const androidProject_1 = require("../core/androidProject");
22
13
  const createRunner_1 = require("../runners/createRunner");
23
- const runtimeConfig_1 = require("../runtime/runtimeConfig");
24
- async function buildCommand(args) {
25
- const ctx = (0, buildContext_1.createBuildContext)(args);
26
- const config = (0, config_1.loadConfig)(process.cwd());
27
- const manifest = ctx.manifest;
28
- const mode = ctx.mode;
29
- const appName = manifest.appName;
30
- const finalPath = path_1.default.resolve(ctx.out);
31
- console.log("\n🚀 Pakstr CLI");
32
- console.log("📦 App:", appName);
33
- console.log("🌐 Web:", ctx.dist);
14
+ /**
15
+ * `pakstr build` — produce an UNSIGNED release APK at `build.out`.
16
+ * Spec §5.2. No nsec required.
17
+ */
18
+ async function buildCommand(configPath) {
19
+ const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
20
+ const finalPath = path_1.default.resolve(config.build.out);
21
+ console.log("\n🚀 pakstr build");
22
+ console.log("📦 App:", config.app.appName);
23
+ console.log("🌐 Web:", config.build.web);
34
24
  console.log("📱 Out:", finalPath);
35
- console.log("🏗️ Builder:", config.builder);
36
- console.log("🚀 BUILD MODE:", mode.toUpperCase());
37
- (0, validateProject_1.validateProject)(ctx.dist, manifest);
25
+ console.log("🏗️ Builder:", config.build.builder);
38
26
  validateOutputParent(finalPath);
39
- if (mode === "release") {
40
- removeExistingOutput(finalPath);
41
- }
42
- const signingInput = mode === "release" ? (0, releaseSigning_1.readReleaseSigningInput)() : undefined;
27
+ removeExistingOutput(finalPath);
43
28
  const templateRoot = await (0, template_1.ensureTemplate)();
44
- let workspace;
45
- let signing;
29
+ const workspace = (0, releaseWorkspace_1.createReleaseWorkspace)(templateRoot);
46
30
  let stagedPath;
47
31
  let primaryError;
48
- let cleanupError;
49
32
  try {
50
- const androidRoot = mode === "release"
51
- ? (workspace = (0, releaseWorkspace_1.createReleaseWorkspace)(templateRoot)).androidRoot
52
- : templateRoot;
33
+ const androidRoot = workspace.androidRoot;
53
34
  console.log("📱 Android:", androidRoot);
54
- await prepareAndroidProject(androidRoot, ctx.dist, manifest, appName, ctx.manifestDir);
55
- if (signingInput) {
56
- signing = (0, releaseSigning_1.createReleaseSigningContext)(signingInput);
57
- signingInput.keystoreBase64 = "";
58
- }
59
- console.log("\n⚙️ Running build runner...");
60
- const result = await (0, createRunner_1.createRunner)(config).build({
35
+ await (0, androidProject_1.prepareAndroidProject)({
61
36
  androidRoot,
62
- mode,
37
+ webDir: config.build.web,
38
+ app: config.app,
39
+ configDir: config.configDir,
40
+ });
41
+ console.log("\n⚙️ Running build runner...");
42
+ const result = await (0, createRunner_1.createRunner)(config.build.builder).build({
43
+ androidRoot,
44
+ mode: "release",
63
45
  expectedMetadata: {
64
- applicationId: manifest.appId,
65
- versionCode: manifest.versionCode,
66
- versionName: manifest.versionName,
67
- appLabel: appName,
46
+ applicationId: config.app.appId,
47
+ versionCode: config.app.versionCode,
48
+ versionName: config.app.versionName,
49
+ appLabel: config.app.appName,
68
50
  },
69
- signing,
70
51
  });
71
- if (mode === "release" && !result.verification) {
72
- console.warn("⚠️ Release verification evidence unavailable locally.");
73
- console.warn("ℹ️ CI/Docker release verification will be used.");
52
+ if (!result.unsigned) {
53
+ throw new Error("Build runner did not produce an unsigned release APK");
74
54
  }
75
55
  stagedPath = stageArtifact(result.artifactPath, finalPath);
76
56
  }
77
57
  catch (error) {
78
58
  primaryError = error;
79
59
  }
60
+ let cleanupError;
80
61
  try {
81
- signing?.cleanup();
62
+ workspace.cleanup();
82
63
  }
83
64
  catch (error) {
84
65
  cleanupError = error;
85
66
  }
86
- try {
87
- workspace?.cleanup();
88
- }
89
- catch (error) {
90
- cleanupError ??= error;
91
- }
92
67
  if (primaryError || cleanupError) {
93
68
  removeStagedArtifact(stagedPath);
94
69
  removeExistingOutput(finalPath);
95
70
  if (primaryError) {
96
71
  if (cleanupError) {
97
- console.error("Cleanup also failed:", safeErrorMessage(cleanupError));
72
+ console.error("Workspace cleanup also failed:", safeErrorMessage(cleanupError));
98
73
  }
99
74
  throw primaryError;
100
75
  }
@@ -104,41 +79,17 @@ async function buildCommand(args) {
104
79
  throw new Error("Build completed without a staged APK");
105
80
  }
106
81
  publishArtifact(stagedPath, finalPath);
107
- console.log("\n🚀 BUILD COMPLETE");
108
- console.log("App:", appName);
109
- console.log("Mode:", mode.toUpperCase());
82
+ console.log("\n BUILD COMPLETE (unsigned)");
110
83
  console.log("📦 Output:", finalPath);
111
- openFolder(finalPath);
112
- }
113
- async function prepareAndroidProject(androidRoot, distPath, manifest, appName, manifestDir) {
114
- const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
115
- if (!fs_1.default.existsSync(distPath)) {
116
- throw new Error("Web assets not found");
117
- }
118
- fs_1.default.rmSync(assetsTarget, { recursive: true, force: true });
119
- fs_1.default.mkdirSync(assetsTarget, { recursive: true });
120
- copyFolder(distPath, assetsTarget);
121
- (0, runtimeConfig_1.generateRuntimeConfig)(assetsTarget, manifest);
122
- (0, gradle_1.patchGradle)(androidRoot, manifest);
123
- (0, branding_1.patchAppName)(androidRoot, appName);
124
- if (manifest.ui?.icon) {
125
- await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF", manifestDir);
126
- }
127
- if (manifest.ui?.splash) {
128
- await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF", manifestDir);
129
- }
130
- (0, permissions_1.patchPermissions)(androidRoot, manifest);
131
- (0, generatedProjectVerification_1.verifyGeneratedAndroidProject)(androidRoot, {
132
- applicationId: manifest.appId,
133
- versionCode: manifest.versionCode,
134
- versionName: manifest.versionName,
135
- appLabel: appName,
136
- });
84
+ console.log("➡️ Next: pakstr sign");
85
+ return finalPath;
137
86
  }
138
87
  function validateOutputParent(finalPath) {
139
88
  const parent = path_1.default.dirname(finalPath);
140
89
  if (!fs_1.default.existsSync(parent) || !fs_1.default.lstatSync(parent).isDirectory()) {
141
- throw new Error(`Output directory must already exist: ${parent}`);
90
+ // Create it per spec §5.2 ("Create the parent directory of build.out if missing").
91
+ fs_1.default.mkdirSync(parent, { recursive: true });
92
+ return;
142
93
  }
143
94
  if (fs_1.default.lstatSync(parent).isSymbolicLink()) {
144
95
  throw new Error(`Output directory must not be a symbolic link: ${parent}`);
@@ -157,7 +108,7 @@ function stageArtifact(artifactPath, finalPath) {
157
108
  fs_1.default.copyFileSync(artifactPath, stagedPath, fs_1.default.constants.COPYFILE_EXCL);
158
109
  if (fs_1.default.statSync(stagedPath).size !== fs_1.default.statSync(artifactPath).size) {
159
110
  fs_1.default.rmSync(stageDir, { recursive: true, force: true });
160
- throw new Error("Staged APK size does not match verified artifact");
111
+ throw new Error("Staged APK size does not match built artifact");
161
112
  }
162
113
  return stagedPath;
163
114
  }
@@ -183,29 +134,6 @@ function removeStagedArtifact(stagedPath) {
183
134
  return;
184
135
  fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
185
136
  }
186
- function copyFolder(src, dest) {
187
- for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
188
- const srcPath = path_1.default.join(src, entry.name);
189
- const destPath = path_1.default.join(dest, entry.name);
190
- if (entry.isDirectory()) {
191
- fs_1.default.mkdirSync(destPath, { recursive: true });
192
- copyFolder(srcPath, destPath);
193
- }
194
- else {
195
- fs_1.default.copyFileSync(srcPath, destPath);
196
- }
197
- }
198
- }
199
- function openFolder(filePath) {
200
- if (process.platform !== "darwin" || process.env.CI)
201
- return;
202
- try {
203
- (0, child_process_1.execFileSync)("open", [path_1.default.dirname(filePath)], { stdio: "ignore" });
204
- }
205
- catch {
206
- // Opening the output folder is optional.
207
- }
208
- }
209
137
  function safeErrorMessage(error) {
210
138
  return error instanceof Error ? error.message : "unknown cleanup error";
211
139
  }