@better-update/cli 0.47.2 → 0.47.4

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
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import process$1 from "node:process";
10
10
  import AppleUtils from "@expo/apple-utils";
11
11
  import { autocomplete, cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
12
- import { open, readFile, writeFile } from "node:fs/promises";
12
+ import { mkdtemp, open, readFile, rm, writeFile } from "node:fs/promises";
13
13
  import { X509Certificate, createHash, createSign, createVerify, randomBytes, randomUUID } from "node:crypto";
14
14
  import { accessSync, chmodSync, constants, createReadStream, promises } from "node:fs";
15
15
  import { Entry } from "@napi-rs/keyring";
@@ -35,7 +35,7 @@ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
35
35
 
36
36
  //#endregion
37
37
  //#region package.json
38
- var version = "0.47.2";
38
+ var version = "0.47.4";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -21280,6 +21280,66 @@ const commitEdit = (params) => callJsonRaw({
21280
21280
  label: "edits.commit"
21281
21281
  });
21282
21282
 
21283
+ //#endregion
21284
+ //#region src/lib/altool.ts
21285
+ /**
21286
+ * Thin wrapper around `xcrun altool` for App Store delivery: run it without
21287
+ * throwing (failures come back as an {@link ExecResult} with a non-zero exit), and
21288
+ * extract the real failure reason from altool's `--output-format xml` output.
21289
+ */
21290
+ const execFileAsync = promisify(execFile);
21291
+ const ExecErrorSchema = Schema.Struct({
21292
+ code: Schema.optional(Schema.Number),
21293
+ stdout: Schema.optional(Schema.String),
21294
+ stderr: Schema.optional(Schema.String)
21295
+ });
21296
+ const runAltool = (args, extraEnv) => Effect.tryPromise({
21297
+ try: async () => {
21298
+ const options = extraEnv ? {
21299
+ encoding: "utf8",
21300
+ env: {
21301
+ ...process.env,
21302
+ ...extraEnv
21303
+ }
21304
+ } : { encoding: "utf8" };
21305
+ const { stdout, stderr } = await execFileAsync("xcrun", ["altool", ...args], options);
21306
+ return {
21307
+ exitCode: 0,
21308
+ stdout,
21309
+ stderr
21310
+ };
21311
+ },
21312
+ catch: (error) => {
21313
+ const parsed = Schema.decodeUnknownSync(ExecErrorSchema, { onExcessProperty: "ignore" })(typeof error === "object" && error !== null ? error : {});
21314
+ const stdout = parsed.stdout ?? "";
21315
+ const stderr = parsed.stderr ?? String(error);
21316
+ return {
21317
+ exitCode: parsed.code ?? 1,
21318
+ stdout,
21319
+ stderr: stderr === "" ? String(error) : stderr
21320
+ };
21321
+ }
21322
+ }).pipe(Effect.catchAll((result) => Effect.succeed(result)));
21323
+ const unescapeXml = (value) => value.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&amp;", "&");
21324
+ /**
21325
+ * altool with `--output-format xml` writes the real failure detail to a stdout
21326
+ * plist (`product-errors`), leaving stderr with only a generic
21327
+ * "UPLOAD FAILED … ExitFailure (N)" banner. Pull the human-readable messages so
21328
+ * the surfaced error names the actual cause (asset validation, export compliance,
21329
+ * a duplicate build number, a missing app record…).
21330
+ */
21331
+ const extractAltoolErrors = (xml) => [...xml.matchAll(/<key>message<\/key>\s*<string>(?<message>[\s\S]*?)<\/string>/gu)].flatMap((match) => {
21332
+ const message = match.groups?.["message"];
21333
+ return message === void 0 ? [] : [unescapeXml(message).trim()];
21334
+ });
21335
+ /** Best human-readable altool failure detail: parsed product-errors, else raw streams. */
21336
+ const altoolFailureDetail = (result) => {
21337
+ const messages = extractAltoolErrors(result.stdout);
21338
+ if (messages.length > 0) return messages.join("; ");
21339
+ const combined = `${result.stdout}\n${result.stderr}`.trim();
21340
+ return combined.length > 0 ? combined : "no output";
21341
+ };
21342
+
21283
21343
  //#endregion
21284
21344
  //#region src/lib/apple-asc-connect.ts
21285
21345
  /**
@@ -21537,32 +21597,6 @@ const applyTestFlightConfig = (inputs) => Effect.gen(function* () {
21537
21597
 
21538
21598
  //#endregion
21539
21599
  //#region src/application/submit-flow.ts
21540
- const execFileAsync = promisify(execFile);
21541
- const ExecErrorSchema = Schema.Struct({
21542
- code: Schema.optional(Schema.Number),
21543
- stdout: Schema.optional(Schema.String),
21544
- stderr: Schema.optional(Schema.String)
21545
- });
21546
- const runAltool = (args) => Effect.tryPromise({
21547
- try: async () => {
21548
- const { stdout, stderr } = await execFileAsync("xcrun", ["altool", ...args]);
21549
- return {
21550
- exitCode: 0,
21551
- stdout,
21552
- stderr
21553
- };
21554
- },
21555
- catch: (error) => {
21556
- const parsed = Schema.decodeUnknownSync(ExecErrorSchema, { onExcessProperty: "ignore" })(typeof error === "object" && error !== null ? error : {});
21557
- const stdout = parsed.stdout ?? "";
21558
- const stderr = parsed.stderr ?? String(error);
21559
- return {
21560
- exitCode: parsed.code ?? 1,
21561
- stdout,
21562
- stderr: stderr === "" ? String(error) : stderr
21563
- };
21564
- }
21565
- }).pipe(Effect.catchAll((result) => Effect.succeed(result)));
21566
21600
  var CliSubmitError = class extends Schema.TaggedError()("CliSubmitError", {
21567
21601
  code: Schema.String,
21568
21602
  message: Schema.String
@@ -21695,11 +21729,23 @@ const resolveAscUploadCredentials = (params) => Effect.gen(function* () {
21695
21729
  p8Pem: creds.p8Pem
21696
21730
  })), Effect.catchAll((error) => printHuman(`Could not prepare ASC API key ${credsKeyId} (${messageOf(error)}).`).pipe(Effect.as(null))));
21697
21731
  });
21698
- /** `altool` reads the API key from `--apiKeyDir`; write the decrypted `.p8` there. */
21699
- const writeP8ForAltool = (credentials) => Effect.gen(function* () {
21700
- const target = path.join(tmpdir(), `better-update-submit-AuthKey_${credentials.keyId}.p8`);
21701
- yield* Effect.promise(async () => writeFile(target, credentials.p8Pem, "utf8"));
21702
- return target;
21732
+ /**
21733
+ * `altool --apiKey <id>` searches for a file named *exactly* `AuthKey_<id>.p8` in
21734
+ * the standard `private_keys` dirs plus `$API_PRIVATE_KEYS_DIR`. Write the decrypted
21735
+ * `.p8` under that exact name into a fresh private temp dir and return the dir so the
21736
+ * caller can point `API_PRIVATE_KEYS_DIR` at it (and remove it afterward — it holds
21737
+ * the unencrypted signing key).
21738
+ */
21739
+ const writeP8KeyDir = (credentials) => Effect.promise(async () => {
21740
+ const dir = await mkdtemp(path.join(tmpdir(), "better-update-asc-"));
21741
+ await writeFile(path.join(dir, `AuthKey_${credentials.keyId}.p8`), credentials.p8Pem, "utf8");
21742
+ return dir;
21743
+ });
21744
+ const removeKeyDir = (dir) => Effect.promise(async () => {
21745
+ await rm(dir, {
21746
+ recursive: true,
21747
+ force: true
21748
+ });
21703
21749
  });
21704
21750
  const baseAltoolArgs = (ipaPath) => [
21705
21751
  "--upload-app",
@@ -21724,15 +21770,12 @@ const buildAltoolArgs = (params) => Effect.gen(function* () {
21724
21770
  code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
21725
21771
  message: "ASC API key is required for an asc-api-key upload but was not resolved."
21726
21772
  });
21727
- const p8Path = yield* writeP8ForAltool(params.ascCredentials);
21728
21773
  return [
21729
21774
  ...baseAltoolArgs(params.ipaPath),
21730
21775
  "--apiKey",
21731
21776
  params.ascCredentials.keyId,
21732
21777
  "--apiIssuer",
21733
- params.ascCredentials.issuerId,
21734
- "--apiKeyDir",
21735
- path.dirname(p8Path)
21778
+ params.ascCredentials.issuerId
21736
21779
  ];
21737
21780
  });
21738
21781
  const runIosSubmit = (inputs) => Effect.gen(function* () {
@@ -21758,12 +21801,12 @@ const runIosSubmit = (inputs) => Effect.gen(function* () {
21758
21801
  ipaPath
21759
21802
  });
21760
21803
  yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "IN_PROGRESS" });
21761
- const result = yield* runAltool(altoolArgs);
21804
+ const result = inputs.auth.kind === "asc-api-key" && ascCredentials !== null ? yield* Effect.acquireUseRelease(writeP8KeyDir(ascCredentials), (keyDir) => runAltool(altoolArgs, { API_PRIVATE_KEYS_DIR: keyDir }), (keyDir) => removeKeyDir(keyDir)) : yield* runAltool(altoolArgs);
21762
21805
  if (result.exitCode !== 0) {
21763
21806
  yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
21764
21807
  status: "ERRORED",
21765
21808
  errorCode: "SUBMISSION_SERVICE_IOS_ALTOOL_FAILED",
21766
- errorMessage: `xcrun altool exited ${String(result.exitCode)}: ${result.stderr}`
21809
+ errorMessage: `xcrun altool exited ${String(result.exitCode)}: ${altoolFailureDetail(result)}`
21767
21810
  });
21768
21811
  return { status: "ERRORED" };
21769
21812
  }