@better-update/cli 0.15.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { defineCommand, runMain } from "citty";
5
- import { Console, Context, Data, Deferred, Duration, Effect, Fiber, Layer, Match, Option, ParseResult, Schema, Stream } from "effect";
5
+ import { Console, Context, Data, Deferred, Duration, Effect, Layer, Match, Option, ParseResult, Schema } from "effect";
6
6
  import { Command, FetchHttpClient, FileSystem, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi, Path } from "@effect/platform";
7
7
  import { NodeContext } from "@effect/platform-node";
8
8
  import path, { join } from "node:path";
@@ -14,10 +14,13 @@ import { createServer } from "node:http";
14
14
  import { maxBy, uniqBy } from "es-toolkit";
15
15
  import { createHash, randomBytes, randomUUID } from "node:crypto";
16
16
  import forge from "node-forge";
17
- import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { createReadStream, existsSync, promises, readFileSync, writeFileSync } from "node:fs";
18
+ import { spawn as spawn$1 } from "node-pty";
19
+ import chalk from "chalk";
18
20
  import os from "node:os";
19
21
  import plistMod from "@expo/plist";
20
22
  import { ExpoRunFormatter } from "@expo/xcpretty";
23
+ import ignore from "ignore";
21
24
  import { Buffer as Buffer$1 } from "node:buffer";
22
25
  import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
23
26
  import qrcode from "qrcode-terminal";
@@ -28,7 +31,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
28
31
 
29
32
  //#endregion
30
33
  //#region package.json
31
- var version = "0.15.4";
34
+ var version = "0.17.0";
32
35
 
33
36
  //#endregion
34
37
  //#region src/lib/interactive-mode.ts
@@ -1663,6 +1666,7 @@ var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError"
1663
1666
  var InteractiveProhibitedError = class extends Data.TaggedError("InteractiveProhibitedError") {};
1664
1667
  var CredentialsJsonError = class extends Data.TaggedError("CredentialsJsonError") {};
1665
1668
  var DirtyRepoError = class extends Data.TaggedError("DirtyRepoError") {};
1669
+ var StagingError = class extends Data.TaggedError("StagingError") {};
1666
1670
 
1667
1671
  //#endregion
1668
1672
  //#region src/lib/format-error.ts
@@ -4388,56 +4392,178 @@ const sha256Namespaced = (contentType, contentSha256Hex) => {
4388
4392
  return toBase64Url(createHash("sha256").update(input).digest());
4389
4393
  };
4390
4394
 
