@better-update/cli 0.40.2 → 0.41.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
@@ -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.40.2";
38
+ var version = "0.41.0";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -19942,20 +19942,98 @@ const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
19942
19942
  */
19943
19943
  const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
19944
19944
  /**
19945
- * Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
19946
- * `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
19947
- * is layered on top of the always-ignore baseline.
19945
+ * Rebase a single nested `.gitignore` line so it applies only within `prefix`
19946
+ * (the posix dir path + trailing slash) when folded into the workspace-root
19947
+ * matcher. Returns `undefined` for blanks/comments. Anchored patterns (a
19948
+ * leading or interior `/`) are prefixed as-is; unanchored ones (which match at
19949
+ * any depth) become `prefix**\/pat`. Negation and directory-only trailing
19950
+ * slashes are preserved.
19951
+ */
19952
+ const rebaseGitignoreLine = (prefix, rawLine) => {
19953
+ const line = rawLine.replace(/(?<!\\)\s+$/u, "");
19954
+ if (line === "" || line.startsWith("#")) return;
19955
+ const negate = line.startsWith("!");
19956
+ const unescaped = negate || line.startsWith(String.raw`\#`) || line.startsWith(String.raw`\!`) ? line.slice(1) : line;
19957
+ const hadLeadingSlash = unescaped.startsWith("/");
19958
+ const body = hadLeadingSlash ? unescaped.slice(1) : unescaped;
19959
+ if (body === "") return;
19960
+ const withoutTrailingSlash = body.endsWith("/") ? body.slice(0, -1) : body;
19961
+ const rebased = hadLeadingSlash || withoutTrailingSlash.includes("/") ? `${prefix}${body}` : `${prefix}**/${body}`;
19962
+ return negate ? `!${rebased}` : rebased;
19963
+ };
19964
+ /**
19965
+ * Rebase every line of a nested `.gitignore` to its own directory (`relDir`,
19966
+ * posix, relative to the workspace root) — mirroring how git scopes a
19967
+ * `.gitignore` to the dir it lives in.
19968
+ */
19969
+ const rebaseGitignore = (relDir, content) => {
19970
+ const prefix = `${relDir}/`;
19971
+ return content.split(/\r?\n/u).map((rawLine) => rebaseGitignoreLine(prefix, rawLine)).filter((pattern) => pattern !== void 0);
19972
+ };
19973
+ const safeReadDir = async (dir) => {
19974
+ try {
19975
+ return await promises.readdir(dir, { withFileTypes: true });
19976
+ } catch {
19977
+ return [];
19978
+ }
19979
+ };
19980
+ const safeReadText = async (file) => {
19981
+ try {
19982
+ return await promises.readFile(file, "utf8");
19983
+ } catch {
19984
+ return "";
19985
+ }
19986
+ };
19987
+ const isDirectory = async (file) => {
19988
+ try {
19989
+ return (await promises.lstat(file)).isDirectory();
19990
+ } catch {
19991
+ return false;
19992
+ }
19993
+ };
19994
+ /**
19995
+ * Fold every NESTED `.gitignore` under `workspaceRoot` into `ig`, each rebased
19996
+ * to its own directory (git scopes a `.gitignore` to the dir it lives in, and
19997
+ * EAS — which stages via git — honors that). The walk prunes any directory the
19998
+ * matcher-so-far already ignores, so it never descends into `node_modules`,
19999
+ * build outputs, or a subtree excluded by a shallower `.gitignore` — including
20000
+ * the entries a just-loaded nested `.gitignore` adds. The root `.gitignore` is
20001
+ * added by the caller, so the walk skips it.
20002
+ */
20003
+ const addNestedGitignores = (workspaceRoot, ig) => Effect.promise(async () => {
20004
+ const walk = async (absDir, relDir) => {
20005
+ const entries = await safeReadDir(absDir);
20006
+ if (relDir !== "" && entries.some((entry) => entry.isFile() && entry.name === ".gitignore")) {
20007
+ const content = await safeReadText(path.join(absDir, ".gitignore"));
20008
+ if (content !== "") ig.add(rebaseGitignore(relDir, content));
20009
+ }
20010
+ const subdirs = entries.filter((entry) => entry.isDirectory());
20011
+ for (const entry of subdirs) {
20012
+ const childRel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
20013
+ if (!ig.ignores(childRel) && !ig.ignores(`${childRel}/`)) await walk(path.join(absDir, entry.name), childRel);
20014
+ }
20015
+ };
20016
+ await walk(workspaceRoot, "");
20017
+ });
20018
+ /**
20019
+ * Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES every
20020
+ * `.gitignore` when present (matches EAS semantics); otherwise the root
20021
+ * `.gitignore` plus every NESTED `.gitignore` (git semantics) is layered on top
20022
+ * of the always-ignore baseline.
19948
20023
  *
19949
20024
  * When `includeNativeSource` is set, the native source dirs are re-included
19950
- * after the ignore files are applied, then their build outputs re-excluded, so
19951
- * a committed `ios/`/`android/` reaches staging intact.
20025
+ * before the nested scan (so the scan descends into committed `android/`/`ios/`
20026
+ * and folds in their nested ignores), then their build outputs re-excluded last,
20027
+ * so a committed `ios/`/`android/` reaches staging intact.
19952
20028
  */
