@better-update/cli 0.40.2 → 0.41.1
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 +235 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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.
|
|
38
|
+
var version = "0.41.1";
|
|
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
|
-
*
|
|
19946
|
-
*
|
|
19947
|
-
*
|
|
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
|
-
*
|
|
19951
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
19970
|
-
|
|
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
|
-
|
|
20060
|
+
const isDir = await isDirectory(src);
|
|
20061
|
+
return !params.ig.ignores(isDir ? `${posixRel}/` : posixRel);
|
|
19985
20062
|
}
|
|
19986
20063
|
});
|
|
19987
20064
|
},
|
|
@@ -20004,6 +20081,50 @@ const initGitRepo = (stagingRoot) => Effect.tryPromise({
|
|
|
20004
20081
|
catch: (cause) => new StagingError({ message: `Failed to init git repo in staging dir: ${formatCause(cause)}` })
|
|
20005
20082
|
}).pipe(Effect.asVoid);
|
|
20006
20083
|
/**
|
|
20084
|
+
* Snapshot the staged tree as a single commit so its working tree reads CLEAN.
|
|
20085
|
+
*
|
|
20086
|
+
* EAS stages via `git clone` + checkout, so the tree it hands to
|
|
20087
|
+
* `expo prebuild --clean` is a clean checkout. Our `cp` + `git init` leaves
|
|
20088
|
+
* every staged file UNTRACKED, which `expo prebuild`'s git check reads as
|
|
20089
|
+
* "dirty" (`git status --porcelain` is non-empty). Because the native build
|
|
20090
|
+
* runs inside a PTY, Expo's `isInteractive()` is true even with no real
|
|
20091
|
+
* controlling TTY, so it prompts `Continue with uncommitted changes?` and then
|
|
20092
|
+
* blocks on stdin we never write — hanging CI / backgrounded / piped builds.
|
|
20093
|
+
*
|
|
20094
|
+
* Committing once here makes `git status` clean, so Expo's check passes on
|
|
20095
|
+
* EVERY Expo version — the `EXPO_NO_GIT_STATUS` env gate the build sets only
|
|
20096
|
+
* exists in newer Expo — with no global `CI=1` side effects. The real
|
|
20097
|
+
* dirty-tree decision already ran against the user's *actual* working tree in
|
|
20098
|
+
* `ensureRepoClean` (honoring `--allow-dirty`). Best-effort: hooks are disabled
|
|
20099
|
+
* and a failure is non-fatal — the build proceeds and `EXPO_NO_GIT_STATUS`
|
|
20100
|
+
* still covers newer Expo.
|
|
20101
|
+
*/
|
|
20102
|
+
const commitStagingSnapshot = (stagingRoot) => Effect.tryPromise(async () => {
|
|
20103
|
+
const run = async (args) => execFileAsync$1("git", [...args], {
|
|
20104
|
+
cwd: stagingRoot,
|
|
20105
|
+
env: {
|
|
20106
|
+
...process.env,
|
|
20107
|
+
LEFTHOOK: "0",
|
|
20108
|
+
HUSKY: "0"
|
|
20109
|
+
}
|
|
20110
|
+
});
|
|
20111
|
+
await run(["add", "-A"]);
|
|
20112
|
+
await run([
|
|
20113
|
+
"-c",
|
|
20114
|
+
"user.name=better-update",
|
|
20115
|
+
"-c",
|
|
20116
|
+
"user.email=build@better-update.dev",
|
|
20117
|
+
"-c",
|
|
20118
|
+
"commit.gpgsign=false",
|
|
20119
|
+
"commit",
|
|
20120
|
+
"--no-verify",
|
|
20121
|
+
"--allow-empty",
|
|
20122
|
+
"-q",
|
|
20123
|
+
"-m",
|
|
20124
|
+
"better-update staging snapshot"
|
|
20125
|
+
]);
|
|
20126
|
+
}).pipe(Effect.ignore);
|
|
20127
|
+
/**
|
|
20007
20128
|
* Install args per package manager, frozen-lockfile variants matching EAS
|
|
20008
20129
|
* (`bun install --frozen-lockfile` / `npm ci --include=dev` / etc.) so the
|
|
20009
20130
|
* staged install resolves exactly what the user's lockfile pins.
|
|
@@ -20059,6 +20180,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
20059
20180
|
env: commandEnv
|
|
20060
20181
|
});
|
|
20061
20182
|
} else yield* printHuman("No package.json at the staging root — skipping dependency install.");
|
|
20183
|
+
yield* commitStagingSnapshot(stagingRoot);
|
|
20062
20184
|
return {
|
|
20063
20185
|
stagingRoot,
|
|
20064
20186
|
projectRoot,
|
|
@@ -23929,6 +24051,26 @@ const discoverSignedTargets = (options) => Effect.gen(function* () {
|
|
|
23929
24051
|
if (results.length === 0) return yield* new XcodeProjectError({ message: `No signed native targets found in ${pbxprojPath} for configuration "${options.configurationName}".` });
|
|
23930
24052
|
return results;
|
|
23931
24053
|
});
|
|
24054
|
+
/**
|
|
24055
|
+
* Like {@link discoverSignedTargets}, but tolerant of a project that hasn't been
|
|
24056
|
+
* prebuilt: returns `undefined` when `iosDir` is absent or contains no
|
|
24057
|
+
* `.xcodeproj` (a managed Expo app before `expo prebuild`). A genuine parse
|
|
24058
|
+
* error in an existing project still surfaces. Used by `credentials configure`,
|
|
24059
|
+
* which runs outside a build and must degrade to the main bundle id from the
|
|
24060
|
+
* Expo config when the native project isn't generated yet.
|
|
24061
|
+
*/
|
|
24062
|
+
const discoverSignedTargetsIfPresent = (options) => Effect.gen(function* () {
|
|
24063
|
+
const fs = yield* FileSystem.FileSystem;
|
|
24064
|
+
if (!(yield* fs.exists(options.iosDir).pipe(Effect.orElseSucceed(() => false)))) return;
|
|
24065
|
+
if (!(yield* fs.readDirectory(options.iosDir).pipe(Effect.orElseSucceed(() => []))).some((entry) => entry.endsWith(".xcodeproj"))) return;
|
|
24066
|
+
return yield* discoverSignedTargets(options);
|
|
24067
|
+
});
|
|
24068
|
+
/**
|
|
24069
|
+
* Pick the "main" target from a discovered set: the application product type if
|
|
24070
|
+
* present, otherwise the first signed target. Shared by the build pipeline and
|
|
24071
|
+
* `credentials configure` so both agree on which bundle id is primary.
|
|
24072
|
+
*/
|
|
24073
|
+
const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
|
|
23932
24074
|
|
|
23933
24075
|
//#endregion
|
|
23934
24076
|
//#region src/lib/xcpretty-formatter.ts
|
|
@@ -24160,7 +24302,6 @@ const installProfileForTarget = (target, profileByBundle) => {
|
|
|
24160
24302
|
installed
|
|
24161
24303
|
})));
|
|
24162
24304
|
};
|
|
24163
|
-
const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
|
|
24164
24305
|
const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
24165
24306
|
const { api, tempDir, projectRoot, iosProfile, envVars } = input;
|
|
24166
24307
|
const runtime = yield* CliRuntime;
|
|
@@ -28347,6 +28488,12 @@ const accessCommand = defineCommand({
|
|
|
28347
28488
|
|
|
28348
28489
|
//#endregion
|
|
28349
28490
|
//#region src/commands/credentials/configure.ts
|
|
28491
|
+
/**
|
|
28492
|
+
* Xcode build configuration whose `PRODUCT_BUNDLE_IDENTIFIER`s the configure
|
|
28493
|
+
* wizard reads when discovering signed targets. Distribution signing always
|
|
28494
|
+
* runs against the Release configuration.
|
|
28495
|
+
*/
|
|
28496
|
+
const IOS_DISCOVERY_CONFIGURATION = "Release";
|
|
28350
28497
|
const bindAndroidFcmGsa = (api, input) => Effect.gen(function* () {
|
|
28351
28498
|
const app = (yield* api.androidApplicationIdentifiers.list({ path: { projectId: input.projectId } })).items.find((entry) => entry.packageName === input.applicationIdentifier);
|
|
28352
28499
|
if (app === void 0) return yield* new MissingCredentialsError({
|
|
@@ -28441,6 +28588,68 @@ const configureIos = (args) => Effect.gen(function* () {
|
|
|
28441
28588
|
yield* printHuman("Run with --rebind to switch certificate, profile, or ASC key.");
|
|
28442
28589
|
yield* printHuman("Run with --bind-push-key <id> / --bind-asc-key <id> to update a single binding.");
|
|
28443
28590
|
});
|
|
28591
|
+
const configureIosTargets = (args) => Effect.gen(function* () {
|
|
28592
|
+
yield* printHuman(`Configuring iOS credentials for ${args.targets.length} signed target(s) (${args.distribution})...`);
|
|
28593
|
+
yield* Effect.forEach(args.targets, (target) => Effect.gen(function* () {
|
|
28594
|
+
const input = {
|
|
28595
|
+
projectId: args.projectId,
|
|
28596
|
+
bundleIdentifier: target.bundleId,
|
|
28597
|
+
distribution: args.distribution
|
|
28598
|
+
};
|
|
28599
|
+
yield* printHuman("");
|
|
28600
|
+
yield* printHuman(`${target.targetName} (${target.bundleId})`);
|
|
28601
|
+
yield* ensureIosCredentials(args.api, input, { freezeCredentials: false });
|
|
28602
|
+
yield* showIosBinding(args.api, input);
|
|
28603
|
+
}), { concurrency: 1 });
|
|
28604
|
+
yield* printHuman("");
|
|
28605
|
+
yield* printHuman("Run with --bundle <id> --rebind to switch a target's certificate, profile, or ASC key.");
|
|
28606
|
+
yield* printHuman("Run with --bundle <id> --bind-push-key <id> / --bind-asc-key <id> to update a single binding.");
|
|
28607
|
+
});
|
|
28608
|
+
const runConfigureIos = (args) => Effect.gen(function* () {
|
|
28609
|
+
const { api, projectId, root, distribution } = args;
|
|
28610
|
+
const singleBundleOnly = args.bundle !== void 0 || args.rebind || args.bindPushKey !== void 0 || args.bindAscKey !== void 0;
|
|
28611
|
+
const iosMeta = yield* readAppMetaOptional(root, "ios");
|
|
28612
|
+
if (!singleBundleOnly) {
|
|
28613
|
+
const targets = yield* discoverSignedTargetsIfPresent({
|
|
28614
|
+
iosDir: path.join(root, "ios"),
|
|
28615
|
+
configurationName: IOS_DISCOVERY_CONFIGURATION
|
|
28616
|
+
});
|
|
28617
|
+
const main = targets === void 0 ? void 0 : pickMainTarget(targets);
|
|
28618
|
+
if (targets !== void 0 && main !== void 0) {
|
|
28619
|
+
yield* configureIosTargets({
|
|
28620
|
+
api,
|
|
28621
|
+
projectId,
|
|
28622
|
+
distribution,
|
|
28623
|
+
targets
|
|
28624
|
+
});
|
|
28625
|
+
return {
|
|
28626
|
+
platform: "ios",
|
|
28627
|
+
projectId,
|
|
28628
|
+
distribution,
|
|
28629
|
+
bundleIdentifier: main.bundleId,
|
|
28630
|
+
bundleIdentifiers: targets.map((target) => target.bundleId)
|
|
28631
|
+
};
|
|
28632
|
+
}
|
|
28633
|
+
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.");
|
|
28634
|
+
}
|
|
28635
|
+
const bundleIdentifier = args.bundle ?? iosMeta.bundleId ?? (yield* promptText("iOS bundle identifier"));
|
|
28636
|
+
yield* configureIos({
|
|
28637
|
+
api,
|
|
28638
|
+
projectId,
|
|
28639
|
+
bundleIdentifier,
|
|
28640
|
+
distribution,
|
|
28641
|
+
rebind: args.rebind,
|
|
28642
|
+
bindPushKey: args.bindPushKey,
|
|
28643
|
+
bindAscKey: args.bindAscKey
|
|
28644
|
+
});
|
|
28645
|
+
return {
|
|
28646
|
+
platform: "ios",
|
|
28647
|
+
projectId,
|
|
28648
|
+
distribution,
|
|
28649
|
+
bundleIdentifier,
|
|
28650
|
+
bundleIdentifiers: [bundleIdentifier]
|
|
28651
|
+
};
|
|
28652
|
+
});
|
|
28444
28653
|
const configureCommand$1 = defineCommand({
|
|
28445
28654
|
meta: {
|
|
28446
28655
|
name: "configure",
|
|
@@ -28454,7 +28663,7 @@ const configureCommand$1 = defineCommand({
|
|
|
28454
28663
|
},
|
|
28455
28664
|
bundle: {
|
|
28456
28665
|
type: "string",
|
|
28457
|
-
description: "iOS bundle identifier (defaults to app
|
|
28666
|
+
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
28667
|
},
|
|
28459
28668
|
"android-package": {
|
|
28460
28669
|
type: "string",
|
|
@@ -28498,25 +28707,16 @@ const configureCommand$1 = defineCommand({
|
|
|
28498
28707
|
}, {
|
|
28499
28708
|
value: "android",
|
|
28500
28709
|
label: "Android"
|
|
28501
|
-
}]))) === "ios") {
|
|
28502
|
-
|
|
28503
|
-
|
|
28504
|
-
|
|
28505
|
-
|
|
28506
|
-
|
|
28507
|
-
|
|
28508
|
-
|
|
28509
|
-
|
|
28510
|
-
|
|
28511
|
-
bindAscKey: args["bind-asc-key"]
|
|
28512
|
-
});
|
|
28513
|
-
return {
|
|
28514
|
-
platform: "ios",
|
|
28515
|
-
projectId,
|
|
28516
|
-
bundleIdentifier,
|
|
28517
|
-
distribution: args.distribution
|
|
28518
|
-
};
|
|
28519
|
-
}
|
|
28710
|
+
}]))) === "ios") return yield* runConfigureIos({
|
|
28711
|
+
api,
|
|
28712
|
+
projectId,
|
|
28713
|
+
root,
|
|
28714
|
+
bundle: args.bundle,
|
|
28715
|
+
distribution: args.distribution,
|
|
28716
|
+
rebind: args.rebind ?? false,
|
|
28717
|
+
bindPushKey: args["bind-push-key"],
|
|
28718
|
+
bindAscKey: args["bind-asc-key"]
|
|
28719
|
+
});
|
|
28520
28720
|
const androidMeta = yield* readAppMetaOptional(root, "android");
|
|
28521
28721
|
const applicationIdentifier = args["android-package"] ?? androidMeta.androidPackage ?? (yield* promptText("Android application identifier"));
|
|
28522
28722
|
yield* configureAndroid({
|