4395
+ //#endregion
4396
+ //#region src/lib/pty-runner.ts
4397
+ const ptyDimensions = () => {
4398
+ const stdout = process$1.stdout;
4399
+ return {
4400
+ cols: typeof stdout.columns === "number" && stdout.columns > 0 ? stdout.columns : 120,
4401
+ rows: typeof stdout.rows === "number" && stdout.rows > 0 ? stdout.rows : 40
4402
+ };
4403
+ };
4404
+ const mergeEnv$1 = (overrides) => {
4405
+ const merged = {};
4406
+ for (const [key, value] of Object.entries(process$1.env)) if (typeof value === "string") merged[key] = value;
4407
+ for (const [key, value] of Object.entries(overrides)) merged[key] = value;
4408
+ return merged;
4409
+ };
4410
+ const trySpawn = (input) => {
4411
+ const { cols, rows } = ptyDimensions();
4412
+ try {
4413
+ return spawn$1(input.command, [...input.args], {
4414
+ name: "xterm-256color",
4415
+ cols,
4416
+ rows,
4417
+ cwd: input.cwd,
4418
+ env: mergeEnv$1(input.env)
4419
+ });
4420
+ } catch (error) {
4421
+ return error instanceof Error ? error : new Error(String(error));
4422
+ }
4423
+ };
4424
+ /**
4425
+ * Run a command in a pseudo-terminal so the subprocess sees a real TTY
4426
+ * (preserves spinners, progress bars, and ANSI colors emitted by tools like
4427
+ * CocoaPods and `expo prebuild`). Subprocess output is tee'd: forwarded to
4428
+ * `process.stdout` as raw bytes (so colors/positioning are preserved), and
4429
+ * also buffered into lines for the optional `onLine` callback.
4430
+ *
4431
+ * Returns the subprocess exit code. Spawn failures and signal exits surface
4432
+ * as non-zero exit codes (128+signal for Unix-style signal exits).
4433
+ */
4434
+ const runInPty = (input) => Effect.async((resume) => {
4435
+ const spawned = trySpawn(input);
4436
+ if (spawned instanceof Error) {
4437
+ process$1.stderr.write(`Failed to spawn "${input.command}" in pty: ${spawned.message}\n`);
4438
+ resume(Effect.succeed(1));
4439
+ return;
4440
+ }
4441
+ const proc = spawned;
4442
+ let lineBuf = "";
4443
+ const handleLine = (line) => {
4444
+ if (input.onLine === void 0) return;
4445
+ const annotation = input.onLine(line);
4446
+ if (annotation !== void 0) process$1.stdout.write(`${annotation}\n`);
4447
+ };
4448
+ proc.onData((chunk) => {
4449
+ if (input.silent !== true) process$1.stdout.write(chunk);
4450
+ if (input.onLine === void 0) return;
4451
+ lineBuf += chunk;
4452
+ let nl = lineBuf.indexOf("\n");
4453
+ while (nl !== -1) {
4454
+ const line = lineBuf.slice(0, nl).replace(/\r$/u, "");
4455
+ lineBuf = lineBuf.slice(nl + 1);
4456
+ handleLine(line);
4457
+ nl = lineBuf.indexOf("\n");
4458
+ }
4459
+ });
4460
+ const handleResize = () => {
4461
+ const { cols, rows } = ptyDimensions();
4462
+ try {
4463
+ proc.resize(cols, rows);
4464
+ } catch {}
4465
+ };
4466
+ process$1.stdout.on("resize", handleResize);
4467
+ proc.onExit(({ exitCode, signal }) => {
4468
+ process$1.stdout.off("resize", handleResize);
4469
+ if (lineBuf.length > 0) {
4470
+ handleLine(lineBuf.replace(/\r$/u, ""));
4471
+ lineBuf = "";
4472
+ }
4473
+ const code = signal !== void 0 && signal !== 0 ? 128 + signal : exitCode;
4474
+ resume(Effect.succeed(code));
4475
+ });
4476
+ return Effect.sync(() => {
4477
+ try {
4478
+ proc.kill();
4479
+ } catch {}
4480
+ process$1.stdout.off("resize", handleResize);
4481
+ });
4482
+ });
4483
+
4484
+ //#endregion
4485
+ //#region src/lib/warning-style.ts
4486
+ const ANSI_REGEX_GLOBAL = /[›][[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[\dA-ORZcf-ntqry=><]/gu;
4487
+ const stripAnsi = (input) => input.replaceAll(ANSI_REGEX_GLOBAL, "");
4488
+ const hasAnsi = (input) => {
4489
+ ANSI_REGEX_GLOBAL.lastIndex = 0;
4490
+ return ANSI_REGEX_GLOBAL.test(input);
4491
+ };
4492
+ const WARNING_PATTERNS = [
4493
+ /^\s*warning:/iu,
4494
+ /^\s*\[!\]/u,
4495
+ /^\s*WARNING:/u,
4496
+ /^\s*WARN\b/u,
4497
+ /\bis deprecated\b/iu,
4498
+ /^\s*DEPRECATION\b/iu,
4499
+ /\[MT\]/u,
4500
+ /⚠/u
4501
+ ];
4502
+ const isWarningLine = (rawLine) => {
4503
+ const plain = stripAnsi(rawLine);
4504
+ return WARNING_PATTERNS.some((pattern) => pattern.test(plain));
4505
+ };
4506
+ /**
4507
+ * Style a single output line as a warning. If the line already contains ANSI
4508
+ * escapes (the subprocess pre-colored it), only prepend our yellow ⚠ marker
4509
+ * so the original colors survive. Otherwise color the whole line yellow.
4510
+ */
4511
+ const styleWarningLine = (line) => hasAnsi(line) ? `${chalk.yellow("⚠")} ${line}` : chalk.yellow(`⚠ ${line}`);
4512
+ /**
4513
+ * Emit a CLI-owned warning. Suppressed in JSON mode; in human mode writes a
4514
+ * yellow, ⚠-prefixed line to stderr so it stands out from regular info logs.
4515
+ */
4516
+ const printWarn = (message) => Effect.gen(function* () {
4517
+ if ((yield* OutputMode).json) return;
4518
+ yield* Console.warn(chalk.yellow(`⚠ warning: ${message}`));
4519
+ });
4520
+
4391
4521
  //#endregion
4392
4522
  //#region src/commands/build/run-step.ts
4393
- const runStep = (cmd, step) => Command.exitCode(cmd.pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
4394
- step,
4395
- exitCode: 1,
4396
- message: `${step} failed to spawn: ${String(cause)}`
4397
- })), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
4523
+ const buildFailed = (step, exitCode, message) => new BuildFailedError({
4398
4524
  step,
4399
- exitCode: code,
4400
- message: `${step} exited with code ${code}`
4401
- }))));
4525
+ exitCode,
4526
+ message
4527
+ });
4528
+ const annotateWarning = (line) => isWarningLine(line) ? styleWarningLine(line) : void 0;
4402
4529
  /**
4403
- * Run a build step with stdout piped through a formatter (e.g., xcpretty).
4404
- * stderr passes through to the terminal directly.
4530
+ * Run a build step in a PTY so the subprocess sees a real TTY (spinners,
4531
+ * progress bars, ANSI colors are preserved). Completed lines are inspected
4532
+ * and any detected warning is re-echoed with our yellow ⚠ annotation.
4533
+ */
4534
+ const runStep = (cmd, step) => runInPty({
4535
+ command: cmd.command,
4536
+ args: cmd.args,
4537
+ cwd: cmd.cwd,
4538
+ env: cmd.env,
4539
+ onLine: annotateWarning
4540
+ }).pipe(Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`))));
4541
+ /**
4542
+ * Run a build step in a PTY, but feed each completed line through the supplied
4543
+ * xcpretty-style formatter before writing. Warning detection still applies to
4544
+ * the formatter's output so xcodebuild deprecation notices stand out.
4545
+ *
4546
+ * The PTY guarantees xcodebuild sees a real TTY (so it keeps colored output);
4547
+ * the formatter strips noise. On failure the formatter's build summary is
4548
+ * flushed to stderr to help diagnose.
4405
4549
  */
4406
4550
  const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
4407
- const proc = yield* Command.start(cmd.pipe(Command.stdout("pipe"), Command.stderr("pipe"))).pipe(Effect.mapError((cause) => new BuildFailedError({
4408
- step,
4409
- exitCode: 1,
4410
- message: `${step} failed to spawn: ${String(cause)}`
4411
- })));
4412
- const stdoutFiber = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => {
4413
- const formatted = formatter.pipe(line);
4414
- return formatted.length > 0 ? Effect.sync(() => {
4415
- for (const output of formatted) process$1.stdout.write(`${output}\n`);
4416
- }) : Effect.void;
4417
- }), Effect.mapError((cause) => new BuildFailedError({
4418
- step,
4419
- exitCode: 1,
4420
- message: `${step} stdout stream error: ${String(cause)}`
4421
- })), Effect.fork);
4422
- const stderrFiber = yield* proc.stderr.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => Effect.sync(() => process$1.stderr.write(`${line}\n`))), Effect.mapError((cause) => new BuildFailedError({
4423
- step,
4424
- exitCode: 1,
4425
- message: `${step} stderr stream error: ${String(cause)}`
4426
- })), Effect.fork);
4427
- yield* Effect.all([Fiber.join(stdoutFiber), Fiber.join(stderrFiber)], { concurrency: 2 }).pipe(Effect.catchAll(() => Effect.void));
4428
- const code = yield* proc.exitCode.pipe(Effect.mapError((cause) => new BuildFailedError({
4429
- step,
4430
- exitCode: 1,
4431
- message: `${step} exit code error: ${String(cause)}`
4432
- })));
4551
+ const code = yield* runInPty({
4552
+ command: cmd.command,
4553
+ args: cmd.args,
4554
+ cwd: cmd.cwd,
4555
+ env: cmd.env,
4556
+ silent: true,
4557
+ onLine: (line) => {
4558
+ const formatted = formatter.pipe(line);
4559
+ for (const output of formatted) if (isWarningLine(output)) process$1.stdout.write(`${styleWarningLine(output)}\n`);
4560
+ else process$1.stdout.write(`${output}\n`);
4561
+ }
4562
+ });
4433
4563
  if (code !== 0) {
4434
4564
  const summary = formatter.getBuildSummary();
4435
- if (summary) process$1.stderr.write(`${summary}\n`);
4436
- return yield* new BuildFailedError({
4437
- step,
4438
- exitCode: code,
4439
- message: `${step} exited with code ${code}`
4440
- });
4565
+ if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
4566
+ return yield* Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`));
4441
4567
  }
