@better-update/cli 0.15.4 → 0.16.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";
@@ -15,6 +15,8 @@ import { maxBy, uniqBy } from "es-toolkit";
15
15
  import { createHash, randomBytes, randomUUID } from "node:crypto";
16
16
  import forge from "node-forge";
17
17
  import { createReadStream, existsSync, 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";
@@ -28,7 +30,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
28
30
 
29
31
  //#endregion
30
32
  //#region package.json
31
- var version = "0.15.4";
33
+ var version = "0.16.0";
32
34
 
33
35
  //#endregion
34
36
  //#region src/lib/interactive-mode.ts
@@ -4388,56 +4390,178 @@ const sha256Namespaced = (contentType, contentSha256Hex) => {
4388
4390
  return toBase64Url(createHash("sha256").update(input).digest());
4389
4391
  };
4390
4392
 
4393
+ //#endregion
4394
+ //#region src/lib/pty-runner.ts
4395
+ const ptyDimensions = () => {
4396
+ const stdout = process$1.stdout;
4397
+ return {
4398
+ cols: typeof stdout.columns === "number" && stdout.columns > 0 ? stdout.columns : 120,
4399
+ rows: typeof stdout.rows === "number" && stdout.rows > 0 ? stdout.rows : 40
4400
+ };
4401
+ };
4402
+ const mergeEnv$1 = (overrides) => {
4403
+ const merged = {};
4404
+ for (const [key, value] of Object.entries(process$1.env)) if (typeof value === "string") merged[key] = value;
4405
+ for (const [key, value] of Object.entries(overrides)) merged[key] = value;
4406
+ return merged;
4407
+ };
4408
+ const trySpawn = (input) => {
4409
+ const { cols, rows } = ptyDimensions();
4410
+ try {
4411
+ return spawn$1(input.command, [...input.args], {
4412
+ name: "xterm-256color",
4413
+ cols,
4414
+ rows,
4415
+ cwd: input.cwd,
4416
+ env: mergeEnv$1(input.env)
4417
+ });
4418
+ } catch (error) {
4419
+ return error instanceof Error ? error : new Error(String(error));
4420
+ }
4421
+ };
4422
+ /**
4423
+ * Run a command in a pseudo-terminal so the subprocess sees a real TTY
4424
+ * (preserves spinners, progress bars, and ANSI colors emitted by tools like
4425
+ * CocoaPods and `expo prebuild`). Subprocess output is tee'd: forwarded to
4426
+ * `process.stdout` as raw bytes (so colors/positioning are preserved), and
4427
+ * also buffered into lines for the optional `onLine` callback.
4428
+ *
4429
+ * Returns the subprocess exit code. Spawn failures and signal exits surface
4430
+ * as non-zero exit codes (128+signal for Unix-style signal exits).
4431
+ */
4432
+ const runInPty = (input) => Effect.async((resume) => {
4433
+ const spawned = trySpawn(input);
4434
+ if (spawned instanceof Error) {
4435
+ process$1.stderr.write(`Failed to spawn "${input.command}" in pty: ${spawned.message}\n`);
4436
+ resume(Effect.succeed(1));
4437
+ return;
4438
+ }
4439
+ const proc = spawned;
4440
+ let lineBuf = "";
4441
+ const handleLine = (line) => {
4442
+ if (input.onLine === void 0) return;
4443
+ const annotation = input.onLine(line);
4444
+ if (annotation !== void 0) process$1.stdout.write(`${annotation}\n`);
4445
+ };
4446
+ proc.onData((chunk) => {
4447
+ if (input.silent !== true) process$1.stdout.write(chunk);
4448
+ if (input.onLine === void 0) return;
4449
+ lineBuf += chunk;
4450
+ let nl = lineBuf.indexOf("\n");
4451
+ while (nl !== -1) {
4452
+ const line = lineBuf.slice(0, nl).replace(/\r$/u, "");
4453
+ lineBuf = lineBuf.slice(nl + 1);
4454
+ handleLine(line);
4455
+ nl = lineBuf.indexOf("\n");
4456
+ }
4457
+ });
4458
+ const handleResize = () => {
4459
+ const { cols, rows } = ptyDimensions();
4460
+ try {
4461
+ proc.resize(cols, rows);
4462
+ } catch {}
4463
+ };
4464
+ process$1.stdout.on("resize", handleResize);
4465
+ proc.onExit(({ exitCode, signal }) => {
4466
+ process$1.stdout.off("resize", handleResize);
4467
+ if (lineBuf.length > 0) {
4468
+ handleLine(lineBuf.replace(/\r$/u, ""));
4469
+ lineBuf = "";
4470
+ }
4471
+ const code = signal !== void 0 && signal !== 0 ? 128 + signal : exitCode;
4472
+ resume(Effect.succeed(code));
4473
+ });
4474
+ return Effect.sync(() => {
4475
+ try {
4476
+ proc.kill();
4477
+ } catch {}
4478
+ process$1.stdout.off("resize", handleResize);
4479
+ });
4480
+ });
4481
+
4482
+ //#endregion
4483
+ //#region src/lib/warning-style.ts
4484
+ const ANSI_REGEX_GLOBAL = /[›][[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[\dA-ORZcf-ntqry=><]/gu;
4485
+ const stripAnsi = (input) => input.replaceAll(ANSI_REGEX_GLOBAL, "");
4486
+ const hasAnsi = (input) => {
4487
+ ANSI_REGEX_GLOBAL.lastIndex = 0;
4488
+ return ANSI_REGEX_GLOBAL.test(input);
4489
+ };
4490
+ const WARNING_PATTERNS = [
4491
+ /^\s*warning:/iu,
4492
+ /^\s*\[!\]/u,
4493
+ /^\s*WARNING:/u,
4494
+ /^\s*WARN\b/u,
4495
+ /\bis deprecated\b/iu,
4496
+ /^\s*DEPRECATION\b/iu,
4497
+ /\[MT\]/u,
4498
+ /⚠/u
4499
+ ];
4500
+ const isWarningLine = (rawLine) => {
4501
+ const plain = stripAnsi(rawLine);
4502
+ return WARNING_PATTERNS.some((pattern) => pattern.test(plain));
4503
+ };
4504
+ /**
4505
+ * Style a single output line as a warning. If the line already contains ANSI
4506
+ * escapes (the subprocess pre-colored it), only prepend our yellow ⚠ marker
4507
+ * so the original colors survive. Otherwise color the whole line yellow.
4508
+ */
4509
+ const styleWarningLine = (line) => hasAnsi(line) ? `${chalk.yellow("⚠")} ${line}` : chalk.yellow(`⚠ ${line}`);
4510
+ /**
4511
+ * Emit a CLI-owned warning. Suppressed in JSON mode; in human mode writes a
4512
+ * yellow, ⚠-prefixed line to stderr so it stands out from regular info logs.
4513
+ */
4514
+ const printWarn = (message) => Effect.gen(function* () {
4515
+ if ((yield* OutputMode).json) return;
4516
+ yield* Console.warn(chalk.yellow(`⚠ warning: ${message}`));
4517
+ });
4518
+
4391
4519
  //#endregion
4392
4520
  //#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({
4521
+ const buildFailed = (step, exitCode, message) => new BuildFailedError({
4394
4522
  step,
4395
- exitCode: 1,
4396
- message: `${step} failed to spawn: ${String(cause)}`
4397
- })), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
4398
- step,
4399
- exitCode: code,
4400
- message: `${step} exited with code ${code}`
4401
- }))));
4523
+ exitCode,
4524
+ message
4525
+ });
4526
+ const annotateWarning = (line) => isWarningLine(line) ? styleWarningLine(line) : void 0;
4527
+ /**
4528
+ * Run a build step in a PTY so the subprocess sees a real TTY (spinners,
4529
+ * progress bars, ANSI colors are preserved). Completed lines are inspected
4530
+ * and any detected warning is re-echoed with our yellow ⚠ annotation.
4531
+ */
4532
+ const runStep = (cmd, step) => runInPty({
4533
+ command: cmd.command,
4534
+ args: cmd.args,
4535
+ cwd: cmd.cwd,
4536
+ env: cmd.env,
4537
+ onLine: annotateWarning
4538
+ }).pipe(Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`))));
4402
4539
  /**
4403
- * Run a build step with stdout piped through a formatter (e.g., xcpretty).
4404
- * stderr passes through to the terminal directly.
4540
+ * Run a build step in a PTY, but feed each completed line through the supplied
4541
+ * xcpretty-style formatter before writing. Warning detection still applies to
4542
+ * the formatter's output so xcodebuild deprecation notices stand out.
4543
+ *
4544
+ * The PTY guarantees xcodebuild sees a real TTY (so it keeps colored output);
4545
+ * the formatter strips noise. On failure the formatter's build summary is
4546
+ * flushed to stderr to help diagnose.
4405
4547
  */
4406
4548
  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
- })));
4549
+ const code = yield* runInPty({
4550
+ command: cmd.command,
4551
+ args: cmd.args,
4552
+ cwd: cmd.cwd,
4553
+ env: cmd.env,
4554
+ silent: true,
4555
+ onLine: (line) => {
4556
+ const formatted = formatter.pipe(line);
4557
+ for (const output of formatted) if (isWarningLine(output)) process$1.stdout.write(`${styleWarningLine(output)}\n`);
4558
+ else process$1.stdout.write(`${output}\n`);
4559
+ }
4560
+ });
4433
4561
  if (code !== 0) {
4434
4562
  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
- });
4563
+ if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
4564
+ return yield* Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`));
4441
4565
  }