19953
20029
  const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function* () {
19954
20030
  const fs = yield* FileSystem.FileSystem;
19955
20031
  const ig = ignore();
19956
20032
  ig.add([...ALWAYS_IGNORE]);
20033
+ const base = options.appRelPath === void 0 || options.appRelPath === "" ? "" : `${options.appRelPath}/`;
19957
20034
  const easignorePath = path.join(workspaceRoot, ".easignore");
19958
- if (yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false))) {
20035
+ const hasEasignore = yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false));
20036
+ if (hasEasignore) {
19959
20037
  const content = yield* fs.readFileString(easignorePath).pipe(Effect.orElseSucceed(() => ""));
19960
20038
  ig.add(content);
19961
20039
  } else {
@@ -19965,11 +20043,9 @@ const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function
19965
20043
  ig.add(content);
19966
20044
  }
19967
20045
  }
19968
- if (options.includeNativeSource === true) {
19969
- const base = options.appRelPath === void 0 || options.appRelPath === "" ? "" : `${options.appRelPath}/`;
19970
- ig.add([`!${base}android`, `!${base}ios`]);
19971
- ig.add(NATIVE_BUILD_OUTPUTS.map((entry) => `${base}${entry}`));
19972
- }
20046
+ if (options.includeNativeSource === true) ig.add([`!${base}android`, `!${base}ios`]);
20047
+ if (!hasEasignore) yield* addNestedGitignores(workspaceRoot, ig);
20048
+ if (options.includeNativeSource === true) ig.add(NATIVE_BUILD_OUTPUTS.map((entry) => `${base}${entry}`));
19973
20049
  return ig;
19974
20050
  });