4442
4568
  });
4443
4569
 
@@ -4470,7 +4596,18 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4470
4596
  applicationIdentifier,
4471
4597
  tempDir
4472
4598
  });
4473
- yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "android", "--clean").pipe(Command.workingDirectory(projectRoot), Command.env(commandEnv)), "expo prebuild android");
4599
+ yield* runStep({
4600
+ command: "bunx",
4601
+ args: [
4602
+ "expo",
4603
+ "prebuild",
4604
+ "--platform",
4605
+ "android",
4606
+ "--clean"
4607
+ ],
4608
+ cwd: projectRoot,
4609
+ env: commandEnv
4610
+ }, "expo prebuild android");
4474
4611
  const fs = yield* FileSystem.FileSystem;
4475
4612
  const signingGradlePath = path.join(tempDir, "signing.gradle");
4476
4613
  yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
@@ -4479,8 +4616,16 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4479
4616
  keyAlias: credentials.keyAlias,
4480
4617
  keyPassword: credentials.keyPassword
4481
4618
  }));
4482
- const taskName = gradleTaskName(format, flavor, buildType);
4483
- yield* runStep(Command.make("./gradlew", "--init-script", signingGradlePath, `:app:${taskName}`).pipe(Command.workingDirectory(androidDir), Command.env(commandEnv)), "gradlew");
4619
+ yield* runStep({
4620
+ command: "./gradlew",
4621
+ args: [
4622
+ "--init-script",
4623
+ signingGradlePath,
4624
+ `:app:${gradleTaskName(format, flavor, buildType)}`
4625
+ ],
4626
+ cwd: androidDir,
4627
+ env: commandEnv
4628
+ }, "gradlew");
4484
4629
  const artifactPath = yield* findAndroidArtifact({
4485
4630
  projectRoot,
4486
4631
  format,
@@ -5333,8 +5478,8 @@ const validateIosBuild = (params) => Effect.gen(function* () {
5333
5478
  const validatedBundleIds = new Set(perBundle.map((entry) => entry.bundleId).filter((id) => id !== void 0));
5334
5479
  for (const expected of params.expectedTargets) if (!validatedBundleIds.has(expected.bundleId)) warnings.push(`Expected signed target "${expected.bundleId}" was not found in the archive.`);
5335
5480
  if (warnings.length > 0) {
5336
- yield* Console.warn("Post-build validation warnings:");
5337
- for (const warning of warnings) yield* Console.warn(` - ${warning}`);
5481
+ yield* printWarn("Post-build validation warnings:");
5482
+ for (const warning of warnings) yield* printWarn(` - ${warning}`);
5338
5483
  }
5339
5484
  return {
5340
5485
  passed: warnings.length === 0,
@@ -5491,8 +5636,24 @@ const findXcworkspace = (iosDir) => Effect.gen(function* () {
5491
5636
  return workspace;
5492
5637
  });
5493
5638
  const prebuildAndPods = (params) => Effect.gen(function* () {
5494
- yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "ios", "--clean").pipe(Command.workingDirectory(params.projectRoot), Command.env(params.commandEnv)), "expo prebuild ios");
5495
- yield* runStep(Command.make("pod", "install").pipe(Command.workingDirectory(params.iosDir), Command.env(params.commandEnv)), "pod install");
5639
+ yield* runStep({
5640
+ command: "bunx",
5641
+ args: [
5642
+ "expo",
5643
+ "prebuild",
5644
+ "--platform",
5645
+ "ios",
5646
+ "--clean"
5647
+ ],
5648
+ cwd: params.projectRoot,
5649
+ env: params.commandEnv
5650
+ }, "expo prebuild ios");
5651
+ yield* runStep({
5652
+ command: "pod",
5653
+ args: ["install"],
5654
+ cwd: params.iosDir,
5655
+ env: params.commandEnv
5656
+ }, "pod install");
5496
5657
  });
5497
5658
  const findAppDirectory = (root) => Effect.gen(function* () {
5498
5659
  const fs = yield* FileSystem.FileSystem;
@@ -5527,13 +5688,46 @@ const runIosSimulatorBuild = (input) => Effect.gen(function* () {
5527
5688
  const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
5528
5689
  const configuration = iosProfile.buildConfiguration ?? "Release";
5529
5690
  const derivedDataPath = path.join(tempDir, "derived-data");
5530
- const buildCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-sdk", "iphonesimulator", "-destination", "generic/platform=iOS Simulator", "-derivedDataPath", derivedDataPath, "build", "CODE_SIGNING_ALLOWED=NO", "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
5691
+ const buildCmd = {
5692
+ command: "xcodebuild",
5693
+ args: [
5694
+ "-workspace",
5695
+ workspaceFilename,
5696
+ "-scheme",
5697
+ scheme,
5698
+ "-configuration",
5699
+ configuration,
5700
+ "-sdk",
5701
+ "iphonesimulator",
5702
+ "-destination",
5703
+ "generic/platform=iOS Simulator",
5704
+ "-derivedDataPath",
5705
+ derivedDataPath,
5706
+ "build",
5707
+ "CODE_SIGNING_ALLOWED=NO",
5708
+ "CODE_SIGNING_REQUIRED=NO",
5709
+ "CODE_SIGN_IDENTITY="
5710
+ ],
5711
+ cwd: iosDir,
5712
+ env: commandEnv
5713
+ };
5531
5714
  const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
5532
5715
  yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
5533
5716
  const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
5534
5717
  const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
5535
5718
  const archivePath = path.join(tempDir, archiveName);
5536
- yield* runStep(Command.make("tar", "-czf", archivePath, "-C", path.dirname(appDir), path.basename(appDir)).pipe(Command.env(commandEnv)), "tar simulator .app");
5719
+ yield* runStep({
5720
+ command: "tar",
5721
+ args: [
5722
+ "-czf",
5723
+ archivePath,
5724
+ "-C",
5725
+ path.dirname(appDir),
5726
+ path.basename(appDir)
5727
+ ],
5728
+ cwd: projectRoot,
5729
+ env: commandEnv
5730
+ }, "tar simulator .app");
5537
5731
  const { sha256, byteSize } = yield* sha256File(archivePath);
5538
5732
  return {
5539
5733
  artifactPath: archivePath,
@@ -5636,7 +5830,23 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
5636
5830
  }))
5637
5831
  });
5638
5832
  const archivePath = path.join(tempDir, "build.xcarchive");
5639
- const archiveCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-archivePath", archivePath, "-allowProvisioningUpdates", "archive").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
5833
+ const archiveCmd = {
5834
+ command: "xcodebuild",
5835
+ args: [
5836
+ "-workspace",
5837
+ workspaceFilename,
5838
+ "-scheme",
5839
+ scheme,
5840
+ "-configuration",
5841
+ configuration,
5842
+ "-archivePath",
5843
+ archivePath,
5844
+ "-allowProvisioningUpdates",
5845
+ "archive"
5846
+ ],
5847
+ cwd: iosDir,
5848
+ env: commandEnv
5849
+ };
5640
5850
  const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
5641
5851
  yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
5642
5852
  const exportOptionsPath = path.join(tempDir, "ExportOptions.plist");
@@ -5656,7 +5866,21 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
5656
5866
  }))