4442
4566
  });
4443
4567
 
@@ -4470,7 +4594,18 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4470
4594
  applicationIdentifier,
4471
4595
  tempDir
4472
4596
  });
4473
- yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "android", "--clean").pipe(Command.workingDirectory(projectRoot), Command.env(commandEnv)), "expo prebuild android");
4597
+ yield* runStep({
4598
+ command: "bunx",
4599
+ args: [
4600
+ "expo",
4601
+ "prebuild",
4602
+ "--platform",
4603
+ "android",
4604
+ "--clean"
4605
+ ],
4606
+ cwd: projectRoot,
4607
+ env: commandEnv
4608
+ }, "expo prebuild android");
4474
4609
  const fs = yield* FileSystem.FileSystem;
4475
4610
  const signingGradlePath = path.join(tempDir, "signing.gradle");
4476
4611
  yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
@@ -4479,8 +4614,16 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4479
4614
  keyAlias: credentials.keyAlias,
4480
4615
  keyPassword: credentials.keyPassword
4481
4616
  }));
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");
4617
+ yield* runStep({
4618
+ command: "./gradlew",
4619
+ args: [
4620
+ "--init-script",
4621
+ signingGradlePath,
4622
+ `:app:${gradleTaskName(format, flavor, buildType)}`
4623
+ ],
4624
+ cwd: androidDir,
4625
+ env: commandEnv
4626
+ }, "gradlew");
4484
4627
  const artifactPath = yield* findAndroidArtifact({
4485
4628
  projectRoot,
4486
4629
  format,
@@ -5333,8 +5476,8 @@ const validateIosBuild = (params) => Effect.gen(function* () {
5333
5476
  const validatedBundleIds = new Set(perBundle.map((entry) => entry.bundleId).filter((id) => id !== void 0));
5334
5477
  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
5478
  if (warnings.length > 0) {
5336
- yield* Console.warn("Post-build validation warnings:");
5337
- for (const warning of warnings) yield* Console.warn(` - ${warning}`);
5479
+ yield* printWarn("Post-build validation warnings:");
5480
+ for (const warning of warnings) yield* printWarn(` - ${warning}`);
5338
5481
  }
5339
5482
  return {
5340
5483
  passed: warnings.length === 0,
@@ -5491,8 +5634,24 @@ const findXcworkspace = (iosDir) => Effect.gen(function* () {
5491
5634
  return workspace;
5492
5635
  });
5493
5636
  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");
5637
+ yield* runStep({
5638
+ command: "bunx",
5639
+ args: [
5640
+ "expo",
5641
+ "prebuild",
5642
+ "--platform",
5643
+ "ios",
5644
+ "--clean"
5645
+ ],
5646
+ cwd: params.projectRoot,
5647
+ env: params.commandEnv
5648
+ }, "expo prebuild ios");
5649
+ yield* runStep({
5650
+ command: "pod",
5651
+ args: ["install"],
5652
+ cwd: params.iosDir,
5653
+ env: params.commandEnv
5654
+ }, "pod install");
5496
5655
  });
5497
5656
  const findAppDirectory = (root) => Effect.gen(function* () {
5498
5657
  const fs = yield* FileSystem.FileSystem;
@@ -5527,13 +5686,46 @@ const runIosSimulatorBuild = (input) => Effect.gen(function* () {
5527
5686
  const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
5528
5687
  const configuration = iosProfile.buildConfiguration ?? "Release";
5529
5688
  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));
5689
+ const buildCmd = {
5690
+ command: "xcodebuild",
5691
+ args: [
5692
+ "-workspace",
5693
+ workspaceFilename,
5694
+ "-scheme",
5695
+ scheme,
5696
+ "-configuration",
5697
+ configuration,
5698
+ "-sdk",
5699
+ "iphonesimulator",
5700
+ "-destination",
5701
+ "generic/platform=iOS Simulator",
5702
+ "-derivedDataPath",
5703
+ derivedDataPath,
5704
+ "build",
5705
+ "CODE_SIGNING_ALLOWED=NO",
5706
+ "CODE_SIGNING_REQUIRED=NO",
5707
+ "CODE_SIGN_IDENTITY="
5708
+ ],
5709
+ cwd: iosDir,
5710
+ env: commandEnv
5711
+ };
5531
5712
  const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
5532
5713
  yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
5533
5714
  const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
5534
5715
  const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
5535
5716
  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");
5717
+ yield* runStep({
5718
+ command: "tar",
5719
+ args: [
5720
+ "-czf",
5721
+ archivePath,
5722
+ "-C",
5723
+ path.dirname(appDir),
5724
+ path.basename(appDir)
5725
+ ],
5726
+ cwd: projectRoot,
5727
+ env: commandEnv
5728
+ }, "tar simulator .app");
5537
5729
  const { sha256, byteSize } = yield* sha256File(archivePath);
5538
5730
  return {
5539
5731
  artifactPath: archivePath,
@@ -5636,7 +5828,23 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
5636
5828
  }))
5637
5829
  });
5638
5830
  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));