19975
20051
  const copyProjectTree = (params) => Effect.tryPromise({
@@ -19977,11 +20053,12 @@ const copyProjectTree = (params) => Effect.tryPromise({
19977
20053
  await promises.cp(params.source, params.dest, {
19978
20054
  recursive: true,
19979
20055
  dereference: false,
19980
- filter: (src) => {
20056
+ filter: async (src) => {
19981
20057
  const rel = path.relative(params.source, src);
19982
20058
  if (rel === "") return true;
19983
20059
  const posixRel = rel.split(path.sep).join("/");
19984
- return !params.ig.ignores(posixRel);
20060
+ const isDir = await isDirectory(src);
20061
+ return !params.ig.ignores(isDir ? `${posixRel}/` : posixRel);
19985
20062
  }
19986
20063
  });
19987
20064
  },
@@ -23929,6 +24006,26 @@ const discoverSignedTargets = (options) => Effect.gen(function* () {
23929
24006
  if (results.length === 0) return yield* new XcodeProjectError({ message: `No signed native targets found in ${pbxprojPath} for configuration "${options.configurationName}".` });
23930
24007
  return results;
23931
24008
  });
24009
+ /**
24010
+ * Like {@link discoverSignedTargets}, but tolerant of a project that hasn't been
24011
+ * prebuilt: returns `undefined` when `iosDir` is absent or contains no
24012
+ * `.xcodeproj` (a managed Expo app before `expo prebuild`). A genuine parse
24013
+ * error in an existing project still surfaces. Used by `credentials configure`,
24014
+ * which runs outside a build and must degrade to the main bundle id from the
24015
+ * Expo config when the native project isn't generated yet.
24016
+ */
24017
+ const discoverSignedTargetsIfPresent = (options) => Effect.gen(function* () {
24018
+ const fs = yield* FileSystem.FileSystem;
24019
+ if (!(yield* fs.exists(options.iosDir).pipe(Effect.orElseSucceed(() => false)))) return;
24020
+ if (!(yield* fs.readDirectory(options.iosDir).pipe(Effect.orElseSucceed(() => []))).some((entry) => entry.endsWith(".xcodeproj"))) return;
24021
+ return yield* discoverSignedTargets(options);
24022
+ });
24023
+ /**
24024
+ * Pick the "main" target from a discovered set: the application product type if
24025
+ * present, otherwise the first signed target. Shared by the build pipeline and
24026
+ * `credentials configure` so both agree on which bundle id is primary.
24027
+ */
24028
+ const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
23932
24029
 
23933
24030
  //#endregion
23934
24031
  //#region src/lib/xcpretty-formatter.ts
@@ -24160,7 +24257,6 @@ const installProfileForTarget = (target, profileByBundle) => {
24160
24257
  installed
24161
24258
  })));
24162
24259
  };