5657
5867
  }));
5658
5868
  const exportPath = path.join(tempDir, "export");
5659
- const exportCmd = Command.make("xcodebuild", "-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", exportOptionsPath, "-allowProvisioningUpdates").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
5869
+ const exportCmd = {
5870
+ command: "xcodebuild",
5871
+ args: [
5872
+ "-exportArchive",
5873
+ "-archivePath",
5874
+ archivePath,
5875
+ "-exportPath",
5876
+ exportPath,
5877
+ "-exportOptionsPlist",
5878
+ exportOptionsPath,
5879
+ "-allowProvisioningUpdates"
5880
+ ],
5881
+ cwd: iosDir,
5882
+ env: commandEnv
5883
+ };
5660
5884
  yield* formatter ? runStepFormatted(exportCmd, "xcodebuild exportArchive", formatter) : runStep(exportCmd, "xcodebuild exportArchive");
5661
5885
  yield* validateIosBuild({
5662
5886
  archivePath,
@@ -6241,7 +6465,7 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
6241
6465
  const warnOnGradleMismatch = (gradleConfig, expectedPackage) => {
6242
6466
  if (!gradleConfig?.applicationId) return Effect.void;
6243
6467
  if (gradleConfig.applicationId === expectedPackage) return Effect.void;
6244
- return Console.warn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
6468
+ return printWarn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
6245
6469
  };
6246
6470
  /**
6247
6471
  * Strip Groovy single-line and block comments.
@@ -6298,6 +6522,131 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
6298
6522
  })));
6299
6523
  });
6300
6524
 
6525
+ //#endregion
6526
+ //#region src/lib/project-staging.ts
6527
+ const LOCKFILES = [
6528
+ ["bun.lock", "bun"],
6529
+ ["bun.lockb", "bun"],
6530
+ ["pnpm-lock.yaml", "pnpm"],
6531
+ ["yarn.lock", "yarn"],
6532
+ ["package-lock.json", "npm"]
6533
+ ];
6534
+ /**
6535
+ * Paths never copied into staging — covers generated native build outputs and
6536
+ * dependency dirs that must be reinstalled fresh in staging.
6537
+ */
6538
+ const ALWAYS_IGNORE = [
6539
+ "node_modules",
6540
+ ".git",
6541
+ "ios/build",
6542
+ "ios/Pods",
6543
+ "ios/DerivedData",
6544
+ "android/build",
6545
+ "android/app/build",
6546
+ "android/.gradle",
6547
+ "android/.kotlin",
6548
+ ".expo",
6549
+ ".gradle",
6550
+ ".turbo",
6551
+ "dist"
6552
+ ];
6553
+ const findLockfile = (fs, dir) => Effect.gen(function* () {
6554
+ for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
6555
+ });
6556
+ const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
6557
+ const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
6558
+ if (pm !== void 0) return {
6559
+ workspaceRoot: dir,
6560
+ packageManager: pm
6561
+ };
6562
+ const parent = path.dirname(dir);
6563
+ if (parent === dir) return {
6564
+ workspaceRoot: startCwd,
6565
+ packageManager: "bun"
6566
+ };
6567
+ return yield* walkUpForLockfile(startCwd, parent);
6568
+ });
6569
+ /**
6570
+ * Walk up from `cwd` to the first ancestor directory containing a lockfile.
6571
+ * That directory is the install root (monorepo workspace root or the app dir
6572
+ * itself in single-app layouts). Defaults to `cwd` + bun when no lockfile is
6573
+ * found anywhere up to the volume root.
6574
+ */
6575
+ const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
6576
+ /**
6577
+ * Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
6578
+ * `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
6579
+ * is layered on top of the always-ignore baseline.
6580
+ */
6581
+ const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
6582
+ const fs = yield* FileSystem.FileSystem;
6583
+ const ig = ignore();
6584
+ ig.add([...ALWAYS_IGNORE]);
6585
+ const easignorePath = path.join(workspaceRoot, ".easignore");
6586
+ if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
6587
+ const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
6588
+ ig.add(content);
6589
+ return ig;
6590
+ }
6591
+ const gitignorePath = path.join(workspaceRoot, ".gitignore");
6592
+ if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
6593
+ const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
6594
+ ig.add(content);
6595
+ }
6596
+ return ig;
6597
+ });
6598
+ const copyProjectTree = (params) => Effect.tryPromise({
6599
+ try: async () => {
6600
+ await promises.cp(params.source, params.dest, {
6601
+ recursive: true,
6602
+ dereference: false,
6603
+ filter: (src) => {
6604
+ const rel = path.relative(params.source, src);
6605
+ if (rel === "") return true;
6606
+ const posixRel = rel.split(path.sep).join("/");
6607
+ return !params.ig.ignores(posixRel);
6608
+ }
6609
+ });
6610
+ },
6611
+ catch: (cause) => new StagingError({ message: `Failed to copy project to staging dir: ${formatCause(cause)}` })
6612
+ });
6613
+ const runInstall = (params) => runStep({
6614
+ command: params.packageManager,
6615
+ args: ["install"],
6616
+ cwd: params.stagingRoot,
6617
+ env: params.env
6618
+ }, `${params.packageManager} install`);
6619
+ /**
6620
+ * Copy the user's project (or workspace root, for monorepos) into a fresh
6621
+ * directory inside `tempDir`, then run `<pm> install` there. The build then
6622
+ * runs entirely against the staged copy — the user's working tree stays clean
6623
+ * regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
6624
+ */
6625
+ const prepareStagingProject = (input) => Effect.gen(function* () {
6626
+ const runtime = yield* CliRuntime;
6627
+ const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
6628
+ const relAppPath = path.relative(workspaceRoot, input.userCwd);
6629
+ const stagingRoot = path.join(input.tempDir, "project");
6630
+ const projectRoot = relAppPath === "" ? stagingRoot : path.join(stagingRoot, relAppPath);
6631
+ yield* Console.log(`Staging build into ${stagingRoot}${relAppPath === "" ? "" : ` (app: ${relAppPath})`}`);
6632
+ yield* copyProjectTree({
6633
+ source: workspaceRoot,
6634
+ dest: stagingRoot,
6635
+ ig: yield* buildIgnoreInstance(workspaceRoot)
6636
+ });
6637
+ yield* runInstall({
6638
+ stagingRoot,
6639
+ packageManager,
6640
+ env: yield* runtime.commandEnvironment(input.envVars)
6641
+ });
6642
+ return {
6643
+ stagingRoot,
6644
+ projectRoot,
6645
+ packageManager,
6646
+ relAppPath
6647
+ };
6648
+ });
6649
+
6301
6650
  //#endregion