5831
+ const archiveCmd = {
5832
+ command: "xcodebuild",
5833
+ args: [
5834
+ "-workspace",
5835
+ workspaceFilename,
5836
+ "-scheme",
5837
+ scheme,
5838
+ "-configuration",
5839
+ configuration,
5840
+ "-archivePath",
5841
+ archivePath,
5842
+ "-allowProvisioningUpdates",
5843
+ "archive"
5844
+ ],
5845
+ cwd: iosDir,
5846
+ env: commandEnv
5847
+ };
5640
5848
  const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
5641
5849
  yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
5642
5850
  const exportOptionsPath = path.join(tempDir, "ExportOptions.plist");
@@ -5656,7 +5864,21 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
5656
5864
  }))
5657
5865
  }));
5658
5866
  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));
5867
+ const exportCmd = {
5868
+ command: "xcodebuild",
5869
+ args: [
5870
+ "-exportArchive",
5871
+ "-archivePath",
5872
+ archivePath,
5873
+ "-exportPath",
5874
+ exportPath,
5875
+ "-exportOptionsPlist",
5876
+ exportOptionsPath,
5877
+ "-allowProvisioningUpdates"
5878
+ ],
5879
+ cwd: iosDir,
5880
+ env: commandEnv
5881
+ };
5660
5882
  yield* formatter ? runStepFormatted(exportCmd, "xcodebuild exportArchive", formatter) : runStep(exportCmd, "xcodebuild exportArchive");
5661
5883
  yield* validateIosBuild({
5662
5884
  archivePath,
@@ -6241,7 +6463,7 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
6241
6463
  const warnOnGradleMismatch = (gradleConfig, expectedPackage) => {
6242
6464
  if (!gradleConfig?.applicationId) return Effect.void;
6243
6465
  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.`);
6466
+ return printWarn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
6245
6467
  };
6246
6468
  /**
6247
6469
  * Strip Groovy single-line and block comments.