24163
- const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
24164
24260
  const runIosDeviceBuild = (input) => Effect.gen(function* () {
24165
24261
  const { api, tempDir, projectRoot, iosProfile, envVars } = input;
24166
24262
  const runtime = yield* CliRuntime;
@@ -28347,6 +28443,12 @@ const accessCommand = defineCommand({
28347
28443
 
28348
28444
  //#endregion
28349
28445
  //#region src/commands/credentials/configure.ts
28446
+ /**
28447
+ * Xcode build configuration whose `PRODUCT_BUNDLE_IDENTIFIER`s the configure
28448
+ * wizard reads when discovering signed targets. Distribution signing always
28449
+ * runs against the Release configuration.
28450
+ */
28451
+ const IOS_DISCOVERY_CONFIGURATION = "Release";
28350
28452
  const bindAndroidFcmGsa = (api, input) => Effect.gen(function* () {
28351
28453
  const app = (yield* api.androidApplicationIdentifiers.list({ path: { projectId: input.projectId } })).items.find((entry) => entry.packageName === input.applicationIdentifier);
28352
28454
  if (app === void 0) return yield* new MissingCredentialsError({
@@ -28441,6 +28543,68 @@ const configureIos = (args) => Effect.gen(function* () {
28441
28543
  yield* printHuman("Run with --rebind to switch certificate, profile, or ASC key.");
28442
28544
  yield* printHuman("Run with --bind-push-key <id> / --bind-asc-key <id> to update a single binding.");
28443
28545
  });
28546
+ const configureIosTargets = (args) => Effect.gen(function* () {
28547
+ yield* printHuman(`Configuring iOS credentials for ${args.targets.length} signed target(s) (${args.distribution})...`);
28548
+ yield* Effect.forEach(args.targets, (target) => Effect.gen(function* () {
28549
+ const input = {
28550
+ projectId: args.projectId,
28551
+ bundleIdentifier: target.bundleId,
28552
+ distribution: args.distribution
28553
+ };
28554
+ yield* printHuman("");
28555
+ yield* printHuman(`${target.targetName} (${target.bundleId})`);
28556
+ yield* ensureIosCredentials(args.api, input, { freezeCredentials: false });
28557
+ yield* showIosBinding(args.api, input);
28558
+ }), { concurrency: 1 });
28559
+ yield* printHuman("");
28560
+ yield* printHuman("Run with --bundle <id> --rebind to switch a target's certificate, profile, or ASC key.");
28561
+ yield* printHuman("Run with --bundle <id> --bind-push-key <id> / --bind-asc-key <id> to update a single binding.");
28562
+ });
28563
+ const runConfigureIos = (args) => Effect.gen(function* () {
28564
+ const { api, projectId, root, distribution } = args;
28565
+ const singleBundleOnly = args.bundle !== void 0 || args.rebind || args.bindPushKey !== void 0 || args.bindAscKey !== void 0;
28566
+ const iosMeta = yield* readAppMetaOptional(root, "ios");
28567
+ if (!singleBundleOnly) {
28568
+ const targets = yield* discoverSignedTargetsIfPresent({
28569
+ iosDir: path.join(root, "ios"),
28570
+ configurationName: IOS_DISCOVERY_CONFIGURATION
28571
+ });
28572
+ const main = targets === void 0 ? void 0 : pickMainTarget(targets);
28573
+ if (targets !== void 0 && main !== void 0) {
28574
+ yield* configureIosTargets({
28575
+ api,
28576
+ projectId,
28577
+ distribution,
28578
+ targets
28579
+ });
28580
+ return {
28581
+ platform: "ios",
28582
+ projectId,
28583
+ distribution,
28584
+ bundleIdentifier: main.bundleId,
28585
+ bundleIdentifiers: targets.map((target) => target.bundleId)
28586
+ };
28587
+ }
28588
+ if (iosMeta.bundleId !== void 0) yield* printHuman("No prebuilt iOS project found — configuring the main bundle only. Run `expo prebuild` (or a build) once so app-extension targets are discovered and configured.");
28589
+ }
28590
+ const bundleIdentifier = args.bundle ?? iosMeta.bundleId ?? (yield* promptText("iOS bundle identifier"));
28591
+ yield* configureIos({
28592
+ api,
28593
+ projectId,
28594
+ bundleIdentifier,
28595
+ distribution,
28596
+ rebind: args.rebind,
28597
+ bindPushKey: args.bindPushKey,
28598
+ bindAscKey: args.bindAscKey
28599
+ });
28600
+ return {
28601
+ platform: "ios",
28602
+ projectId,
28603
+ distribution,
28604
+ bundleIdentifier,
28605
+ bundleIdentifiers: [bundleIdentifier]
28606
+ };
28607
+ });
28444
28608
  const configureCommand$1 = defineCommand({
28445
28609
  meta: {
28446
28610
  name: "configure",
@@ -28454,7 +28618,7 @@ const configureCommand$1 = defineCommand({
28454
28618
  },
28455
28619
  bundle: {
28456
28620
  type: "string",
28457
- description: "iOS bundle identifier (defaults to app.json)"
28621
+ description: "iOS bundle identifier to scope to a single target (defaults to configuring every signed target — main app + extensions — discovered from the Xcode project)"
28458
28622
  },
28459
28623
  "android-package": {
28460
28624
  type: "string",
@@ -28498,25 +28662,16 @@ const configureCommand$1 = defineCommand({
28498
28662
  }, {
28499
28663
  value: "android",
28500
28664
  label: "Android"
28501
- }]))) === "ios") {
28502
- const iosMeta = yield* readAppMetaOptional(root, "ios");
28503
- const bundleIdentifier = args.bundle ?? iosMeta.bundleId ?? (yield* promptText("iOS bundle identifier"));
28504
- yield* configureIos({
28505
- api,
28506
- projectId,
28507
- bundleIdentifier,
28508
- distribution: args.distribution,
28509
- rebind: args.rebind ?? false,
28510
- bindPushKey: args["bind-push-key"],
28511
- bindAscKey: args["bind-asc-key"]
28512
- });
28513
- return {
28514
- platform: "ios",
28515
- projectId,
28516
- bundleIdentifier,
28517
- distribution: args.distribution
28518
- };
28519
- }
28665
+ }]))) === "ios") return yield* runConfigureIos({
28666
+ api,
28667
+ projectId,
28668
+ root,
28669
+ bundle: args.bundle,
28670
+ distribution: args.distribution,
28671
+ rebind: args.rebind ?? false,
28672
+ bindPushKey: args["bind-push-key"],
28673
+ bindAscKey: args["bind-asc-key"]
28674
+ });
28520
28675
  const androidMeta = yield* readAppMetaOptional(root, "android");
28521
28676
  const applicationIdentifier = args["android-package"] ?? androidMeta.androidPackage ?? (yield* promptText("Android application identifier"));
28522
28677
  yield* configureAndroid({