6302
6651
  //#region src/lib/repo-clean.ts
6303
6652
  const MAX_FILES_SHOWN = 10;
@@ -6447,35 +6796,40 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
6447
6796
  });
6448
6797
  const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6449
6798
  const api = yield* apiClient;
6450
- const projectRoot = yield* (yield* CliRuntime).cwd;
6799
+ const userCwd = yield* (yield* CliRuntime).cwd;
6451
6800
  yield* ensureRepoClean({
6452
- projectRoot,
6801
+ projectRoot: userCwd,
6453
6802
  allowDirty: options.allowDirty ?? false,
6454
6803
  label: "build"
6455
6804
  });
6456
- const baseConfig = yield* readExpoConfig(projectRoot);
6805
+ const baseConfig = yield* readExpoConfig(userCwd);
6457
6806
  const projectId = yield* extractProjectId(baseConfig);
6458
6807
  const platform = yield* detectPlatform(options.platform, baseConfig);
6459
- const profile = yield* readBuildProfile(projectRoot, yield* resolveProfileName(projectRoot, options.profileName));
6808
+ const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
6460
6809
  const envVars = yield* pullEnvVars(api, {
6461
6810
  projectId,
6462
6811
  environment: profile.environment
6463
6812
  });
6464
6813
  yield* applyAutoIncrement({
6465
- projectRoot,
6814
+ projectRoot: userCwd,
6466
6815
  platform,
6467
- config: yield* readExpoConfig(projectRoot, envVars),
6816
+ config: yield* readExpoConfig(userCwd, envVars),
6468
6817
  ...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
6469
6818
  ...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
6470
6819
  });
