mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
1069
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4eb9ff37b | ||
|
|
ddfe541af2 | ||
|
|
cde1ab36d2 | ||
|
|
087074d831 | ||
|
|
365e2d7e07 | ||
|
|
1691f3271f | ||
|
|
c4adf7056e | ||
|
|
0e7f326de2 | ||
|
|
bbc8b1ac76 | ||
|
|
3db2a69183 | ||
|
|
51155bb2b4 | ||
|
|
30ec5f967b | ||
|
|
89dda20149 | ||
|
|
b591698c31 | ||
|
|
66543b658b | ||
|
|
4769048578 | ||
|
|
64c1e47674 | ||
|
|
be3a86abda | ||
|
|
eb2ca117fc | ||
|
|
5043385077 | ||
|
|
bb4c08a52a | ||
|
|
8b75057fa1 | ||
|
|
80cc849137 | ||
|
|
2d407ea9d4 | ||
|
|
91456d1c65 | ||
|
|
757a0ccad7 | ||
|
|
4b7cac0392 | ||
|
|
8ff8022b3f | ||
|
|
a2acea9fe9 | ||
|
|
9abdfcfdbe | ||
|
|
696f9dde13 | ||
|
|
1e73c8c96f | ||
|
|
1b02a67d88 | ||
|
|
d0f158a546 | ||
|
|
16a2dc681b | ||
|
|
73800252ae | ||
|
|
e7c4ba1f54 | ||
|
|
84cddaf0e1 | ||
|
|
896f7f0385 | ||
|
|
7f31ea6a2d | ||
|
|
3ef397f96c | ||
|
|
680a684a8e | ||
|
|
1eae0c75ee | ||
|
|
483e0f2dca | ||
|
|
6ee843e41c | ||
|
|
5402ebdc5c | ||
|
|
1b5119d818 | ||
|
|
8a82365680 | ||
|
|
9c55621719 | ||
|
|
1afc3093ec | ||
|
|
790c3d776e | ||
|
|
be84ffd62a | ||
|
|
fa812f448f | ||
|
|
d09161b745 | ||
|
|
653f26257b | ||
|
|
b1e874966c | ||
|
|
315abf0a03 | ||
|
|
89a9255929 | ||
|
|
cae0ba21eb | ||
|
|
ed52e291cb | ||
|
|
6db7ac617b | ||
|
|
1dd9614dbe | ||
|
|
67c58a75d9 | ||
|
|
5b3e4cb3e1 | ||
|
|
caf257ce9c | ||
|
|
31122623fa | ||
|
|
eb36dd0fef | ||
|
|
bb0723354f | ||
|
|
1b9feed040 | ||
|
|
0220ca17a6 | ||
|
|
74cec12c23 | ||
|
|
e807fc105d | ||
|
|
ae30c4c7db | ||
|
|
35b9fda122 | ||
|
|
8d010ad7e8 | ||
|
|
d7181478c8 | ||
|
|
70ed1a482a | ||
|
|
920e5ee8a2 | ||
|
|
f334f2c019 | ||
|
|
b145a7d98c | ||
|
|
3c750b60c0 | ||
|
|
b3a9f1af70 | ||
|
|
39d8ab65d1 | ||
|
|
1ff2274d8d | ||
|
|
5ed4b65133 | ||
|
|
10aca5208f | ||
|
|
b2da80d5ea | ||
|
|
b9457d71f9 | ||
|
|
5fd3149a95 | ||
|
|
5f83724833 | ||
|
|
7df558d713 | ||
|
|
543e8ce26d | ||
|
|
355fada383 | ||
|
|
b6625a4cba | ||
|
|
c1ef20afab | ||
|
|
d8321e1f06 | ||
|
|
d8906e085e | ||
|
|
3b823865d3 | ||
|
|
a29ed19653 | ||
|
|
e70ecaa9b5 | ||
|
|
9886fcb3ef | ||
|
|
4f5739b2dc | ||
|
|
c7a40b06a5 | ||
|
|
3f88dc8690 | ||
|
|
874db1c125 | ||
|
|
91a147b40b | ||
|
|
cc4553b318 | ||
|
|
dd2303dbb2 | ||
|
|
967b5d9985 | ||
|
|
8439cd2141 | ||
|
|
f46063cca5 | ||
|
|
c0cfa93b69 | ||
|
|
7d6f620f91 | ||
|
|
c973a1f603 | ||
|
|
1b8f60fad4 | ||
|
|
7a30de9ad7 | ||
|
|
4247b65e09 | ||
|
|
8c388d5173 | ||
|
|
fce945679f | ||
|
|
7772e199b4 | ||
|
|
755e105252 | ||
|
|
611d0c98ee | ||
|
|
949a90187b | ||
|
|
bb14d9255a | ||
|
|
98e35926ff | ||
|
|
cd433405cb | ||
|
|
3ad54b0fc9 | ||
|
|
5184117ed6 | ||
|
|
868df87146 | ||
|
|
2f0cc2361b | ||
|
|
4be6c51418 | ||
|
|
99c4c618c9 | ||
|
|
742a1a05e5 | ||
|
|
6f806cabb1 | ||
|
|
4c84794a31 | ||
|
|
a1ab5a4fb5 | ||
|
|
dc11c1c487 | ||
|
|
805865356c | ||
|
|
5fc0757fb0 | ||
|
|
2c6d92745f | ||
|
|
f677e038cf | ||
|
|
ef13c2bf0e | ||
|
|
68e552010e | ||
|
|
c5143fabad | ||
|
|
64efdad61d | ||
|
|
8bc07ca452 | ||
|
|
ea4d28e7d9 | ||
|
|
e1fce4a066 | ||
|
|
76d75e9c17 | ||
|
|
9278b3c17d | ||
|
|
ef9fbe84c0 | ||
|
|
b3a69e8863 | ||
|
|
5e4663ac44 | ||
|
|
ca18a555c1 | ||
|
|
c446bbb016 | ||
|
|
d5254434c6 | ||
|
|
e2277b9482 | ||
|
|
739502aef9 | ||
|
|
de42624992 | ||
|
|
0ccfbe5dd6 | ||
|
|
3ffad12681 | ||
|
|
565867b5a8 | ||
|
|
303cc82a06 | ||
|
|
4c2abc0693 | ||
|
|
b63167fbae | ||
|
|
8adb6434bf | ||
|
|
4c9e407fef | ||
|
|
ef566b3f9c | ||
|
|
895231ea98 | ||
|
|
c7000a754e | ||
|
|
9ac8404681 | ||
|
|
6c0b7d5951 | ||
|
|
ddfe60a7b0 | ||
|
|
7dedeb904b | ||
|
|
4c02e0102b | ||
|
|
e91516fcc7 | ||
|
|
d01f6c0d8e | ||
|
|
1ce69cd7c9 | ||
|
|
83c10793ac | ||
|
|
a344c11ba1 | ||
|
|
c7d5a9a819 | ||
|
|
2f43ed94ab | ||
|
|
4a4ebf3cd6 | ||
|
|
18b832dbda | ||
|
|
1200e7fecd | ||
|
|
3cc6b73d3a | ||
|
|
4831ff186a | ||
|
|
a2d60a4a76 | ||
|
|
9f604777b9 | ||
|
|
fce9bda9bb | ||
|
|
4cc6745a0d | ||
|
|
67f0b806f4 | ||
|
|
9280dfb3ad | ||
|
|
2befbb0eba | ||
|
|
41359b7194 | ||
|
|
c57861a67d | ||
|
|
d31ece8793 | ||
|
|
4f7148b756 | ||
|
|
bc8a781d86 | ||
|
|
153a70fb85 | ||
|
|
2f93492fc3 | ||
|
|
0b39ff1457 | ||
|
|
f6817037ae | ||
|
|
216f97ea8a | ||
|
|
11c194a168 | ||
|
|
3c74c111a7 | ||
|
|
b69b8b87a7 | ||
|
|
825baa9f68 | ||
|
|
9f5736cfaa | ||
|
|
ae24ffbe86 | ||
|
|
487316370b | ||
|
|
8bc3565270 | ||
|
|
07450f6bf7 | ||
|
|
71d48c3cf2 | ||
|
|
7343e81065 | ||
|
|
1f1310bef6 | ||
|
|
27c69ec8e1 | ||
|
|
b81b896bc6 | ||
|
|
ef79db2ca5 | ||
|
|
88565eec62 | ||
|
|
e8a520e55f | ||
|
|
9a33f7bd93 | ||
|
|
23c70e237b | ||
|
|
7fcf41db78 | ||
|
|
36f4a4bd38 | ||
|
|
87541db80d | ||
|
|
80112c682f | ||
|
|
eb29d3dadb | ||
|
|
558c367038 | ||
|
|
4b88d00379 | ||
|
|
aad3b5ccd5 | ||
|
|
8b50806093 | ||
|
|
45c9bda2e9 | ||
|
|
23237210d1 | ||
|
|
e34382e6a3 | ||
|
|
b8ad2ee29a | ||
|
|
6bd62bac64 | ||
|
|
8b959fc6d8 | ||
|
|
6e615e20d6 | ||
|
|
efbc5fc4bc | ||
|
|
79c77ae835 | ||
|
|
618b6b1caf | ||
|
|
864658df80 | ||
|
|
4510e86b64 | ||
|
|
4af4490abc | ||
|
|
37c4040203 | ||
|
|
55f9be3b5c | ||
|
|
152b25b93c | ||
|
|
ea0d0bb4dc | ||
|
|
37e360aead | ||
|
|
20211e6265 | ||
|
|
226c888cc9 | ||
|
|
0e9f8ec272 | ||
|
|
8eb5d8028d | ||
|
|
2fb296d116 | ||
|
|
010c1ee439 | ||
|
|
dddc4a0e95 | ||
|
|
679ccf19d9 | ||
|
|
5ba3cc6623 | ||
|
|
c1a4731762 | ||
|
|
767577e386 | ||
|
|
d2f620dbc6 | ||
|
|
0aa556c29e | ||
|
|
10cf003758 | ||
|
|
0e6254453a | ||
|
|
587a29892f | ||
|
|
db8245d954 | ||
|
|
0088306bca | ||
|
|
b6fc718713 | ||
|
|
18e55dafd4 | ||
|
|
8e013ab793 | ||
|
|
b88e79f0f3 | ||
|
|
35b505120d | ||
|
|
6807d3080b | ||
|
|
0d1cfb3286 | ||
|
|
0b9a5621fe | ||
|
|
9bd15af02a | ||
|
|
aab3bf1330 | ||
|
|
9e95218523 | ||
|
|
bb98e6de03 | ||
|
|
2604228233 | ||
|
|
a7035d3469 | ||
|
|
f6dce0e17c | ||
|
|
ccfa6f9fc3 | ||
|
|
0ee8e2f967 | ||
|
|
94dc4c6e93 | ||
|
|
4144e387d6 | ||
|
|
de4e05e1e0 | ||
|
|
36118b700f | ||
|
|
2fb947b47a | ||
|
|
0f1156e951 | ||
|
|
e9f8be92b7 | ||
|
|
6360ea9f8e | ||
|
|
ad2d8e80f0 | ||
|
|
bc0a5fdaa6 | ||
|
|
d6cfc887c3 | ||
|
|
ce41c436aa | ||
|
|
ec096eaaa2 | ||
|
|
9c32dd5308 | ||
|
|
69d7ee9547 | ||
|
|
e2e4cb67ca | ||
|
|
4a08ad4997 | ||
|
|
107a1c12bd | ||
|
|
8fe5268a73 | ||
|
|
faba1ad8e3 | ||
|
|
ea4885c710 | ||
|
|
4d85002d2b | ||
|
|
535b17f592 | ||
|
|
e556b45d4d | ||
|
|
48130dcbdc | ||
|
|
1419ca6325 | ||
|
|
8513dc0645 | ||
|
|
70e8f8370b | ||
|
|
135c90a8b8 | ||
|
|
fa588c2d24 | ||
|
|
b4ce7a91c2 | ||
|
|
8ca9d66c32 | ||
|
|
a86fabde59 | ||
|
|
40a896675f | ||
|
|
cf7848e3c5 | ||
|
|
3d4394a9bc | ||
|
|
05f4c08d09 | ||
|
|
a4107a86f9 | ||
|
|
a5c695dd88 | ||
|
|
7576d9195d | ||
|
|
838c1508be | ||
|
|
7941683435 | ||
|
|
91f2311a50 | ||
|
|
45d10d9b83 | ||
|
|
f5d5093922 | ||
|
|
fa4687b0f8 | ||
|
|
e78c475118 | ||
|
|
def91af01f | ||
|
|
5eb0fb75ac | ||
|
|
de8c69f766 | ||
|
|
5879ac424d | ||
|
|
e30f62ec12 | ||
|
|
dc4e36e8a8 | ||
|
|
12ded8c9a9 | ||
|
|
7378c6fd0e | ||
|
|
77a8b08834 | ||
|
|
79db9eb482 | ||
|
|
59f6d36114 | ||
|
|
c36c26f5a9 | ||
|
|
d6692f20b5 | ||
|
|
6e37f596df | ||
|
|
d326891ef8 | ||
|
|
46a34327d9 | ||
|
|
97242af30f | ||
|
|
ca0a8f79cd | ||
|
|
67eac59585 | ||
|
|
8cf1f0dc22 | ||
|
|
51702e4bda | ||
|
|
426a3d1ef3 | ||
|
|
e8d71a52ec | ||
|
|
ccd0cb19a4 | ||
|
|
9f85178d71 | ||
|
|
085ee52305 | ||
|
|
22a71275b2 | ||
|
|
5a730d70d6 | ||
|
|
41f58e5e52 | ||
|
|
34af57f585 | ||
|
|
4674cbebc5 | ||
|
|
69add6f523 | ||
|
|
b70e874135 | ||
|
|
03fa29dd5a | ||
|
|
9e467b900e | ||
|
|
6e1c7dd91e | ||
|
|
9812406c18 | ||
|
|
cf11ae93af | ||
|
|
14756ad39b | ||
|
|
1486b481f6 | ||
|
|
84d82e9a59 | ||
|
|
c7ce4a8670 | ||
|
|
fe0a3b4c41 | ||
|
|
0bce278bd4 | ||
|
|
fedfdb3364 | ||
|
|
028f0ab74b | ||
|
|
cd278155e2 | ||
|
|
1e419ab49d | ||
|
|
2f57fd199d | ||
|
|
5932775cd4 | ||
|
|
d68d6c38d6 | ||
|
|
d690e10caf | ||
|
|
99ea613b39 | ||
|
|
13715317bd | ||
|
|
18d1ed441b | ||
|
|
db067c4757 | ||
|
|
51388f6adb | ||
|
|
fcbf363538 | ||
|
|
b4d7795006 | ||
|
|
2364ed74d7 | ||
|
|
87fb74dd5a | ||
|
|
eef0c6ae6e | ||
|
|
5ee905a977 | ||
|
|
ca3e083995 | ||
|
|
b7ae91892c | ||
|
|
508ca745c1 | ||
|
|
95e8a38c85 | ||
|
|
441f3eaf4c | ||
|
|
9edb10ca55 | ||
|
|
7f094bc605 | ||
|
|
2463e958aa | ||
|
|
390e6fef9e | ||
|
|
6b416c3dfb | ||
|
|
60d2daf4c8 | ||
|
|
9528f25ff2 | ||
|
|
5da03dfca7 | ||
|
|
8b3b0376c3 | ||
|
|
5f3c4756d2 | ||
|
|
3b34a64893 | ||
|
|
243cf8d630 | ||
|
|
671ce30ed4 | ||
|
|
9021887072 | ||
|
|
6a80d4c8e9 | ||
|
|
471e26817a | ||
|
|
14bbd1ee0f | ||
|
|
22b06b444d | ||
|
|
cebd29e40d | ||
|
|
acf7e55b5b | ||
|
|
7c29aa3b38 | ||
|
|
dee5b4cf60 | ||
|
|
896e14b1e4 | ||
|
|
42c20d6ef5 | ||
|
|
0c94f80f68 | ||
|
|
95f308737e | ||
|
|
2a2a0392cd | ||
|
|
bcbdae8278 | ||
|
|
805aee67ee | ||
|
|
0477984d7a | ||
|
|
4e96196e07 | ||
|
|
25ee2abdaa | ||
|
|
75b71ae457 | ||
|
|
7548fc35fd | ||
|
|
5bc14b5b30 | ||
|
|
bee98ce25e | ||
|
|
0e6a5315ab | ||
|
|
91b51528ef | ||
|
|
5e3962afc2 | ||
|
|
f07c52d2fb | ||
|
|
50846a1a96 | ||
|
|
4cb726f374 | ||
|
|
da9d1b4984 | ||
|
|
64ad52dc45 | ||
|
|
6169e5ff5b | ||
|
|
ef33973f7f | ||
|
|
33a5254f2a | ||
|
|
0d737ecf6d | ||
|
|
951eea2396 | ||
|
|
75968bf449 | ||
|
|
32aa46ac69 | ||
|
|
339cae2532 | ||
|
|
e6ec3bfe6a | ||
|
|
012518c375 | ||
|
|
cc8c8584d9 | ||
|
|
d1910b3c42 | ||
|
|
4a1fa3d4fa | ||
|
|
61178c9d06 | ||
|
|
094c04c7f0 | ||
|
|
37cca4878d | ||
|
|
2b40e1ec05 | ||
|
|
6e95d3afcc | ||
|
|
6b9b9f0be2 | ||
|
|
f14b963b29 | ||
|
|
ddd2814e8f | ||
|
|
36f15a81a9 | ||
|
|
ece93dac20 | ||
|
|
0226dbba5a | ||
|
|
e773e77185 | ||
|
|
893cbb2ec5 | ||
|
|
1898de2609 | ||
|
|
fe03cc28c4 | ||
|
|
5309b7b6ce | ||
|
|
bc9b8442d7 | ||
|
|
a84762b00d | ||
|
|
2d117e1d7e | ||
|
|
986674942d | ||
|
|
13c224589b | ||
|
|
20b091fbfa | ||
|
|
168b6afdfa | ||
|
|
3c6320b929 | ||
|
|
4f1dc582bd | ||
|
|
daf2ef1e30 | ||
|
|
f703bd8b5a | ||
|
|
997cd7058a | ||
|
|
e896c2f6c5 | ||
|
|
3d723ca426 | ||
|
|
c17e1b3fa2 | ||
|
|
d70fc36485 | ||
|
|
47784ca6d3 | ||
|
|
3e5a3aca27 | ||
|
|
89b0227594 | ||
|
|
b98a30e7c6 | ||
|
|
5c7862a685 | ||
|
|
82833a9eb4 | ||
|
|
66171c3c4b | ||
|
|
179def91c7 | ||
|
|
9f63ad4527 | ||
|
|
878028271a | ||
|
|
d4e15bbcbf | ||
|
|
13bb056df1 | ||
|
|
14d56904d2 | ||
|
|
c2ffd05763 | ||
|
|
46e394fbef | ||
|
|
2613f0c2f9 | ||
|
|
e5fd7c01f4 | ||
|
|
73a0dd6e23 | ||
|
|
a9dc7f9eeb | ||
|
|
c3836f23d1 | ||
|
|
bfd73cbb19 | ||
|
|
6a1753edaa | ||
|
|
558a73086d | ||
|
|
15966fbd26 | ||
|
|
1e8b55383e | ||
|
|
6920ebb0ba | ||
|
|
55f35949e1 | ||
|
|
ccb998a221 | ||
|
|
30bc27b4c4 | ||
|
|
da3c95ccaf | ||
|
|
bbcbe1dceb | ||
|
|
f15beea454 | ||
|
|
16e384be76 | ||
|
|
e74e6d8fc5 | ||
|
|
b6f4603107 | ||
|
|
0e7d7f765a | ||
|
|
33b4d8d719 | ||
|
|
0d765748e3 | ||
|
|
93953f1424 | ||
|
|
e9b67db05c | ||
|
|
09bf912b9e | ||
|
|
e701bacdaa | ||
|
|
cc22542031 | ||
|
|
6c53262b5b | ||
|
|
7d7d4995d8 | ||
|
|
977546c8c9 | ||
|
|
53dcd4cb43 | ||
|
|
9e18e738eb | ||
|
|
252c7187d5 | ||
|
|
90852b9557 | ||
|
|
0df16236a2 | ||
|
|
ecaf5a4318 | ||
|
|
86e5e1a7b1 | ||
|
|
ab10094071 | ||
|
|
2d536d73fe | ||
|
|
80ea8ee50b | ||
|
|
b49c5f47d1 | ||
|
|
a9da78946b | ||
|
|
b6d9accea4 | ||
|
|
4611a65bce | ||
|
|
db29b3fc18 | ||
|
|
2534a46bd3 | ||
|
|
7def4b502e | ||
|
|
4ea26d7ca3 | ||
|
|
c4c8075b1f | ||
|
|
266545d2a0 | ||
|
|
ddd7046513 | ||
|
|
7a44eeb22b | ||
|
|
5bcf8263cf | ||
|
|
f380ba0106 | ||
|
|
1a6d81f68e | ||
|
|
522818113b | ||
|
|
f93d347665 | ||
|
|
e96cee1c3b | ||
|
|
4d46e0a8ac | ||
|
|
80e35c7524 | ||
|
|
186c26bc99 | ||
|
|
be6a7f54f9 | ||
|
|
fff7aac5b6 | ||
|
|
0d90fbd3ff | ||
|
|
e71779049e | ||
|
|
7da909e3c1 | ||
|
|
e439fd14a9 | ||
|
|
ffed1b446a | ||
|
|
7b65412ec6 | ||
|
|
f1cbdcc094 | ||
|
|
ec5573efa6 | ||
|
|
242da62271 | ||
|
|
23d1221ef9 | ||
|
|
5402d8c74f | ||
|
|
176026645c | ||
|
|
b0d4ed6c0e | ||
|
|
2f7ac71921 | ||
|
|
b599315a85 | ||
|
|
e86d8aa77a | ||
|
|
ab4ac87d52 | ||
|
|
af98fb1621 | ||
|
|
0ce7c1b3b4 | ||
|
|
2bdaa0d428 | ||
|
|
2486a4de97 | ||
|
|
6137b215e9 | ||
|
|
265ba16594 | ||
|
|
a7bc3d0c98 | ||
|
|
ee618880d7 | ||
|
|
2c95c8c310 | ||
|
|
b90e37d9fc | ||
|
|
34563070f4 | ||
|
|
66b269f4bf | ||
|
|
168cb4d4a0 | ||
|
|
91b531da43 | ||
|
|
2e0d567361 | ||
|
|
81e6280cff | ||
|
|
e2fda7b9d3 | ||
|
|
ede30807f5 | ||
|
|
9f354013fa | ||
|
|
30552f134c | ||
|
|
e4beba9a21 | ||
|
|
b71ab7ec84 | ||
|
|
596cfc372b | ||
|
|
d5f6c5680c | ||
|
|
28c508ff62 | ||
|
|
97ff6f406d | ||
|
|
bd39b296fa | ||
|
|
bfbe1e930b | ||
|
|
3fe45176cc | ||
|
|
bffa6fbf03 | ||
|
|
161808bc37 | ||
|
|
ac42b15acd | ||
|
|
47d21de4eb | ||
|
|
e12ffa9ebc | ||
|
|
2944023986 | ||
|
|
49c1e7670d | ||
|
|
b157b901e3 | ||
|
|
1a1f88f202 | ||
|
|
d02d70da91 | ||
|
|
c37f2957bb | ||
|
|
a32f5fe57a | ||
|
|
b7f6d36a63 | ||
|
|
2975a025ac | ||
|
|
f4e396dde2 | ||
|
|
ef54911323 | ||
|
|
18b1074232 | ||
|
|
8468e0c5ea | ||
|
|
436d5c9ec9 | ||
|
|
1f88f7e1a3 | ||
|
|
09682754f8 | ||
|
|
8e3d84b1e5 | ||
|
|
8bb2631192 | ||
|
|
38a7fb6266 | ||
|
|
0ac2fb77a1 | ||
|
|
f12774b539 | ||
|
|
d50bdedcec | ||
|
|
8d443e5992 | ||
|
|
2b5fb6de53 | ||
|
|
dc86f27199 | ||
|
|
d97ed80867 | ||
|
|
3601b448fe | ||
|
|
68aab13d51 | ||
|
|
6cef13d756 | ||
|
|
1d45941f5d | ||
|
|
5e5badf11f | ||
|
|
840c65946b | ||
|
|
fdbd786126 | ||
|
|
d5365275e0 | ||
|
|
886e48d3ee | ||
|
|
342c78a4f6 | ||
|
|
a483d94e26 | ||
|
|
b40412a7a1 | ||
|
|
d652e04f7b | ||
|
|
85326d1104 | ||
|
|
28c087138b | ||
|
|
8badf14ec3 | ||
|
|
4f1db29ef8 | ||
|
|
2354dc1b71 | ||
|
|
f106bfa6d7 | ||
|
|
1a7edbea06 | ||
|
|
5bfaee9e4d | ||
|
|
0cb2309d7d | ||
|
|
0415a1d97f | ||
|
|
249c60f4d1 | ||
|
|
c357cc378c | ||
|
|
f7c0d731b6 | ||
|
|
e852e3edac | ||
|
|
2a283b74a3 | ||
|
|
c749bc2372 | ||
|
|
9c60f7190e | ||
|
|
8126b2276e | ||
|
|
e1de21015f | ||
|
|
61e505f1f8 | ||
|
|
2e3611161e | ||
|
|
95f5eaaacf | ||
|
|
87a3ea99e2 | ||
|
|
ddab65a55d | ||
|
|
0cf8f8ce0d | ||
|
|
2b531ec70c | ||
|
|
6cd7966904 | ||
|
|
ad72ab990f | ||
|
|
b53412fc00 | ||
|
|
cbb2de9e11 | ||
|
|
593c42f601 | ||
|
|
84b10b9dad | ||
|
|
0f286061e4 | ||
|
|
c8aad01480 | ||
|
|
d70a6bee69 | ||
|
|
67799b5c54 | ||
|
|
05b8eef622 | ||
|
|
50e56b5bc4 | ||
|
|
30ab4e62ec | ||
|
|
a1d901294b | ||
|
|
eed818cfe5 | ||
|
|
db0b6bfcf5 | ||
|
|
da81c1c6df | ||
|
|
42d3296f07 | ||
|
|
f9b4b1a6e8 | ||
|
|
d03603a75f | ||
|
|
ffa9e5ef66 | ||
|
|
e7f3cf9c74 | ||
|
|
e82a4a0a57 | ||
|
|
175f07fac9 | ||
|
|
2869744a5d | ||
|
|
99f898a483 | ||
|
|
1448db96e6 | ||
|
|
e02036a46a | ||
|
|
09f33263cb | ||
|
|
412b1ec74c | ||
|
|
9a1538fb4d | ||
|
|
0ad9b31ee3 | ||
|
|
4cbb7bb2f2 | ||
|
|
ae109cd8c6 | ||
|
|
fbaba774a6 | ||
|
|
38bc8062a4 | ||
|
|
d41d184b55 | ||
|
|
d6d32a7a7d | ||
|
|
15a9532fa7 | ||
|
|
3b9fb724fe | ||
|
|
33767b5635 | ||
|
|
175fc2b30e | ||
|
|
aec9347d69 | ||
|
|
91500d07c0 | ||
|
|
62ca14b504 | ||
|
|
9158a95d57 | ||
|
|
d0f3e387e7 | ||
|
|
158856f8ec | ||
|
|
1e59957753 | ||
|
|
f97d46040c | ||
|
|
d7485cb5d2 | ||
|
|
071fdb7f4a | ||
|
|
6dd96ffb20 | ||
|
|
f56fb0f7de | ||
|
|
7d28f6add9 | ||
|
|
eebef3e461 | ||
|
|
8336f630ea | ||
|
|
7d6bdbb6ed | ||
|
|
403b5c237f | ||
|
|
2367ed7911 | ||
|
|
b650f76979 | ||
|
|
96e59c198d | ||
|
|
bf1704d268 | ||
|
|
279dfd00c8 | ||
|
|
4d2577865a | ||
|
|
9d88fba40e | ||
|
|
9d2353b4f7 | ||
|
|
44b338d50a | ||
|
|
62114cab3b | ||
|
|
d426fb0355 | ||
|
|
2a8d6683ff | ||
|
|
7b18165390 | ||
|
|
814a44ac27 | ||
|
|
f5483c8996 | ||
|
|
acdbed4762 | ||
|
|
f09e4b54cf | ||
|
|
859fdf5e46 | ||
|
|
6aa72d5b7d | ||
|
|
886900df29 | ||
|
|
a450fed1f8 | ||
|
|
4c78100adb | ||
|
|
0317e6fe71 | ||
|
|
6825877344 | ||
|
|
a47ea88d28 | ||
|
|
a9e7f304ec | ||
|
|
d92a2ae3f4 | ||
|
|
b7dea35da4 | ||
|
|
5a5f11752d | ||
|
|
b6c300b5c2 | ||
|
|
0117dbfa5f | ||
|
|
c106f8a1a4 | ||
|
|
8b9741a70c | ||
|
|
271b85fd74 | ||
|
|
21a481790b | ||
|
|
ab5d9237f1 | ||
|
|
756853f2f3 | ||
|
|
5eb80d15f6 | ||
|
|
a98043b4ad | ||
|
|
d0818932e7 | ||
|
|
23b60ed385 | ||
|
|
5d2811bf21 | ||
|
|
7b202f2aa7 | ||
|
|
fa6e9c1735 | ||
|
|
c3300036d8 | ||
|
|
5efcdb2940 | ||
|
|
8113450ea4 | ||
|
|
d10b0c3c65 | ||
|
|
e8de0e8712 | ||
|
|
e1da1099da | ||
|
|
264aa8fefa | ||
|
|
941d0294b7 | ||
|
|
366f4e8914 | ||
|
|
1460c3c5a2 | ||
|
|
6271472827 | ||
|
|
3bc4a054ae | ||
|
|
dd85d559d7 | ||
|
|
ab10b082e5 | ||
|
|
60b60066f9 | ||
|
|
9e9e517698 | ||
|
|
020c0f2b98 | ||
|
|
dd74ebdc51 | ||
|
|
ddf7381efb | ||
|
|
3b67b912e4 | ||
|
|
37d7fa4510 | ||
|
|
096601473a | ||
|
|
2e25ed81ca | ||
|
|
4e11ea1d77 | ||
|
|
d9d470426b | ||
|
|
119b4ad8b2 | ||
|
|
ae1e9f000d | ||
|
|
eb0505f8f8 | ||
|
|
c8f7800ab5 | ||
|
|
bc4e7becdd | ||
|
|
71ece19c40 | ||
|
|
e9053c0d15 | ||
|
|
cf76296142 | ||
|
|
5e5d3626f4 | ||
|
|
5808c5b412 | ||
|
|
cd64392d44 | ||
|
|
79f889e16c | ||
|
|
595bfecf1c | ||
|
|
ced2e192eb | ||
|
|
ee91612c07 | ||
|
|
4d815c1fd9 | ||
|
|
d2687a55b7 | ||
|
|
41142d33dc | ||
|
|
52ab6107c5 | ||
|
|
56ac348ed4 | ||
|
|
1ac0af53ab | ||
|
|
f9361b471c | ||
|
|
8f39e0dcde | ||
|
|
81de402d2d | ||
|
|
9d6e3f52d8 | ||
|
|
6a0a9bbaaa | ||
|
|
24bc29b3cd | ||
|
|
aac5ebff73 | ||
|
|
61cc5b0a4a | ||
|
|
1383783c33 | ||
|
|
971129be6b | ||
|
|
f4fb10483a | ||
|
|
256a0e1f22 | ||
|
|
b82f0b04fe | ||
|
|
9714420f90 | ||
|
|
0054f0bc26 | ||
|
|
38f565d849 | ||
|
|
1aff7ae2a3 | ||
|
|
31020047ca | ||
|
|
26778ec52b | ||
|
|
b27590f4f5 | ||
|
|
ea2af5af04 | ||
|
|
3f1a7a0da0 | ||
|
|
8184a13bf3 | ||
|
|
2f7be56bef | ||
|
|
5c166bb3b2 | ||
|
|
4e76526843 | ||
|
|
0e6fae0632 | ||
|
|
62a8fd1b02 | ||
|
|
5a8e252790 | ||
|
|
78613cd7c0 | ||
|
|
8f6f7f7e1c | ||
|
|
f46ad6b898 | ||
|
|
f153b08a1e | ||
|
|
6b57f00d7d | ||
|
|
de65ab702b | ||
|
|
a87b735832 | ||
|
|
afc6022529 | ||
|
|
2d3bb21f60 | ||
|
|
c26f478b45 | ||
|
|
f958a0db3e | ||
|
|
6df1130140 | ||
|
|
260a771070 | ||
|
|
6659ed97ca | ||
|
|
4eae49fe58 | ||
|
|
41fef5c0e4 | ||
|
|
8c4b45a439 | ||
|
|
e35b5e78d2 | ||
|
|
b04c1a2c20 | ||
|
|
25d75b6f94 | ||
|
|
a2adacf5a3 | ||
|
|
ca718d8da9 | ||
|
|
3e7b849ad1 | ||
|
|
2ec646659d | ||
|
|
c33e3d8a4c | ||
|
|
d65c180746 | ||
|
|
754af79e0c | ||
|
|
3733cc4cf8 | ||
|
|
9e5c1740b4 | ||
|
|
58919d37ab | ||
|
|
2ec4279ac7 | ||
|
|
3052276c3b | ||
|
|
65a9238d48 | ||
|
|
5a1bb94563 | ||
|
|
1094d3f9f7 | ||
|
|
d8452ce6f8 | ||
|
|
a5be039fd2 | ||
|
|
6178a54292 | ||
|
|
3a2e9c42ab | ||
|
|
8bfb97aa1c | ||
|
|
4c32b9bdec | ||
|
|
6dcb29caa9 | ||
|
|
917a46eea5 | ||
|
|
2be9d92268 | ||
|
|
29e644ab1e | ||
|
|
843a2fd10a | ||
|
|
4d8e6cc53a | ||
|
|
a52e443a44 | ||
|
|
70eb4a1b64 | ||
|
|
19b5cb9b86 | ||
|
|
5d026ea462 | ||
|
|
9c972430bb | ||
|
|
cff9a7a1d0 | ||
|
|
3cbc670d90 | ||
|
|
5b39620619 | ||
|
|
8498547377 | ||
|
|
175103ed32 | ||
|
|
e177b94ca1 | ||
|
|
193da3cf46 | ||
|
|
f29dd0b89a | ||
|
|
14cd1835dd | ||
|
|
01f82bb873 | ||
|
|
844eddf127 | ||
|
|
bdb7bf8475 | ||
|
|
04b687a050 | ||
|
|
a67bf6877a | ||
|
|
ea8fd7d4ff | ||
|
|
bc69ef34ff | ||
|
|
38a0ef8b1e | ||
|
|
fd21811e85 | ||
|
|
17d4fbbf45 | ||
|
|
8f455237c7 | ||
|
|
fd340c2bb5 | ||
|
|
6e2accf00a | ||
|
|
26e0ac7318 | ||
|
|
f58dc5db08 | ||
|
|
aba6b31bbd | ||
|
|
5db237afd3 | ||
|
|
45815e2dc6 | ||
|
|
d827a1dad9 | ||
|
|
5a94367073 | ||
|
|
eab7c24d53 | ||
|
|
24b4b9b59f | ||
|
|
f79d9b2f56 | ||
|
|
0237c51f48 | ||
|
|
c49f6cce18 | ||
|
|
61a3c01e26 | ||
|
|
625e7b588a | ||
|
|
0817cb36b8 | ||
|
|
46220028ab | ||
|
|
b546716582 | ||
|
|
46342c8cb1 | ||
|
|
a281b500d4 | ||
|
|
101fda5ee3 | ||
|
|
67fed5bcc1 | ||
|
|
aec4de430d | ||
|
|
e8ac985b89 | ||
|
|
c937987772 | ||
|
|
03f4ab3e8b | ||
|
|
afeb8df13e | ||
|
|
bc73db7656 | ||
|
|
511b3f3e4a | ||
|
|
0633cfefd0 | ||
|
|
77ea5a60a5 | ||
|
|
8a8c4b4382 | ||
|
|
75e3563e75 | ||
|
|
119dd2180c | ||
|
|
12e98beec7 | ||
|
|
b829e00b5b | ||
|
|
fd56a19156 | ||
|
|
2607430fa8 | ||
|
|
a449413a3e | ||
|
|
15bb9f9b24 | ||
|
|
dd2ef27115 | ||
|
|
250b257507 | ||
|
|
b32955136c | ||
|
|
8b73152765 | ||
|
|
4c8181fa82 | ||
|
|
aa54eca4ee | ||
|
|
8a0a5a249e | ||
|
|
77e6f42161 | ||
|
|
7c8603138b | ||
|
|
ebade7523e | ||
|
|
406605b509 | ||
|
|
77f881af67 | ||
|
|
4714ddc8d6 | ||
|
|
a94b187da5 | ||
|
|
636a1f110a | ||
|
|
e6e8bbcaf6 | ||
|
|
4f49135c1f | ||
|
|
10cc68357e | ||
|
|
932f641ae7 | ||
|
|
a139571ef5 | ||
|
|
bf067f99a9 | ||
|
|
a6d6593cd3 | ||
|
|
f485e4b6ef | ||
|
|
df80eb1bd9 | ||
|
|
2a67e0206b | ||
|
|
2b5d91b622 | ||
|
|
1c5a6aae76 | ||
|
|
2ae64e70ad | ||
|
|
a302978636 | ||
|
|
1ebf16cf08 | ||
|
|
9a7e8953c7 | ||
|
|
047b8e278e | ||
|
|
9878c07c09 | ||
|
|
acb07aa72a | ||
|
|
e889347710 | ||
|
|
3ab974856f | ||
|
|
f431feacf8 | ||
|
|
791e80b9fc | ||
|
|
12138832a0 | ||
|
|
302a94a9d8 | ||
|
|
03e754e0f6 | ||
|
|
925cb44819 | ||
|
|
ed798057a3 | ||
|
|
f08e1727bd | ||
|
|
c8c5cb589a | ||
|
|
fe1b34559e | ||
|
|
4df45dd8c2 | ||
|
|
fa6e894bfb | ||
|
|
9ed15d2b5d | ||
|
|
07d78b31f4 | ||
|
|
f342c87cf0 | ||
|
|
ed8d7229d0 | ||
|
|
2f9d388543 | ||
|
|
20a3324aab | ||
|
|
0d3008e8de | ||
|
|
78e0732440 | ||
|
|
91e87afa74 | ||
|
|
3461815e74 | ||
|
|
9b807bb2da | ||
|
|
8b53ca7d2f | ||
|
|
943eb7087f | ||
|
|
9a14346b0b | ||
|
|
56fb45f242 | ||
|
|
d0e62eb82f | ||
|
|
6dce03afa6 | ||
|
|
ea5790a2d1 | ||
|
|
a0c236f089 | ||
|
|
aa66a3d47a | ||
|
|
6994fc7d76 | ||
|
|
ea33e9228b | ||
|
|
7cf50a2efa | ||
|
|
17897400c0 | ||
|
|
de41182868 | ||
|
|
6d30315c53 | ||
|
|
728a73d58a | ||
|
|
6a00ce9b45 | ||
|
|
fdb61ee0ce | ||
|
|
6bba5a5b7d | ||
|
|
fadd7bfec8 | ||
|
|
d70443085b | ||
|
|
3af9407367 | ||
|
|
4238937b1e | ||
|
|
cdb0cfe8fb | ||
|
|
71b21f1bfa | ||
|
|
0be60b3180 | ||
|
|
c5a933de66 | ||
|
|
bf174cca6a | ||
|
|
76f85e7441 | ||
|
|
e2b6e9bd65 | ||
|
|
5f3376558f | ||
|
|
060a9c0ad7 | ||
|
|
1c6060fa2a | ||
|
|
d3b5e4988f | ||
|
|
e197731e89 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,7 @@
|
||||
**/build/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
packages/eas-cli/src/graphql/generated.ts
|
||||
|
||||
node_modules
|
||||
@@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['universe/node'],
|
||||
plugins: ['async-protect', 'node'],
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
'sort-imports': [
|
||||
'warn',
|
||||
{
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'lodash',
|
||||
message: "Don't use lodash, it's heavy!",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'async-protect/async-suffix': 'error',
|
||||
'node/no-sync': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
|
||||
rules: {
|
||||
'async-protect/async-suffix': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -0,0 +1,58 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'needs review'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Build/Submit details page URL
|
||||
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
|
||||
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Managed or bare?
|
||||
description: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx expo-env-info` and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: Error output
|
||||
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Reproducible demo or steps to reproduce from a blank project
|
||||
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
|
||||
@@ -0,0 +1,14 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement,needs review'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Do you understand that any feature requests opened in this repository will be closed?
|
||||
options:
|
||||
- 'Yes'
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
|
||||
|
||||
# Why
|
||||
|
||||
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
|
||||
|
||||
# How
|
||||
|
||||
How did you build this feature or fix this bug and why?
|
||||
|
||||
# Test Plan
|
||||
|
||||
Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction.
|
||||
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
name: Changelog
|
||||
|
||||
jobs:
|
||||
pr-commented:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
run: hub pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Add changelog entry
|
||||
working-directory: ./scripts
|
||||
run: |
|
||||
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
||||
if [[ ! "$PARAMS" =~ " " ]]; then
|
||||
echo "Invalid /changelog-entry command"
|
||||
exit 1
|
||||
fi
|
||||
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
||||
env:
|
||||
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
||||
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
# use the last commit author name and email
|
||||
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Installer tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run the installer
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: bash bin/install.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: eas --version
|
||||
@@ -0,0 +1,197 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Release workflow
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create a release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
draft: true
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- id: arm
|
||||
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload linux-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload linux-arm tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
|
||||
asset_name: ${{ steps.arm.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
build-mac:
|
||||
name: Build for macOS
|
||||
# Use macOS to get the correct platform specific dependencies
|
||||
runs-on: macos-latest
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets darwin-x64
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload darwin-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
- run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- id: x86
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- name: Upload x64 installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload x86 installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
|
||||
asset_name: ${{ steps.x86.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
publish-to-npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Resolve version
|
||||
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Update changelog
|
||||
working-directory: ./scripts
|
||||
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||
- name: Publish release
|
||||
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git fetch
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
|
||||
npx slackmd /tmp/changelog.md
|
||||
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
|
||||
cat /tmp/changelog.slack.md >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: eas-cli
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_ICON: https://github.com/expo-ci.png?size=48
|
||||
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
||||
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
||||
SLACK_USERNAME: 'Expo CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Size
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, opened]
|
||||
jobs:
|
||||
compare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
build-script: 'build:tarballs:linux'
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
pattern: './packages/eas-cli/dist/*.tar.gz'
|
||||
compression: 'none'
|
||||
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
|
||||
clean-script: 'clean'
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
ascending: false
|
||||
operations-per-run: 300
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
enable-statistics: true
|
||||
exempt-issue-labels: "needs review"
|
||||
exempt-all-assignees: true
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Run tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['12', '14', '16']
|
||||
include:
|
||||
- node: '16'
|
||||
coverage: true
|
||||
name: Test with Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
if: ${{ matrix.coverage }}
|
||||
- run: yarn lint --max-warnings=0
|
||||
- run: shellcheck bin/*.sh
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Build directories
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
packages/eas-cli/tmp
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
# Tests
|
||||
coverage
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+1115
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
||||
packages/eas-cli/src/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
|
||||
packages/eas-cli/src/submit @barthap @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
|
||||
|
||||
packages/eas-json @dsokal @wkozyra95
|
||||
@@ -0,0 +1,7 @@
|
||||
# Community Code of Conduct
|
||||
|
||||
The Expo team is committed to fostering an open and welcoming environment.
|
||||
|
||||
**Our Community Guidelines can be found here:**
|
||||
|
||||
https://expo.dev/guidelines
|
||||
@@ -0,0 +1,4 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Run `yarn release` in the repository root folder.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Simulator control validation
|
||||
|
||||
Codex captured these terminal sessions from the built EAS CLI against native serve-sim on an owned iPhone 17 Pro Simulator (iOS 26.4, Xcode 26.4 / 17E192). Session lookup and authentication used local GraphQL/context fixtures. This is local native validation; hosted EAS and Android device execution were not tested.
|
||||
|
||||
Interactive source: `52544e70d05d431582f3c0cdfba437a523a8fb69`. Import source: `721e2dc22c2a95c6cd71bc26b00b7250dec0254d`.
|
||||
|
||||
Codex ran the EAS CLI package suite: 353 suites and 3,448 tests passed, with 4 skipped. The focused control run passed 193 tests with default Babel coverage: 97.29% lines and 90.48% branches across the seven control modules. Typecheck, changed-file lint, formatting, and build passed.
|
||||
|
||||
Independent Codex reviews found output preflight and competing upload-source issues in config import. Both were fixed, reproduced again, and re-reviewed. Human review remains pending.
|
||||
|
||||
- Interactive selection: set hardware keyboard off, read back off, return to picker, Exit (status 0).
|
||||
- Ctrl-C at the picker: status 130. Session ending at the picker: status 0. Canonical input and echo restored in all three terminal runs.
|
||||
- Seven-action import: keyboard set/read, clipboard write/read, local image upload to Photos, screenshot download, keyboard restore. The import returned to the picker and Exit restored the terminal.
|
||||
- Invalid later parameters rejected with status 1; keyboard readback remained on.
|
||||
- Runtime failure at step 2 preserved the first result, reported the failing step, exited 1, and left the third action unapplied.
|
||||
- Android session selection rejected with the explicit unsupported message.
|
||||
|
||||
The PNG, GIF, MP4, and cast files come from actual PTY output. The MP4s are conversions of the captured GIFs. `downloaded-screen.png` is the screenshot returned by the native import.
|
||||
|
||||
— Codex
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script downloads the latest release version of EAS CLI from GitHub and
|
||||
# installs it in /usr/local/bin
|
||||
|
||||
{
|
||||
# All code is inside a block to ensure the script executes only when
|
||||
# downloaded completely.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
prefix="${EAS_PREFIX:-/usr/local}"
|
||||
temp_dir="$(mktemp -d)"
|
||||
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
|
||||
then
|
||||
# Use colors when stdout is a terminal and NO_COLOR isn't set.
|
||||
text_bold=$'\033[1m'
|
||||
text_normal=$'\033[22m'
|
||||
text_red=$'\033[31m'
|
||||
text_default_color=$'\033[39m'
|
||||
else
|
||||
# Disable colors
|
||||
text_bold=""
|
||||
text_normal=""
|
||||
text_red=""
|
||||
text_default_color=""
|
||||
fi
|
||||
|
||||
abort() {
|
||||
echo -e "$text_bold$text_red$1$text_normal$text_default_color"
|
||||
exit 1
|
||||
}
|
||||
|
||||
install() {
|
||||
needs_sudo=""
|
||||
# Make sure path exists and can be written to by the current user.
|
||||
if ! mkdir -p "$prefix/lib" || [[ ! -w "$prefix/lib" ]]
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
if ! mkdir -p "$prefix/bin" || [[ ! -w "$prefix/bin" ]]
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
if ! rm -rf "$prefix/lib/eas"
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
# Run commands with sudo if necessary.
|
||||
if [ -n "$needs_sudo" ]
|
||||
then
|
||||
if [ "$EUID" -ne 0 ]
|
||||
then
|
||||
echo "Installing requires superuser access."
|
||||
echo "The sudo command will prompt for your password."
|
||||
fi
|
||||
sudo rm -rf "$prefix/lib/eas"
|
||||
sudo mv "$temp_dir/eas" "$prefix/lib/eas"
|
||||
sudo ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
|
||||
else
|
||||
mv "$temp_dir/eas" "$prefix/lib/eas"
|
||||
ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
|
||||
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
|
||||
fi
|
||||
|
||||
hardware="$(uname -m)"
|
||||
case "$hardware" in
|
||||
x86_64)
|
||||
arch="x64"
|
||||
;;
|
||||
arm*)
|
||||
arch="arm"
|
||||
;;
|
||||
*)
|
||||
abort "Unsupported CPU architecture $hardware."
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
platform="darwin"
|
||||
if [[ "$arch" != "x64" ]]; then
|
||||
abort "Unsupported CPU architecture $hardware on macOS."
|
||||
fi
|
||||
;;
|
||||
Linux*)
|
||||
platform="linux"
|
||||
;;
|
||||
*)
|
||||
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
|
||||
;;
|
||||
esac
|
||||
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
|
||||
echo "Installing EAS CLI from $url"
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir"
|
||||
|
||||
echo
|
||||
echo "Downloading..."
|
||||
if [ "$(command -v curl)" ]
|
||||
then
|
||||
curl --location --progress-bar "$url" | tar xz
|
||||
else
|
||||
wget -O- "$url" | tar xz
|
||||
fi
|
||||
|
||||
# Move the installation over and link the executable (may require sudo)
|
||||
install
|
||||
|
||||
# Test the bundled node binary and if it doesn't work remove it (fall back to
|
||||
# node installed in path).
|
||||
"$prefix/lib/eas/bin/node" -v > /dev/null || {
|
||||
rm "$prefix/lib/eas/bin/node"
|
||||
}
|
||||
|
||||
# Test the CLI
|
||||
executable=$(command -v eas) || abort "Could not run eas after installation."
|
||||
echo
|
||||
echo -e "EAS CLI is installed to ${executable}"
|
||||
eas --version
|
||||
|
||||
echo
|
||||
echo -e "Type ${text_bold}eas --help${text_normal} to get started."
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
coverage:
|
||||
round: up
|
||||
precision: 2
|
||||
range: 30..100
|
||||
status:
|
||||
patch: false
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
eas-cli:
|
||||
informational: true
|
||||
paths:
|
||||
- packages/eas-cli
|
||||
eas-json:
|
||||
informational: true
|
||||
paths:
|
||||
- packages/eas-json
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB |
-142
@@ -1,142 +0,0 @@
|
||||
{"version": 2, "width": 100, "height": 32, "title": "eas simulator:control \u00b7 ordered config import \u00b7 local iOS", "env": {"TERM": "xterm-256color"}}
|
||||
[0.676555750076659, "o", "\u001b[?25l"]
|
||||
[0.6802548749838024, "o", "\u001b[36m⠋\u001b[39m Connecting to simulator session local-qa"]
|
||||
[0.7588533330708742, "o", "\u001b[2K"]
|
||||
[0.7608620420796797, "o", "\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session local-qa"]
|
||||
[0.8391176250297576, "o", "\u001b[2K"]
|
||||
[0.8414844170911238, "o", "\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session local-qa"]
|
||||
[0.9204518750775605, "o", "\u001b[2K\u001b[1G"]
|
||||
[0.922253750031814, "o", "\u001b[36m⠸\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.0015661250799894, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.0038247919874266, "o", "\u001b[36m⠼\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.0832262500189245, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.1625298330327496, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.163212166982703, "o", "\u001b[36m⠦\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.243104499997571, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.2453719170298427, "o", "\u001b[36m⠧\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.3232696250779554, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.325416125007905, "o", "\u001b[?25h"]
|
||||
[1.326131624984555, "o", "\u001b[32m✔\u001b[39m Connected to simulator session local-qa\r\n"]
|
||||
[1.326963625033386, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.3281740830279887, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m"]
|
||||
[1.3288150420412421, "i", "Import"]
|
||||
[1.3295168750919402, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.3299033750081435, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m I\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.330885292030871, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m Im\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m Imp\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4"]
|
||||
[1.331562792067416, "o", "mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m Impo\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON fil"]
|
||||
[1.3321025000186637, "o", "e.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m Impor\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90"]
|
||||
[1.3326134170638397, "o", "m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m Import\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the c"]
|
||||
[1.3330068750074133, "o", "urrent selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Import\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mImport configuration\u001b[39m\u001b[24m\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m"]
|
||||
[1.7557747500250116, "i", "\r"]
|
||||
[1.7560387500561774, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.758876375039108, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Import configuration\r\n\u001b[?25h"]
|
||||
[1.7595430000219494, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m \u001b7\u001b8"]
|
||||
[1.7601794170914218, "i", "/private/tmp/eas-control-evidence-20261016/import-native.json\r"]
|
||||
[1.7603633330436423, "o", "\u001b[2K\u001b[G"]
|
||||
[1.7615788329858333, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /p\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /pr\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /pri\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /priv\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /priva\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /privat\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/t\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tm\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private"]
|
||||
[1.762914124992676, "o", "/tmp/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/e\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/ea\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-c\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-co\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-con\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-cont\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-contr\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-contro\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control\u001b7\u001b8\u001b[2"]
|
||||
[1.764029583078809, "o", "K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-e\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-ev\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evi\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evid\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evide\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-eviden\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidenc\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-\u001b7\u001b8\u001b["]
|
||||
[1.765096583054401, "o", "2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-2\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-202\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-2026\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-202610\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-2026101\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[3"]
|
||||
[1.7662591250846162, "o", "6m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/i\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/im\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/imp\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/impo\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/impor\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-n\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/"]
|
||||
[1.7673499170923606, "o", "tmp/eas-control-evidence-20261016/import-na\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-nat\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-nati\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-nativ\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native.\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native.j\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native.js\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /priv"]
|
||||
[1.7683137920685112, "o", "ate/tmp/eas-control-evidence-20261016/import-native.jso\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native.json\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mConfiguration file\u001b[22m \u001b[90m…\u001b[39m /private/tmp/eas-control-evidence-20261016/import-native.json\u001b7\u001b8\r\n\u001b[?25h"]
|
||||
[1.8129357500001788, "o", "\u001b[?25l"]
|
||||
[1.8136624580947682, "o", "\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[1.8944749170914292, "o", "\u001b[2K"]
|
||||
[1.9007807080633938, "o", "\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[1.9756850000703707, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.9829818330472335, "o", "\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.0568826670059934, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.1380670000799, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.218690957990475, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.2998275420395657, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.3057220830814913, "o", "\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.3807418330106884, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.461088875075802, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.542084375047125, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.6231417920207605, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.6287869580555707, "o", "\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.704322708072141, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.7856317500118166, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.8666434170445427, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[2.947801083093509, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.0277905000839382, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.107919125002809, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.189039583085105, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.2705921250162646, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.278782333014533, "o", "\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.3510445000138134, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.4322339580394328, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.5133616670500487, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.594498125021346, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.675616000080481, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.7560152080841362, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.837074958020821, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.8441887920489535, "o", "\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.918185167014599, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.926536999992095, "o", "\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[3.99930900009349, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.005660333088599, "o", "\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.079819167032838, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.160742667037994, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.1672443330753595, "o", "\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.240931417094544, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.322033958043903, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.403169249999337, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.484215792035684, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.490052958019078, "o", "\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.56534637499135, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.571338042034768, "o", "\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.645086167030968, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.726108042057604, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.732906499993987, "o", "\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.807227333076298, "o", "\u001b[2K\u001b[1G"]
|
||||
[4.8134438750566915, "o", "\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.887950208038092, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[4.96876349998638, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.048057332984172, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.129674500087276, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.210538667044602, "o", "\u001b[2K\u001b[1G"]
|
||||
[5.216706792009063, "o", "\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.2916494170203805, "o", "\u001b[2K\u001b[1G"]
|
||||
[5.298674417077564, "o", "\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.372778458055109, "o", "\u001b[2K\u001b[1G"]
|
||||
[5.379554042010568, "o", "\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.452623042045161, "o", "\u001b[2K\u001b[1G"]
|
||||
[5.453131875023246, "o", "\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.533165167085826, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.614185417070985, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.695238917018287, "o", "\u001b[2K\u001b[1G"]
|
||||
[5.70190637500491, "o", "\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.775641417014413, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.856526207993738, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[5.937601250014268, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.018718332983553, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.099817875074223, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.17958258301951, "o", "\u001b[2K\u001b[1G"]
|
||||
[6.1802016670117155, "o", "\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.2606536250095814, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.341942458064295, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.422668375074863, "o", "\u001b[2K\u001b[1G"]
|
||||
[6.429952832986601, "o", "\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.50353070802521, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.584587333025411, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.665263750008307, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.745779792079702, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.825891082989983, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.9070447500562295, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[6.9881776249967515, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.0692853750661016, "o", "\u001b[2K\u001b[1G"]
|
||||
[7.075469500035979, "o", "\u001b[36m⠴\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.149777083075605, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.229895500000566, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.310974250081927, "o", "\u001b[2K\u001b[1G"]
|
||||
[7.317655625054613, "o", "\u001b[36m⠇\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.391714500030503, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Importing configuration on simulator session local-qa"]
|
||||
[7.399334124987945, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
|
||||
[7.400241167051718, "o", "\u001b[32m✔\u001b[39m Ran 7 actions on simulator session local-qa\r\n1. hardwareKeyboard.set: done\r\n2. hardwareKeyboard.get: off\r\n3. clipboard.write: {\"ok\":true}\r\n4. clipboard.read: {\"text\":\"EAS CLI ordered config import\"}\r\n5. media.add: done\r\n6. screenshot.capture: saved /private/tmp/eas-control-evidence-20261016/imported-screen.png\r\n7. hardwareKeyboard.set: done\r\n"]
|
||||
[7.401531666982919, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n Import configuration\u001b[90m - Run actions from a version 1 JSON file.\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n ↓ hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m"]
|
||||
[8.09189054206945, "i", "\r"]
|
||||
[8.092182000051253, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[8.10421729204245, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Exit\r\n\u001b[?25h"]
|
||||
[8.105274667032063, "o", "\u001b[?25h"]
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 356 KiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -1,114 +0,0 @@
|
||||
{"version": 2, "width": 100, "height": 30, "title": "Interactive EAS simulator control \u00b7 owned iPhone 17 Pro \u00b7 local adapter", "env": {"TERM": "xterm-256color"}}
|
||||
[0.7721600420773029, "o", "(node:87001) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.\r\n(Use `node --trace-deprecation ...` to show where the warning was created)\r\n"]
|
||||
[0.7799546250607818, "o", "\u001b[?25l"]
|
||||
[0.7805876249913126, "o", "\u001b[36m⠋\u001b[39m Connecting to simulator session local-qa"]
|
||||
[0.8612586669623852, "o", "\u001b[2K"]
|
||||
[0.8686974999727681, "o", "\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session local-qa"]
|
||||
[0.9416937920032069, "o", "\u001b[2K"]
|
||||
[0.9488202499924228, "o", "\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.0221677500521764, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.1033293750369921, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.1844352920306846, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session local-qa"]
|
||||
[1.2452304170001298, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.2458177920198068, "o", "\u001b[?25h"]
|
||||
[1.246452167048119, "o", "\u001b[32m✔\u001b[39m Connected to simulator session local-qa\r\n"]
|
||||
[1.2471100830007344, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m"]
|
||||
[1.267376041971147, "i", "hardwareKeyboard.set"]
|
||||
[1.2677512080408633, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m"]
|
||||
[1.2873269580304623, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m h\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoo"]
|
||||
[1.2878355419961736, "o", "se a simulator action\u001b[22m \u001b[90m…\u001b[39m ha\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m har\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m -"]
|
||||
[1.288349875016138, "o", " Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n "]
|
||||
[1.2888687499798834, "o", " clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardw\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text i"]
|
||||
[1.2893870420521125, "o", "nto the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light"]
|
||||
[1.2898918749997392, "o", " or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whe"]
|
||||
[1.2904068749630824, "o", "ther the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware ke"]
|
||||
[1.2910350420279428, "o", "yboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareK\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1"]
|
||||
[1.2916030830238014, "o", "A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKe\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[3"]
|
||||
[1.2922136670676991, "o", "9m hardwareKey\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyb\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clip"]
|
||||
[1.2927493749884889, "o", "board text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeybo\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboar"]
|
||||
[1.2933355419663712, "o", "d.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text int"]
|
||||
[1.2938686250708997, "o", "o the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Rea"]
|
||||
[1.294383833068423, "o", "d light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get"]
|
||||
[1.2948922920040786, "o", "\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or d"]
|
||||
[1.2954250000184402, "o", "isconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.s\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b"]
|
||||
[1.295944832963869, "o", "[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.se\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1"]
|
||||
[1.2964522499823943, "o", "mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.set\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardwareKeyboard.set\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mhardwareKey"]
|
||||
[1.2964961250545457, "o", "board.set\u001b[39m\u001b[24m\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m"]
|
||||
[1.67176629207097, "i", "\r"]
|
||||
[1.672128958045505, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardwareKeyboard.set\r\n"]
|
||||
[1.680720292031765, "o", "\u001b[?25h"]
|
||||
[1.6816639580065385, "o", "\u001b[?25l"]
|
||||
[1.6822952500078827, "o", "\u001b[36m?\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m \u001b[90m- Use arrow-keys. Return to submit.\u001b[39m\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mon\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n off\u001b[90m\u001b[39m\r\n"]
|
||||
[1.6962782919872552, "i", "\u001b[B"]
|
||||
[1.696546999970451, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.6966472920030355, "o", "\u001b[36m?\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m \u001b[90m- Use arrow-keys. Return to submit.\u001b[39m\r\n on\u001b[90m\u001b[39m\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4moff\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n"]
|
||||
[1.7997166250133887, "i", "\r"]
|
||||
[1.799987707985565, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[1.8083316250704229, "o", "\u001b[32m✔\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m off\r\n\u001b[?25h"]
|
||||
[1.808870500070043, "o", "\u001b[?25l"]
|
||||
[1.8094840829726309, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[1.8907162500545382, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[1.9710942920064554, "o", "\u001b[2K\u001b[1G"]
|
||||
[1.98023533297237, "o", "\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.05221470806282, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.0617807500530034, "o", "\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.1333555419696495, "o", "\u001b[2K"]
|
||||
[2.1427700000349432, "o", "\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.21452604199294, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.2228643749840558, "o", "\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.2957063750363886, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.37681112496648, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.4576392080634832, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.5387683330336586, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.549614083021879, "o", "\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.619110041996464, "o", "\u001b[2K\u001b[1G"]
|
||||
[2.6283346250420436, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.6995909999823198, "o", "\u001b[2K"]
|
||||
[2.7077512920368463, "o", "\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set on simulator session local-qa"]
|
||||
[2.7452095000771806, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
|
||||
[2.7458574579795823, "o", "\u001b[32m✔\u001b[39m Ran hardwareKeyboard.set on simulator session local-qa\r\n"]
|
||||
[2.7610850000055507, "i", "hardwareKeyboard.get"]
|
||||
[2.7617503750370815, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m"]
|
||||
[2.762284707976505, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m h\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoo"]
|
||||
[2.7627869580173865, "o", "se a simulator action\u001b[22m \u001b[90m…\u001b[39m ha\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m har\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m -"]
|
||||
[2.76329266699031, "o", " Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n "]
|
||||
[2.7637974170502275, "o", " clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardw\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text i"]
|
||||
[2.764298875001259, "o", "nto the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light"]
|
||||
[2.7648017500760034, "o", " or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whe"]
|
||||
[2.765301250037737, "o", "ther the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware ke"]
|
||||
[2.765805625007488, "o", "yboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareK\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1"]
|
||||
[2.7663046249654144, "o", "A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKe\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[3"]
|
||||
[2.766807375010103, "o", "9m hardwareKey\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyb\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clip"]
|
||||
[2.767315083066933, "o", "board text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeybo\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboar"]
|
||||
[2.7677770419977605, "o", "d.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text int"]
|
||||
[2.7682648750487715, "o", "o the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Rea"]
|
||||
[2.7687851670198143, "o", "d light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get"]
|
||||
[2.7692870419705287, "o", "\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or d"]
|
||||
[2.7697974579641595, "o", "isconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.g\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b"]
|
||||
[2.770301542012021, "o", "[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.ge\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1"]
|
||||
[2.7708002920262516, "o", "mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.get\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardwareKeyboard.get\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mhardwareKey"]
|
||||
[2.7708483750466257, "o", "board.get\u001b[39m\u001b[24m\u001b[90m\r\n Read whether the hardware keyboard is connected.\u001b[39m"]
|
||||
[3.165473875007592, "i", "\r"]
|
||||
[3.165730374981649, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[3.1756717080716044, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardwareKeyboard.get\r\n\u001b[?25h"]
|
||||
[3.17622929206118, "o", "\u001b[?25l"]
|
||||
[3.1767272499855608, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.25785704201553, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.3389085000380874, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.3505813750671223, "o", "\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.420439667068422, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.5016636250074953, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.582632292062044, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.6637173750204965, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.6741337080020458, "o", "\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.744791708071716, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.75589716702234, "o", "\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.8250304580433294, "o", "\u001b[2K\u001b[1G"]
|
||||
[3.836755375028588, "o", "\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get on simulator session local-qa"]
|
||||
[3.8945711670676246, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
|
||||
[3.895249707973562, "o", "\u001b[32m✔\u001b[39m Ran hardwareKeyboard.get on simulator session local-qa\r\noff\r\n"]
|
||||
[3.8962680420372635, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n clipboard.read\u001b[90m - Read simulator clipboard text.\u001b[39m\r\n clipboard.write\u001b[90m - Write simulator clipboard text without pasting.\u001b[39m\r\n clipboard.copy\u001b[90m - Copy the current selection and read its text.\u001b[39m\r\n clipboard.paste\u001b[90m\r\n Paste text into the focused field, or paste the current simulator clipboard if omitted.\u001b[39m\r\n appearance.get\u001b[90m - Read light or dark mode.\u001b[39m\r\n appearance.set\u001b[90m - Set light or dark mode.\u001b[39m\r\n hardwareKeyboard.get\u001b[90m - Read whether the hardware keyboard is connected.\u001b[39m\r\n hardwareKeyboard.set\u001b[90m - Connect or disconnect the hardware keyboard.\u001b[39m\r\n ↓ reduceMotion.get\u001b[90m - Read Reduce Motion.\u001b[39m"]
|
||||
[4.130599374999292, "i", "\r"]
|
||||
[4.13118145801127, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
|
||||
[4.14170804200694, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Exit\r\n\u001b[?25h"]
|
||||
[4.142687042010948, "o", "\u001b[?25h"]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 306 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
projects: [
|
||||
require('./packages/eas-cli/jest.config.js'),
|
||||
require('./packages/eas-json/jest.config.js'),
|
||||
],
|
||||
testPathIgnorePatterns: ['.*'],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.52.0"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build-for-size-check",
|
||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "16.7.8",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-config-universe": "10.0.0",
|
||||
"eslint-plugin-async-protect": "1.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "27.5.1",
|
||||
"jest-watch-typeahead": "1.0.0",
|
||||
"lerna": "4.0.0",
|
||||
"oclif": "2.6.3",
|
||||
"prettier": "2.6.1",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.10.0",
|
||||
"yarn": "1.22.15"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
const rootEslintrc = require('../../.eslintrc.js');
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
...rootEslintrc.rules['no-restricted-imports'][1].paths,
|
||||
{
|
||||
name: 'ora',
|
||||
message: 'Import ora from src/ora.ts instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'graphql/template-strings': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/named-operations': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/required-fields': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
requiredFields: ['id'],
|
||||
},
|
||||
],
|
||||
'graphql/capitalized-type-name': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['graphql'],
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
graphql.schema.json linguist-generated
|
||||
src/graphql/generated.ts linguist-generated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync('/tmp');
|
||||
if (process.env.TMPDIR) {
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
|
||||
}
|
||||
|
||||
// fs-extra@10 is not compatible with memfs, the following line fixes tests
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
@@ -0,0 +1 @@
|
||||
export default jest.fn(() => ({}));
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
OUTFILE="src/graphql/generated.ts"
|
||||
|
||||
content="/**
|
||||
* This file was generated using GraphQL Codegen
|
||||
* Command: yarn generate-graphql-code
|
||||
* Run this during development for automatic type generation when editing GraphQL documents
|
||||
* For more info and docs, visit https://graphql-code-generator.com/
|
||||
*/
|
||||
"
|
||||
|
||||
printf '%s\n%s\n' "$content" "$(cat $OUTFILE)" > $OUTFILE
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core');
|
||||
const path = require('path');
|
||||
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json');
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
require('ts-node').register({ project });
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.disableJsonFlag = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/core')
|
||||
.run()
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@tsconfig/node16/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
overwrite: true
|
||||
schema: 'http://localhost:3000/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
config:
|
||||
dedupeOperationSuffix: true
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- ./annotate-graphql-codegen.sh
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- 'introspection'
|
||||
Generated
+26723
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
...require('../../jest/jest.config.js'),
|
||||
rootDir: __dirname,
|
||||
roots: ['src', '__mocks__'],
|
||||
globalSetup: './jest/global-setup.ts',
|
||||
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.EAS_NO_VCS = '1';
|
||||
process.env.EAS_PROJECT_ROOT = '/app';
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.52.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.31",
|
||||
"@expo/code-signing-certificates": "0.0.2",
|
||||
"@expo/config": "6.0.21",
|
||||
"@expo/config-plugins": "4.1.2",
|
||||
"@expo/config-types": "44.0.0",
|
||||
"@expo/eas-build-job": "0.2.71",
|
||||
"@expo/eas-json": "0.52.0",
|
||||
"@expo/json-file": "8.2.34",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/package-manager": "0.0.50",
|
||||
"@expo/pkcs12": "0.0.7",
|
||||
"@expo/plist": "0.0.17",
|
||||
"@expo/plugin-warn-if-update-available": "2.1.0",
|
||||
"@expo/prebuild-config": "3.1.2",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.6.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.6.4",
|
||||
"@oclif/plugin-autocomplete": "1.2.0",
|
||||
"@oclif/plugin-help": "5.1.12",
|
||||
"@urql/core": "2.4.3",
|
||||
"@urql/exchange-retry": "0.3.2",
|
||||
"adm-zip": "0.5.9",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.10.0",
|
||||
"cli-table3": "0.6.1",
|
||||
"dateformat": "4.6.3",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.11",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.0.1",
|
||||
"getenv": "1.0.0",
|
||||
"got": "^11.1.4",
|
||||
"gradle-to-js": "2.0.0",
|
||||
"graphql": "16.3.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"ignore": "5.2.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"joi": "17.6.0",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "3.0.5",
|
||||
"nanoid": "3.3.2",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"pnp-webpack-plugin": "1.7.0",
|
||||
"react-dev-utils": "12.0.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.5",
|
||||
"slash": "3.0.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"ts-loader": "^9.2.8",
|
||||
"tslib": "2.3.1",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"webpack": "5.72.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.6.2",
|
||||
"@graphql-codegen/introspection": "2.1.1",
|
||||
"@graphql-codegen/typescript": "2.4.8",
|
||||
"@graphql-codegen/typescript-operations": "2.3.5",
|
||||
"@types/cli-progress": "3.9.2",
|
||||
"@types/adm-zip": "0.4.34",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "1.0.1",
|
||||
"@types/prompts": "2.0.14",
|
||||
"@types/semver": "7.3.9",
|
||||
"@types/tar": "6.1.1",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/webpack": "5.28.0",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.26.1",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"memfs": "3.4.1",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.4",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "2.0.1",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/build",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"keywords": [
|
||||
"expo",
|
||||
"eas",
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"oclif": {
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@oclif/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
],
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage account"
|
||||
},
|
||||
"branch": {
|
||||
"description": "manage update branches"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
},
|
||||
"channel": {
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage Apple devices for Internal Distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
},
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
"webhook": {
|
||||
"description": "manage webhooks"
|
||||
}
|
||||
},
|
||||
"warn-if-update-available": {
|
||||
"timeoutInDays": 0.5,
|
||||
"message": "<%= chalk.bold('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available, please upgrade.') %>\n<%= chalk.dim('Proceeding with outdated version') %>\n"
|
||||
},
|
||||
"update": {
|
||||
"node": {
|
||||
"version": "12.13.0"
|
||||
},
|
||||
"s3": {
|
||||
"templates": {
|
||||
"target": {
|
||||
"unversioned": "<%- bin %>-<%- platform %>-<%- arch %><%- ext %>",
|
||||
"versioned": "<%- bin %>-<%- platform %>-<%- arch %><%- ext %>"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"-h"
|
||||
],
|
||||
"additionalVersionFlags": [
|
||||
"-v"
|
||||
]
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "./scripts/prepack.sh",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"clean": "rm -rf dist build tmp node_modules yarn-error.log"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
(async () => {
|
||||
// Patch `oclif readme` path and link generation
|
||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||
await fs.writeFile('README.md', readmeContent);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Patched README path generation');
|
||||
})();
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
rm -rf build
|
||||
yarn build
|
||||
|
||||
if [ -z "$CLI_SIZE_CHECK" ]; then
|
||||
yarn oclif manifest
|
||||
fi
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
EAS_CLI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
PACKAGE_JSON_PATH="$EAS_CLI_DIR/package.json"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
else
|
||||
sed -i "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
export const ora = jest.fn().mockReturnValue({
|
||||
start: jest.fn().mockReturnValue({
|
||||
fail: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
}),
|
||||
stop: jest.fn(),
|
||||
stopAndPersist: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
|
||||
import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
|
||||
|
||||
describe('apiClient', () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl())
|
||||
.post('/v2/test')
|
||||
.reply(400, {
|
||||
errors: [
|
||||
{
|
||||
message: 'hellomessage',
|
||||
code: 'TEST_CODE',
|
||||
stack: 'line 1: hello',
|
||||
details: { who: 'world' },
|
||||
metadata: { an: 'object' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await api.postAsync('test', { body: {} });
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(ApiV2Error);
|
||||
assert(error instanceof ApiV2Error);
|
||||
|
||||
expect(error.message).toEqual('hellomessage');
|
||||
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
|
||||
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
|
||||
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
|
||||
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await api.postAsync('test', { body: {} });
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).not.toBeInstanceOf(ApiV2Error);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
interface MockProject {
|
||||
projectRoot: string;
|
||||
projectTree: Record<string, string>;
|
||||
appJSON: AppJSON;
|
||||
packageJSON: PackageJSON;
|
||||
}
|
||||
|
||||
interface PackageJSON {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
main: string;
|
||||
}
|
||||
|
||||
interface AppJSON {
|
||||
expo: {
|
||||
name: string;
|
||||
version: string;
|
||||
slug: string;
|
||||
sdkVersion: string;
|
||||
owner?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestProject(
|
||||
user: Actor,
|
||||
appJSONExtraData?: Record<string, object | string>
|
||||
): MockProject {
|
||||
const projectRoot = '/test-project';
|
||||
const packageJSON: PackageJSON = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const owner = appJSONExtraData?.owner as string | undefined;
|
||||
const appJSON: AppJSON = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: owner ?? (user.__typename === 'User' ? user.username : undefined),
|
||||
...appJSONExtraData,
|
||||
},
|
||||
};
|
||||
return {
|
||||
appJSON,
|
||||
packageJSON,
|
||||
projectRoot,
|
||||
projectTree: {
|
||||
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
|
||||
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Analytics, Event } from './events';
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
|
||||
export async function withAnalyticsAsync<Result>(
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
attemptEvent: Event;
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
trackingCtx: TrackingContext;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { logEvent } from './rudderstackClient';
|
||||
export type Event = BuildEvent | SubmissionEvent;
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export class Analytics {
|
||||
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||
logEvent(name, properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
traits: Record<string, any>;
|
||||
} | null = null;
|
||||
|
||||
export async function initAsync(): Promise<void> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||
}
|
||||
// TODO: cut here
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync('analyticsEnabled', false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||
if (analyticsEnabled) {
|
||||
const config =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||
const deviceId = savedDeviceId ?? uuidv4();
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
traits,
|
||||
};
|
||||
|
||||
ensureUserIdentified();
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||
if (!rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
function ensureUserIdentified(): void {
|
||||
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
|
||||
import fetch, { RequestError, RequestInit } from './fetch';
|
||||
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||
|
||||
export class ApiV2Error extends Error {
|
||||
readonly name = 'ApiV2Error';
|
||||
readonly expoApiV2ErrorCode: string;
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
|
||||
constructor(response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}) {
|
||||
super(response.message);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
body: JSONValue;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async putAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
|
||||
},
|
||||
|
||||
async postAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
|
||||
},
|
||||
|
||||
async deleteAsync(path: string): Promise<any> {
|
||||
return await requestAsync(path, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async getAsync(path: string): Promise<any> {
|
||||
return await requestAsync(path, { method: 'GET' });
|
||||
},
|
||||
};
|
||||
|
||||
async function requestAsync(path: string, options: RequestInit): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
await handleApiErrorAsync(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApiErrorAsync(err: any): Promise<void> {
|
||||
if (err instanceof RequestError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = (await err.response.json()) as { [key: string]: any };
|
||||
} catch {
|
||||
throw new Error(`Malformed api response: ${await err.response.text()}`);
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
throw new ApiV2Error(result.errors[0]);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
return { authorization: `Bearer ${token}` };
|
||||
}
|
||||
const sessionSecret = getSessionSecret();
|
||||
if (sessionSecret) {
|
||||
return { 'expo-session': sessionSecret };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getExpoApiBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging-api.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://127.0.0.1:3000`;
|
||||
} else {
|
||||
return `https://api.expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getExpoWebsiteBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://expo.test`;
|
||||
} else {
|
||||
return `https://expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return new URL(projectId, `https://staging-u.expo.dev`).href;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
|
||||
} else {
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('throws an informative error when multiple flavors are detected in the android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(124);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(124);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
|
||||
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(127);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(127);
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
});
|
||||
|
||||
describe(maybeResolveVersionsAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('3.0.0');
|
||||
expect(appBuildVersion).toBe('123');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the versions from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('5.0.0');
|
||||
expect(appBuildVersion).toBe('126');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
versionCode?: number;
|
||||
versionName?: string;
|
||||
} = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '3.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '5.0.0',
|
||||
android: {
|
||||
versionCode: 126,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {} as any,
|
||||
}),
|
||||
},
|
||||
'/multiflavor'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
|
||||
buildProfile.gradleCommand
|
||||
)}) includes string 'bundle'.
|
||||
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error('Please update eas.json and come back again.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
|
||||
return { applicationId, gradleContext };
|
||||
}
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, jobData);
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
|
||||
return !ctx.buildProfile.withoutCredentials;
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
});
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
gradleCommand: job.gradleCommand,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
|
||||
if (buildType === Android.BuildType.APK) {
|
||||
return AndroidBuildType.Apk;
|
||||
} else {
|
||||
return AndroidBuildType.AppBundle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
keystore: {
|
||||
dataBase64: credentials.keystore.keystore,
|
||||
keystorePassword: credentials.keystore.keystorePassword,
|
||||
keyAlias: credentials.keystore.keyAlias,
|
||||
keyPassword: credentials.keystore.keyPassword,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
buildType,
|
||||
username,
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: BuildProfile<Platform.ANDROID>;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'versionCode') {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this after a few months
|
||||
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
|
||||
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
|
||||
if (await fs.pathExists(easBuildGradlePath)) {
|
||||
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
|
||||
const buildGradleContentsWithoutApply = buildGradleContents.replace(
|
||||
/apply from: ["'].\/eas-build.gradle["']\n/,
|
||||
''
|
||||
);
|
||||
if (buildGradleContentsWithoutApply !== buildGradleContents) {
|
||||
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
getAppBuildGradleAsync,
|
||||
parseGradleCommand,
|
||||
resolveConfigValue,
|
||||
} from '../../project/android/gradleUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
VERSION_CODE,
|
||||
NOOP,
|
||||
}
|
||||
|
||||
export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
const isMultiFlavor =
|
||||
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
|
||||
if (isMultiFlavor) {
|
||||
throw new Error(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
}
|
||||
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
});
|
||||
Log.log('Synchronized versions with build gradle');
|
||||
}
|
||||
|
||||
export async function bumpVersionInAppJsonAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
bumpStrategy: BumpStrategy;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const versionCode = AndroidConfig.Version.getVersionCode(exp);
|
||||
const bumpedVersionCode = versionCode + 1;
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
|
||||
versionCode
|
||||
)} to ${chalk.bold(bumpedVersionCode)}`
|
||||
);
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.android = { ...config.android, versionCode: bumpedVersionCode };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
try {
|
||||
const parsedGradleCommand = buildProfile.gradleCommand
|
||||
? parseGradleCommand(buildProfile.gradleCommand, buildGradle)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appVersion:
|
||||
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
|
||||
appVersion: exp.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const buildGradle = await readBuildGradleAsync(projectDir);
|
||||
if (!buildGradle) {
|
||||
throw new Error('This project is missing a build.gradle file.');
|
||||
}
|
||||
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
|
||||
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
|
||||
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
|
||||
return updatedBuildGradle;
|
||||
}
|
||||
|
||||
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!(await fs.pathExists(buildGradlePath))) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.readFile(buildGradlePath, 'utf8');
|
||||
}
|
||||
|
||||
async function writeBuildGradleAsync({
|
||||
projectDir,
|
||||
buildGradle,
|
||||
}: {
|
||||
projectDir: string;
|
||||
buildGradle: string;
|
||||
}): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
await fs.writeFile(buildGradlePath, buildGradle);
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import cliProgress from 'cli-progress';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { BuildEvent } from '../analytics/events';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { Ora, ora } from '../ora';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
export interface CredentialsResult<Credentials> {
|
||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
TJob extends Job
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
|
||||
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = ctx.localBuildOptions.enable
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (ctx.localBuildOptions.enable) {
|
||||
await runLocalBuildAsync(job, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<string> {
|
||||
let projectTarballPath;
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
const projectTarball = await makeProjectTarballAsync();
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const { bucketKey } = await uploadAsync(
|
||||
UploadSessionType.EasBuildProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) =>
|
||||
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`,
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
if (projectTarballPath) {
|
||||
await fs.remove(projectTarballPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
}
|
||||
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||
{
|
||||
// 2 hours (max build time limit) + 10 minutes (possible queue time)
|
||||
timeoutSec = 2 * 60 * 60 + 10 * 60,
|
||||
intervalSec = 10,
|
||||
} = {}
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
const b = `build${buildIds.length > 1 ? 's' : ''}`;
|
||||
Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
|
||||
const spinner = ora(`Waiting for ${b} to complete.`).start();
|
||||
const endTime = new Date().getTime() + timeoutSec * 1000;
|
||||
while (new Date().getTime() <= endTime) {
|
||||
const builds = await getBuildsSafelyAsync(buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
||||
: await handleMultipleBuildsProgressAsync({ builds }, { spinner });
|
||||
if (!refetch) {
|
||||
return builds;
|
||||
}
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
}
|
||||
spinner.fail('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
|
||||
const promises = buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
interface BuildProgressResult {
|
||||
refetch: boolean;
|
||||
}
|
||||
|
||||
let queueProgressBarStarted = false;
|
||||
const queueProgressBar = new cliProgress.SingleBar(
|
||||
{ format: '|{bar}| {estimatedWaitTime}' },
|
||||
cliProgress.Presets.rect
|
||||
);
|
||||
|
||||
async function handleSingleBuildProgressAsync(
|
||||
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
if (build === null) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
|
||||
if (build.status === BuildStatus.InProgress) {
|
||||
queueProgressBar.update(queueProgressBar.getTotal(), {
|
||||
estimatedWaitTime: '',
|
||||
});
|
||||
}
|
||||
queueProgressBar.stop();
|
||||
Log.newLine();
|
||||
queueProgressBarStarted = false;
|
||||
spinner.start('Build is about to start');
|
||||
}
|
||||
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
case BuildStatus.InQueue: {
|
||||
spinner.text = 'Build queued...';
|
||||
const progressBarPayload =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
|
||||
: { estimatedWaitTime: '' };
|
||||
|
||||
if (
|
||||
!queueProgressBarStarted &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
spinner.stopAndPersist();
|
||||
if (build.priority !== BuildPriority.High) {
|
||||
Log.newLine();
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log('Waiting in queue');
|
||||
queueProgressBar.start(
|
||||
build.initialQueuePosition + 1,
|
||||
build.initialQueuePosition - build.queuePosition + 1,
|
||||
progressBarPayload
|
||||
);
|
||||
queueProgressBarStarted = true;
|
||||
}
|
||||
if (typeof build.queuePosition === 'number') {
|
||||
queueProgressBar.update(build.queuePosition, progressBarPayload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BuildStatus.Canceled:
|
||||
spinner.fail('Build canceled');
|
||||
return { refetch: false };
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return { refetch: false };
|
||||
} else {
|
||||
throw new Error('Standalone build failed!');
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
const statusToDisplayName: Record<BuildStatus, string> = {
|
||||
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
|
||||
[BuildStatus.InQueue]: 'in queue',
|
||||
[BuildStatus.InProgress]: 'in progress',
|
||||
[BuildStatus.Canceled]: 'canceled',
|
||||
[BuildStatus.Finished]: 'finished',
|
||||
[BuildStatus.Errored]: 'failed',
|
||||
};
|
||||
|
||||
const platforms = [AppPlatform.Android, AppPlatform.Ios];
|
||||
|
||||
async function handleMultipleBuildsProgressAsync(
|
||||
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
const buildCount = maybeBuilds.length;
|
||||
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
|
||||
|
||||
const allFinished =
|
||||
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
|
||||
const allSettled =
|
||||
builds.filter(build =>
|
||||
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
|
||||
).length === buildCount;
|
||||
|
||||
if (allSettled) {
|
||||
if (allFinished) {
|
||||
spinner.succeed(formatSettledBuildsText(builds));
|
||||
} else {
|
||||
spinner.fail(formatSettledBuildsText(builds));
|
||||
}
|
||||
return { refetch: false };
|
||||
} else {
|
||||
spinner.text = formatPendingBuildsText(builds);
|
||||
return { refetch: true };
|
||||
}
|
||||
}
|
||||
|
||||
function formatSettledBuildsText(builds: BuildFragment[]): string {
|
||||
return platforms
|
||||
.map(platform => {
|
||||
const build = nullthrows(
|
||||
builds.find(build => build.platform === platform),
|
||||
`Build for platform ${platform} must be defined in this context`
|
||||
);
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
|
||||
})
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
function formatPendingBuildsText(builds: BuildFragment[]): string {
|
||||
return platforms
|
||||
.map(platform => {
|
||||
const build = builds.find(build => build.platform === platform);
|
||||
const status = build ? statusToDisplayName[build.status] : 'unknown';
|
||||
let extraInfo = '';
|
||||
if (
|
||||
build?.status === BuildStatus.InQueue &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
const percent = Math.floor(
|
||||
((build.initialQueuePosition - build.queuePosition + 1) /
|
||||
(build.initialQueuePosition + 1)) *
|
||||
100
|
||||
);
|
||||
const estimatedWaitTime =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
|
||||
: '';
|
||||
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
|
||||
}
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(status)}${extraInfo}`;
|
||||
})
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
|
||||
return maybeBuild !== null;
|
||||
}
|
||||
|
||||
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
|
||||
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
|
||||
return 'starting soon...';
|
||||
} else {
|
||||
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
|
||||
return `starting in about ${n}0 minutes...`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/settings/subscriptions`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonReader } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
interface ConfigureParams {
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates eas.json if it does not exist.
|
||||
*
|
||||
* Returns:
|
||||
* - false - if eas.json already exists
|
||||
* - true - if eas.json was created by the function
|
||||
*/
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
configureParams: ConfigureParams
|
||||
): Promise<boolean> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await configureAsync(configureParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
await createEasJsonAsync(projectDir);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const easJson =
|
||||
hasAndroidNativeProject || hasIosNativeProject
|
||||
? EAS_JSON_BARE_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
import { Actor } from '../user/User';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
}
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
localBuildOptions,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
return {
|
||||
...common,
|
||||
android: await createAndroidContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
} else {
|
||||
const common = commonContext as CommonContext<Platform.IOS>;
|
||||
return {
|
||||
...common,
|
||||
ios: await createIosContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
|
||||
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
|
||||
if (archiveSource.type === ArchiveSourceType.S3) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.S3,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
url: archiveSource.url,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported project archive source type: '${archiveSource.type}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
iosEnterpriseProvisioning:
|
||||
metadata.iosEnterpriseProvisioning &&
|
||||
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
|
||||
};
|
||||
}
|
||||
|
||||
function transformCredentialsSource(
|
||||
credentialsSource: Metadata['credentialsSource']
|
||||
): BuildCredentialsSource {
|
||||
if (credentialsSource === 'local') {
|
||||
return BuildCredentialsSource.Local;
|
||||
} else {
|
||||
return BuildCredentialsSource.Remote;
|
||||
}
|
||||
}
|
||||
|
||||
function transformDistribution(distribution: Metadata['distribution']): DistributionType {
|
||||
if (distribution === 'internal') {
|
||||
return DistributionType.Internal;
|
||||
} else if (distribution === 'simulator') {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return DistributionType.Store;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
return BuildWorkflow.Generic;
|
||||
} else {
|
||||
return BuildWorkflow.Managed;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformIosEnterpriseProvisioning(
|
||||
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
|
||||
): BuildIosEnterpriseProvisioning {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return BuildIosEnterpriseProvisioning.Adhoc;
|
||||
} else {
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
evaluateTemplateString,
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(evaluateTemplateString, () => {
|
||||
it('evaluates the template string when value is a number', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when value is a string', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when template is not the only element', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
it('evaluates the template string when template value is double quoted', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
|
||||
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
||||
},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info2.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initBareWorkflowProject({
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
});
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(getInfoPlistPath, () => {
|
||||
it('returns default path if INFOPLIST_FILE is not specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {});
|
||||
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
|
||||
});
|
||||
it('returns INFOPLIST_FILE if specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates SRCROOT in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates BuildSettings in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
|
||||
TARGET_NAME: 'NotInfo',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
version = '1',
|
||||
infoPlistName = 'Info.plist',
|
||||
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${appVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
xcodeBuildContext,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
|
||||
|
||||
return {
|
||||
bundleIdentifier: applicationTarget.bundleIdentifier,
|
||||
applicationTarget,
|
||||
applicationTargetBuildSettings,
|
||||
targets,
|
||||
xcodeBuildContext,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
buildSettings: ctx.ios.applicationTargetBuildSettings,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBuildSettings(
|
||||
ctx: CommonContext<Platform.IOS>,
|
||||
applicationTarget: Target
|
||||
): XCBuildConfiguration['buildSettings'] {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return {};
|
||||
}
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
return xcBuildConfiguration?.buildSettings ?? {};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Env, Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
scheme: job.scheme,
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
developmentClient: job.developmentClient,
|
||||
simulator: job.simulator,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
env?: Env;
|
||||
}): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
|
||||
for (const targetName of Object.keys(secrets.buildCredentials ?? {})) {
|
||||
buildCredentials.push({
|
||||
targetName,
|
||||
provisioningProfileBase64: nullthrows(secrets.buildCredentials?.[targetName])
|
||||
.provisioningProfileBase64,
|
||||
distributionCertificate: nullthrows(secrets.buildCredentials?.[targetName])
|
||||
.distributionCertificate,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
buildCredentials,
|
||||
environmentSecrets: secrets.env,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
|
||||
}
|
||||
}
|
||||
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: ctx.buildProfile.developmentClient,
|
||||
simulator: ctx.buildProfile.simulator,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
username,
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certificateP12,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: BuildProfile<Platform.IOS>;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'buildNumber') {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync } from '../utils/version';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
BUILD_NUMBER,
|
||||
NOOP,
|
||||
}
|
||||
|
||||
export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
|
||||
Log.log('Synchronized versions with Info.plist');
|
||||
}
|
||||
|
||||
export async function bumpVersionInAppJsonAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
bumpStrategy: BumpStrategy;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = IOSConfig.Version.getVersion(exp);
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
|
||||
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
|
||||
const comps = buildNumber.split('.');
|
||||
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
|
||||
const bumpedBuildNumber = comps.join('.');
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
|
||||
buildNumber
|
||||
)} to ${chalk.bold(bumpedBuildNumber)}`
|
||||
);
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
|
||||
});
|
||||
} else {
|
||||
Log.log(`${chalk.bold('expo.ios.buildNumber')} = ${chalk.bold(buildNumber)} is not a number`);
|
||||
const { bumpedBuildNumber } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'bumpedBuildNumber',
|
||||
message: 'What is the next build number?',
|
||||
});
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function readShortVersionAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
try {
|
||||
return {
|
||||
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
|
||||
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
|
||||
};
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.debug(err);
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<IOSConfig.InfoPlist> {
|
||||
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
|
||||
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
|
||||
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
export function getInfoPlistPath(
|
||||
projectDir: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
if (buildSettings.INFOPLIST_FILE) {
|
||||
const infoPlistFile = buildSettings.INFOPLIST_FILE.startsWith('"')
|
||||
? buildSettings.INFOPLIST_FILE.slice(1, -1)
|
||||
: buildSettings.INFOPLIST_FILE;
|
||||
const iosDir = path.join(projectDir, 'ios');
|
||||
const plistPath = evaluateTemplateString(infoPlistFile, {
|
||||
...buildSettings,
|
||||
SRCROOT: iosDir,
|
||||
});
|
||||
return path.isAbsolute(plistPath) ? plistPath : path.resolve(iosDir, plistPath);
|
||||
} else {
|
||||
return IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(
|
||||
projectDir: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
projectDir,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
await writePlistAsync(infoPlistPath, infoPlist);
|
||||
}
|
||||
|
||||
function ensureStaticConfigExists(projectDir: string): void {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
throw new Error('autoIncrement option is not supported when using app.config.js');
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(
|
||||
s: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
|
||||
const vars = { ...buildSettings };
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => {
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
const value = String(vars[key]);
|
||||
return value.startsWith('"') ? value.slice(1, -1) : value;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.78';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
artifactPath?: string;
|
||||
workingdir?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job);
|
||||
let spinner;
|
||||
if (!options.verbose) {
|
||||
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
|
||||
}
|
||||
let childProcess: ChildProcess | undefined;
|
||||
const interruptHandler = (): void => {
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
}
|
||||
};
|
||||
process.on('SIGINT', interruptHandler);
|
||||
try {
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: options.verbose ? 'inherit' : 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
|
||||
},
|
||||
});
|
||||
childProcess = spawnPromise.child;
|
||||
await spawnPromise;
|
||||
} finally {
|
||||
process.removeListener('SIGINT', interruptHandler);
|
||||
spinner?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
|
||||
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
|
||||
return {
|
||||
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
|
||||
args: [jobBase64],
|
||||
};
|
||||
} else {
|
||||
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
|
||||
if (await isAtLeastNpm7Async()) {
|
||||
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
|
||||
// prompting the user to install a package that is used for the first time
|
||||
args.unshift('-y');
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function isAtLeastNpm7Async(): Promise<boolean> {
|
||||
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
|
||||
return semver.gte(version, '7.0.0');
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import Log from '../log';
|
||||
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from '../update/android/UpdatesModule';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from '../update/ios/UpdatesModule';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
ctx as BuildContext<Platform.IOS>
|
||||
),
|
||||
}),
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
}
|
||||
|
||||
async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = ctx as BuildContext<Platform.IOS>;
|
||||
return await maybeResolveIosVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
iosContext.ios.applicationTargetBuildSettings
|
||||
);
|
||||
} else if (ctx.platform === Platform.ANDROID) {
|
||||
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
|
||||
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
|
||||
} else {
|
||||
throw new Error(`Unsupported platform ${ctx.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
|
||||
} else {
|
||||
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.buildProfile.channel) {
|
||||
return { channel: ctx.buildProfile.channel };
|
||||
}
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||
}
|
||||
const channel = await getNativeChannelAsync(ctx);
|
||||
if (channel) {
|
||||
return { channel };
|
||||
}
|
||||
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||
return { releaseChannel };
|
||||
}
|
||||
|
||||
async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
async function getNativeChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getReactNativeVersionAsync(
|
||||
ctx: BuildContext<Platform>
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
|
||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||
} catch (err) {
|
||||
Log.debug('Failed to resolve react-native version:');
|
||||
Log.debug(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
return ctx.buildProfile.enterpriseProvisioning;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from './build';
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
let metroConfigValidated = false;
|
||||
let sdkVersionChecked = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.submitProfile,
|
||||
type: 'submit',
|
||||
});
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submitProfile = nullthrows(
|
||||
submitProfiles.find(
|
||||
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
|
||||
)
|
||||
).profile;
|
||||
const submission = await prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile.profile,
|
||||
submitProfile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
||||
const builds = await waitForBuildEndAsync({
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
accountName,
|
||||
});
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
// the following function also exits with non zero code if any of the submissions failed
|
||||
await waitForSubmissionsToCompleteAsync(submissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
if (!sdkVersionChecked) {
|
||||
await checkExpoSdkIsSupportedAsync(buildCtx);
|
||||
sdkVersionChecked = true;
|
||||
}
|
||||
if (!metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const build = await startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
async function prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
buildCtx,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
submitProfile,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
submitProfile: SubmitProfile;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const platform = toPlatform(build.platform);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
ERRORED = 'errored',
|
||||
FINISHED = 'finished',
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export enum BuildDistributionType {
|
||||
STORE = 'store',
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as ExpoConfig from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import assert from 'assert';
|
||||
|
||||
export async function updateAppJsonConfigAsync(
|
||||
{
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig.ExpoConfig;
|
||||
},
|
||||
modifyConfig: (config: any) => void
|
||||
): Promise<void> {
|
||||
const paths = ExpoConfig.getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic config");
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
|
||||
modifyConfig(rawStaticConfig.expo);
|
||||
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
|
||||
|
||||
modifyConfig(exp);
|
||||
}
|
||||
|
||||
// TODO: remove this once @expo/config exports getStaticConfig
|
||||
export function readAppJson(appJsonPath: string): any {
|
||||
const config = JsonFile.read(appJsonPath, { json5: true });
|
||||
if (config) {
|
||||
return config as any;
|
||||
}
|
||||
throw new Error(`Failed to read app.json`);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../../platform';
|
||||
|
||||
export function logCredentialsSource(
|
||||
credentialsSource: CredentialsSource,
|
||||
platform: Platform
|
||||
): void {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL) {
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
} else if (credentialsSource === CredentialsSource.REMOTE) {
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
}
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { reviewAndCommitChangesAsync } from './repository';
|
||||
|
||||
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive = false,
|
||||
buildProfiles = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
|
||||
buildProfile => buildProfile.profile.developmentClient
|
||||
);
|
||||
|
||||
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
|
||||
|
||||
if (!isDevelopmentClientRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
|
||||
({ platform }) => platform
|
||||
);
|
||||
|
||||
const workflowPerPlatformList = await Promise.all(
|
||||
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
|
||||
);
|
||||
|
||||
Log.newLine();
|
||||
Log.error(
|
||||
`You want to build a development client build for platforms: ${platformsToCheck
|
||||
.map(i => chalk.bold(appPlatformDisplayNames[toAppPlatform(i)]))
|
||||
.join(', ')}`
|
||||
);
|
||||
Log.error(
|
||||
`However, we detected that you don't have ${chalk.bold(
|
||||
'expo-dev-client'
|
||||
)} installed for your project.`
|
||||
);
|
||||
const areAllManaged = workflowPerPlatformList.every(i => i === Workflow.MANAGED);
|
||||
if (areAllManaged) {
|
||||
const install = await confirmAsync({
|
||||
message: 'Do you want EAS CLI to install expo-dev-client for you?',
|
||||
instructions: 'The command will abort unless you agree.',
|
||||
});
|
||||
if (install) {
|
||||
await installExpoDevClientAsync(projectDir, { nonInteractive });
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
|
||||
Log.warn(
|
||||
learnMore('https://docs.expo.dev/clients/installation/', {
|
||||
learnMoreMessage: 'See installation instructions on how to do it.',
|
||||
dim: false,
|
||||
})
|
||||
);
|
||||
Log.warn('If you proceed anyway, you might not get the build you want.');
|
||||
Log.newLine();
|
||||
const shouldContinue = await confirmAsync({
|
||||
message: 'Do you want to proceed anyway?',
|
||||
initial: false,
|
||||
});
|
||||
if (!shouldContinue) {
|
||||
Errors.error('Come back later', { exit: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolean> {
|
||||
try {
|
||||
resolveFrom(projectDir, 'expo-dev-client/package.json');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
Log.debug(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installExpoDevClientAsync(
|
||||
projectDir: string,
|
||||
{ nonInteractive }: { nonInteractive: boolean }
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log(`Running ${chalk.bold('expo install expo-dev-client')}`);
|
||||
Log.newLine();
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
||||
Log.newLine();
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
const actor = getActorName(build);
|
||||
const fields: { label: string; value?: string | null }[] = [
|
||||
{ label: 'ID', value: build.id },
|
||||
{
|
||||
label: 'Platform',
|
||||
value: appPlatformDisplayNames[build.platform],
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
return chalk.blue('new');
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
return chalk.blue('in queue');
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return chalk.blue('in progress');
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
return chalk.gray('canceled');
|
||||
case GraphQLBuildStatus.Finished:
|
||||
return chalk.green('finished');
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return chalk.red('errored');
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Enterprise Provisioning',
|
||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Release Channel',
|
||||
value: build.releaseChannel,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
value: build.channel,
|
||||
},
|
||||
{
|
||||
label: 'SDK Version',
|
||||
value: build.sdkVersion,
|
||||
},
|
||||
{
|
||||
label: 'Runtime Version',
|
||||
value: build.runtimeVersion,
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
value: build.appVersion,
|
||||
},
|
||||
{
|
||||
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
|
||||
value: build.appBuildVersion,
|
||||
},
|
||||
{
|
||||
label: 'Commit',
|
||||
value: build.gitCommitHash,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? url : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
|
||||
{
|
||||
label: 'Finished at',
|
||||
value: [
|
||||
GraphQLBuildStatus.New,
|
||||
GraphQLBuildStatus.InQueue,
|
||||
GraphQLBuildStatus.InProgress,
|
||||
].includes(build.status)
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
{ label: 'Started by', value: actor ?? 'unknown' },
|
||||
];
|
||||
|
||||
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
return formatFields(filteredFields);
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
return build.initiatingActor?.displayName || 'unknown';
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildError,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
DistributionType,
|
||||
EasBuildDeprecationInfo,
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printBuildResult(build: BuildFragment): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === BuildStatus.Errored) {
|
||||
const userError = build.error;
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build failed${userError ? ':' : ''}`
|
||||
);
|
||||
if (userError) {
|
||||
printUserError(userError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (build.status === BuildStatus.Canceled) {
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build was canceled`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
// It's tricky to install the .apk file directly on Android so let's fallback
|
||||
// to the build details page and let people press the button to download there
|
||||
const qrcodeUrl =
|
||||
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
|
||||
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
|
||||
Log.log(`${indentString(code, 2)}\n`)
|
||||
);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${chalk.underline(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationInfo | null): void {
|
||||
if (!deprecationInfo) {
|
||||
return;
|
||||
}
|
||||
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn("Changes won't affect your project config.");
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
|
||||
Log.warn('This command is using API that soon will be deprecated.');
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
Log.warn(deprecationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function printUserError(error: BuildError): void {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import admzip from 'adm-zip';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`It's recommended to ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
)}, so you can revert the changes made by this command if necessary.`
|
||||
);
|
||||
const answer = await confirmAsync({
|
||||
message: `Would you like to proceed?`,
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
)}.`
|
||||
);
|
||||
if (nonInteractive) {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
const answer = await confirmAsync({
|
||||
message: `Commit changes to git?`,
|
||||
});
|
||||
if (answer) {
|
||||
await commitPromptAsync({ commitAllFiles: true });
|
||||
} else {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
export async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
|
||||
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
|
||||
|
||||
// If the compression takes longer then a second, show the spinner.
|
||||
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
|
||||
// A basic project on a Mac can compress in roughly ~40ms.
|
||||
// A fairly complex project without CocoaPods ignored can take up to 30s.
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
spinner.start();
|
||||
},
|
||||
Log.isDebug ? 1 : 1000
|
||||
);
|
||||
// TODO: Possibly warn after more time about unoptimized assets.
|
||||
const compressTimerLabel = 'makeProjectTarballAsync';
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
if (spinner.isSpinning) {
|
||||
spinner.fail();
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(shallowClonePath);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
||||
const { size } = await fs.stat(tarPath);
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(
|
||||
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
export async function makeZipAsync(pathToZip: string): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
|
||||
|
||||
try {
|
||||
process.chdir(pathToZip);
|
||||
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
|
||||
process.chdir('..');
|
||||
|
||||
const zipPath = path.join(getTmpDirectory(), `${uuidv4()}.zip`);
|
||||
|
||||
// If the compression takes longer then a second, show the spinner.
|
||||
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
|
||||
// A basic project on a Mac can compress in roughly ~40ms.
|
||||
// A fairly complex project without CocoaPods ignored can take up to 30s.
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
spinner.start();
|
||||
},
|
||||
Log.isDebug ? 1 : 1000
|
||||
);
|
||||
// TODO: Possibly warn after more time about unoptimized assets.
|
||||
const compressTimerLabel = 'makeProjectZipAsync';
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
const zip = new admzip();
|
||||
zip.addLocalFolder(shallowClonePath);
|
||||
await zipAsync(zip, zipPath);
|
||||
clearTimeout(timer);
|
||||
|
||||
const { size } = await fs.stat(zipPath);
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(
|
||||
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { size, path: zipPath };
|
||||
} catch (err) {
|
||||
if (spinner.isSpinning) {
|
||||
spinner.fail();
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(shallowClonePath);
|
||||
}
|
||||
}
|
||||
|
||||
async function zipAsync(zip: admzip, targetFileName?: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.writeZip(targetFileName, (error: Error | null) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Abort,
|
||||
}
|
||||
|
||||
export async function reviewAndCommitChangesAsync(
|
||||
initialCommitMessage: string,
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
nonInteractive,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode or set EAS_BUILD_AUTOCOMMIT=1 in your environment.'
|
||||
);
|
||||
}
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Abort build process',
|
||||
value: ShouldCommitChanges.Abort,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Abort) {
|
||||
throw new Error(
|
||||
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
||||
);
|
||||
} else if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/projects/${projectName}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
const url =
|
||||
project.__typename !== 'App'
|
||||
? `/builds/${build.id}`
|
||||
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
|
||||
|
||||
return new URL(url, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return new URL(`/artifacts/${artifactId}`, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
if (build.platform === AppPlatform.Ios) {
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
|
||||
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from './appJson';
|
||||
|
||||
export function ensureStaticConfigExists(projectDir: string): void {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
throw new Error('autoIncrement option is not supported when using app.config.js');
|
||||
}
|
||||
}
|
||||
|
||||
export async function bumpAppVersionAsync({
|
||||
appVersion,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
appVersion: string;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
let bumpedAppVersion: string;
|
||||
if (semver.valid(appVersion)) {
|
||||
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
|
||||
bumpedAppVersion
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
|
||||
bumpedAppVersion = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'bumpedAppVersion',
|
||||
message: 'What is the next version?',
|
||||
})
|
||||
).bumpedAppVersion;
|
||||
}
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.version = bumpedAppVersion;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
}
|
||||
const googleServicesFilePath = ctx.exp[ctx.platform]?.googleServicesFile;
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
);
|
||||
Log.warn(
|
||||
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command } from '@oclif/core';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
protected requiresAuthentication = true;
|
||||
protected mustBeRunInsideProject = true;
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
await initAnalyticsAsync();
|
||||
if (this.mustBeRunInsideProject) {
|
||||
await this.applyCliConfigAsync();
|
||||
}
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
const { flags } = await this.parse();
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
await ensureLoggedInAsync({ nonInteractive });
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
return this.runAsync();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async finally(err: Error): Promise<any> {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
private async applyCliConfigAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const config = await easJsonReader.getCliConfigAsync();
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
jest.mock('../../analytics/rudderstackClient', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let originalProcessArgv: string[];
|
||||
|
||||
beforeAll(() => {
|
||||
originalProcessArgv = process.argv;
|
||||
process.argv = [];
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.mocked(getUserAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(async () => mockJester);
|
||||
jest
|
||||
.mocked(ensureLoggedInAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(async () => mockJester);
|
||||
jest.mocked(initAsync).mockReset();
|
||||
jest.mocked(flushAsync).mockReset();
|
||||
jest.mocked(logEvent).mockReset();
|
||||
});
|
||||
|
||||
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
|
||||
class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = authValue;
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
return TestEasCommand;
|
||||
};
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
describe('without exceptions', () => {
|
||||
// The first test in this suite should have an increased timeout
|
||||
// because of the implementation of Command from @oclif/command.
|
||||
// It seems that loading config takes significant amount of time
|
||||
// and I'm not sure how to mock it.
|
||||
//
|
||||
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
|
||||
// and look for "Config.load"
|
||||
it('ensures the user is logged in', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveBeenCalled();
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
const TestEasCommand = createTestEasCommand(false);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
|
||||
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { showLoginPromptAsync } from '../../user/actions';
|
||||
|
||||
export default class AccountLogin extends EasCommand {
|
||||
static description = 'log in with your Expo account';
|
||||
static aliases = ['login'];
|
||||
|
||||
protected mustBeRunInsideProject = false;
|
||||
protected requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await showLoginPromptAsync();
|
||||
Log.log('Logged in');
|
||||
}
|
||||
}
|
||||
Loaded 100 of 567 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user