6471
- const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, envVars), platform);
6820
+ const appMeta = yield* readAppMeta(yield* readExpoConfig(userCwd, envVars), platform);
6472
6821
  const runtimeVersion = yield* resolveRuntimeVersion({
6473
6822
  raw: appMeta.rawRuntimeVersion,
6474
6823
  appVersion: appMeta.appVersion,
6475
- projectRoot
6824
+ projectRoot: userCwd
6476
6825
  });
6477
- if (options.clearCache) yield* clearBuildCaches(projectRoot);
6826
+ if (options.clearCache) yield* clearBuildCaches(userCwd);
6478
6827
  const tempDir = yield* acquireBuildTempDir;
6828
+ const staging = yield* prepareStagingProject({
6829
+ userCwd,
6830
+ tempDir,
6831
+ envVars
6832
+ });
6479
6833
  yield* Console.log(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
6480
6834
  const { build, target, bundleId } = yield* runPlatformBuild({
6481
6835
  api,
@@ -6485,14 +6839,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6485
6839
  appMeta,
6486
6840
  envVars,
6487
6841
  projectId,
6488
- projectRoot,
6842
+ projectRoot: staging.projectRoot,
6489
6843
  tempDir
6490
6844
  });
6491
6845
  yield* Console.log(`Artifact produced: ${build.artifactPath}`);
6492
6846
  let exportedArtifactPath = void 0;
6493
6847
  if (options.output !== void 0) {
6494
6848
  const fs = yield* FileSystem.FileSystem;
6495
- const outputPath = path.resolve(projectRoot, options.output);
6849
+ const outputPath = path.resolve(userCwd, options.output);
6496
6850
  const outputDir = path.dirname(outputPath);
6497
6851
  yield* fs.makeDirectory(outputDir, { recursive: true }).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to create output directory: ${formatCause(cause)}` })));
6498
6852
  yield* fs.copyFile(build.artifactPath, outputPath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to copy artifact to ${outputPath}: ${formatCause(cause)}` })));
@@ -6509,7 +6863,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6509
6863
  ]);
6510
6864
  return;
6511
6865
  }
6512
- const rawGitContext = yield* readGitContext(projectRoot);
6866
+ const rawGitContext = yield* readGitContext(userCwd);
6513
6867
  const gitContext = {
6514
6868
  ...rawGitContext.ref === void 0 ? {} : { ref: rawGitContext.ref },
6515
6869
  ...rawGitContext.commit === void 0 ? {} : { commit: rawGitContext.commit },
@@ -6653,7 +7007,8 @@ const BUILD_EXIT_EXTRAS = {
6653
7007
  PresignedUrlExpiredError: 7,
6654
7008
  CompleteError: 7,
6655
7009
  EnvExportError: 7,
6656
- DirtyRepoError: 3
7010
+ DirtyRepoError: 3,
7011
+ StagingError: 6
6657
7012
  };
6658
7013
  const buildCommand = defineCommand({
6659
7014
  meta: {