mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1152 Commits
v12.36.1
...
example-mo
Author | SHA1 | Date | |
---|---|---|---|
357866e345 | |||
629ac9e5e9 | |||
2f03b24bcf | |||
233ee990f9 | |||
facc66e9f9 | |||
6efd24489f | |||
0339160a0b | |||
0591f5edbd | |||
c30dd323f1 | |||
1640bd6457 | |||
da2ffde483 | |||
5c9e3ad8f6 | |||
7515d4b710 | |||
fae5af6b75 | |||
45aa1adacb | |||
b34ea14413 | |||
90eae06017 | |||
41da8f6f6f | |||
8d706a7d81 | |||
d67952024c | |||
8895fc485c | |||
c2dbcaaaf4 | |||
52cb951e49 | |||
2a357a438f | |||
a9a202281d | |||
b74979fb9e | |||
4760866c77 | |||
2b044348e0 | |||
c9fa10b9c6 | |||
63674c8201 | |||
a08ac447a3 | |||
12a338fb21 | |||
1d70e6b4b4 | |||
d3458379e6 | |||
544f615ca0 | |||
245c51d974 | |||
03f0f11f8b | |||
2c0c1f8fd1 | |||
d4d7fce2c1 | |||
0f23318367 | |||
003d537433 | |||
46d3497663 | |||
a39a772c9e | |||
efa0d67f0a | |||
232b9678bc | |||
a8ce14b0e8 | |||
838a36758a | |||
4e101e2fd9 | |||
9f9fd97795 | |||
1b36dc84fc | |||
5d6ee707ff | |||
3c64e13fb3 | |||
7e41fda8d4 | |||
5df316e9cb | |||
79fcd95491 | |||
33199acbe8 | |||
4633c2456d | |||
f8bc081228 | |||
1702f8ba59 | |||
60b0c7e346 | |||
e95ef8b3b4 | |||
1bc0f7447f | |||
f65215e144 | |||
97abc5cf1c | |||
e64a09d2f4 | |||
b1073ca549 | |||
e659e3577a | |||
f7233c5d42 | |||
4ae2ff1740 | |||
19a60bb0ab | |||
d1a6f7560c | |||
4619ce7daa | |||
7624240d5e | |||
7273656d07 | |||
00bd4d5415 | |||
c2d3c9fc71 | |||
1749937373 | |||
bcb7fb8902 | |||
81e9601d6b | |||
6c89ba4b22 | |||
57d3d6d537 | |||
6330574c01 | |||
b6d1afac2d | |||
f2d0da0837 | |||
068cd887c8 | |||
93e597a596 | |||
5b1d6a3190 | |||
dba102f347 | |||
c30a1dc1ed | |||
78368c8a51 | |||
d7250ccc4e | |||
2d47eb53cd | |||
b5fc97bdf9 | |||
3472df2c04 | |||
6b5657625a | |||
dad6b23202 | |||
6b59c06978 | |||
b518067058 | |||
bd4bdb805f | |||
32e59eccc5 | |||
f05e49915d | |||
92146429c4 | |||
40f5214317 | |||
14e1255b5f | |||
15e91e95b4 | |||
1814fe7581 | |||
7325e8d9d5 | |||
5358f92590 | |||
fe6a7cfdba | |||
a29bd8d0ef | |||
049e1da53e | |||
2c0b4072ae | |||
15c0c32a01 | |||
8f2c7f9dbf | |||
90982256c7 | |||
73220206a2 | |||
8b453aae89 | |||
d85d5933fb | |||
2cd455ff81 | |||
066cbaf35f | |||
17fa888fea | |||
f50287873a | |||
edff14fa72 | |||
9de753d9d3 | |||
75d2d7d375 | |||
d9b193acc1 | |||
2e42999642 | |||
5a3f0ea453 | |||
e1cd30060c | |||
7959e23cd3 | |||
9c4d788d6d | |||
181f5a6a2f | |||
163dcf596e | |||
1724187466 | |||
b27dcdd582 | |||
c28039a3f2 | |||
233bc705de | |||
71518678e1 | |||
88a705c935 | |||
55d06aced2 | |||
aa9a148c46 | |||
10ca5b4f59 | |||
47e11d5f9b | |||
6fb65bcf22 | |||
954de13b10 | |||
f81a27e931 | |||
e8815d0275 | |||
766e6d4e5e | |||
7b46f65a01 | |||
db8df0ac35 | |||
7c7f46fe2b | |||
b29aae1821 | |||
0b10701015 | |||
1dbe08d7e0 | |||
d01461ff3e | |||
2a970478bd | |||
ffd44d3fec | |||
df51f87fbc | |||
6178f34f88 | |||
c5ecf692bb | |||
87f5f18721 | |||
e33810b448 | |||
3caf54aa16 | |||
9d3ee9eb49 | |||
3dac94db70 | |||
04b4444fc2 | |||
98514cef09 | |||
4811031172 | |||
be682c7426 | |||
c6827ee51d | |||
2cba3bbc22 | |||
933eacf275 | |||
e7869f4c6d | |||
1a246a9ba5 | |||
e26895085d | |||
71345a8cc1 | |||
619f605eb2 | |||
bb4713ab9a | |||
168bddf7db | |||
24076e4f8d | |||
634ad156ce | |||
6ebeb97917 | |||
cb444998cd | |||
742c015f21 | |||
556e50c87c | |||
3294f78b00 | |||
7f11805a7f | |||
42dd732f68 | |||
aed50480c3 | |||
6515d6ae10 | |||
7903c82821 | |||
eee8a0ecca | |||
38a2817587 | |||
2bd0641d5f | |||
122a763f82 | |||
756f6b328b | |||
eb9db6f7b4 | |||
6f9e5a697c | |||
f9f41eef4b | |||
5371fea588 | |||
bacb55a1ea | |||
ecfd4a260e | |||
1525822239 | |||
1614d9b2c8 | |||
2e061845ae | |||
9e4dd3fce2 | |||
b2590136fc | |||
bf5e61a61c | |||
f550d0c596 | |||
54302669b8 | |||
a4a4e33d7b | |||
8d6a621bfb | |||
4b2602676b | |||
b0810c0f85 | |||
97a6013537 | |||
1ba8db1459 | |||
cdada0aec8 | |||
1166533482 | |||
01538728cd | |||
3a7f6d78b0 | |||
dce48c90e9 | |||
fe70d164c1 | |||
09e2550b32 | |||
07854c3d42 | |||
858a455501 | |||
4e5eb4bcee | |||
696bad3ed6 | |||
9a9d0f02ef | |||
f46d00640b | |||
e369bd3599 | |||
75b29112a7 | |||
b7b01ecd53 | |||
801a25995c | |||
8296dea78c | |||
1da5a75c14 | |||
166de57179 | |||
85dece9e95 | |||
bfbc71215c | |||
d243c14d74 | |||
804eb27551 | |||
4266dc6951 | |||
0ba3522584 | |||
19b0e9489d | |||
d9fed9c34c | |||
81ee9f397f | |||
b9722c6796 | |||
29ade0f696 | |||
d5ae612513 | |||
65ba63d1a8 | |||
f5ffa7d84f | |||
dac3ace61d | |||
72459a04d1 | |||
1e83fcf1e3 | |||
b8769bb9e9 | |||
9f52ee8b21 | |||
90b65cd06b | |||
72a924f00e | |||
e4624eda10 | |||
4173cd82e6 | |||
b393f27e1b | |||
1a4a0e2439 | |||
4cd8f4c16e | |||
2de9d526e5 | |||
d9427c3c59 | |||
fc0cfac475 | |||
99094dbfda | |||
0711eefb7c | |||
dc40b0d969 | |||
4b5def0a8a | |||
f44fa38113 | |||
167dfeb269 | |||
a816548bb5 | |||
94001efc81 | |||
8bfafe8ecc | |||
d78045b6ab | |||
11eabc4b96 | |||
bfaa91c752 | |||
1b615e4690 | |||
7954e13154 | |||
45d8872a82 | |||
56cff46408 | |||
47a1a9c6af | |||
a434a5e657 | |||
221c213791 | |||
d2150c5cb7 | |||
70e113152d | |||
4a64102d67 | |||
9bf267166e | |||
f12249bc81 | |||
80d6c71b02 | |||
9ef4117fb8 | |||
25f3bf1fbe | |||
fe65351666 | |||
7d13946c3e | |||
1ffa8d38f1 | |||
6914d39370 | |||
04db9c7a91 | |||
c785d01a1c | |||
87ba364f89 | |||
b7e5915c7a | |||
0cef6b8f87 | |||
71f1dbd80a | |||
36edcf0cb8 | |||
8204dcad93 | |||
03bcb6cff7 | |||
66b6eed57c | |||
99b0f2c022 | |||
947a9bade7 | |||
15ea601d68 | |||
ce888966dd | |||
3fc6eb7a47 | |||
73aebf10bf | |||
292c372eb4 | |||
dc3261d9c7 | |||
301d0ab3a0 | |||
efe0a018d7 | |||
fa2a232e5f | |||
da1f022df9 | |||
8e6a6b81d9 | |||
77906c4152 | |||
26bc68753b | |||
af6b263f7a | |||
a27e216e44 | |||
50e1efa448 | |||
519ac0383a | |||
3d0ef9bc4f | |||
49e23464f9 | |||
a1c9b4b80e | |||
2b1be3e5d9 | |||
e46378ec51 | |||
27ee9c85e7 | |||
21b6ec46e3 | |||
817ce5dc96 | |||
d9af28bca7 | |||
8646be7979 | |||
14ba287e0d | |||
1671e46d99 | |||
507333c463 | |||
8b320d3e9e | |||
e1be268749 | |||
1a0019e6d0 | |||
e79cdb671f | |||
f38e643cf0 | |||
b8e190cd1d | |||
9cca654bd5 | |||
35177e2d2f | |||
1a24b193e7 | |||
272915192b | |||
96774f4c52 | |||
a034f585ba | |||
365d95c36b | |||
c6313c08ae | |||
f5764c4659 | |||
aff094575b | |||
4aaaf64f8d | |||
7b88ce273f | |||
b011af89ad | |||
1bf8c1bfe7 | |||
2b39d5d111 | |||
98663af7f6 | |||
5628824bee | |||
d12d7996bc | |||
0dcf4cbff6 | |||
884e37d242 | |||
f4a24e26c3 | |||
122eccf3dc | |||
bd598788dc | |||
406482b4da | |||
a381c97ca9 | |||
8ce78ba33c | |||
f53f148c89 | |||
0086feb645 | |||
4ee55b049f | |||
90c6f121cc | |||
d3c27ae859 | |||
8f39c1de6c | |||
4df1831187 | |||
2bce761ace | |||
d78b76aceb | |||
f07f6b84d4 | |||
d297a10570 | |||
9d0b82122a | |||
338477463a | |||
d1275760fa | |||
0f4054fa4d | |||
7545fc5d6e | |||
a1f25809cb | |||
e0a3c4bd95 | |||
d843e75512 | |||
72c57608d5 | |||
d9de7636db | |||
10b5af6967 | |||
51c050c725 | |||
eb52c47de5 | |||
4b1378dfbc | |||
1a77d86347 | |||
bd5188f4b9 | |||
034f459bfa | |||
bc405d997e | |||
af27cf2cbe | |||
83b9bf67c2 | |||
abd73b805b | |||
37bfd4db98 | |||
be74143d5f | |||
9975e5d9ac | |||
1341413966 | |||
1a5b914a6f | |||
c5e8f0d6ea | |||
3a143fe413 | |||
3445e4a08e | |||
166130c3df | |||
c3a8a905f7 | |||
2b878e87d8 | |||
063e9d40f0 | |||
2b58143164 | |||
861d4f33b7 | |||
81f4aae7d2 | |||
46ab335407 | |||
07cb0cbfcd | |||
a2392dc580 | |||
8b3235ab2b | |||
15dac6f194 | |||
3c93db8449 | |||
9d8df0b781 | |||
bcadbdbed8 | |||
05a96fa60e | |||
2e37536e7a | |||
025c4ef7f2 | |||
ecbc660bf5 | |||
ba1f17d537 | |||
3ab8f7500e | |||
0a25bec010 | |||
01e765e670 | |||
61844f2386 | |||
46aa08c953 | |||
b6c7fb82c3 | |||
fcda09009a | |||
1a6fe1f3de | |||
98e91c0607 | |||
bed2387d83 | |||
50e852acee | |||
da30623e4e | |||
7a46b367a7 | |||
d9651c7393 | |||
e371b1e759 | |||
77cf4af166 | |||
9d197317ca | |||
9a8b0b4a0d | |||
0c62b9ef08 | |||
83a5e7392a | |||
f0c8c37022 | |||
ba26d3204d | |||
d53542975e | |||
632296a271 | |||
3e089fcdb2 | |||
d61c300750 | |||
a0a97c5f40 | |||
165f3b83ca | |||
5bf95300ee | |||
adb460b270 | |||
ca80bd52fe | |||
281f8abb9a | |||
2cf2918d73 | |||
7dfb7474f5 | |||
6ee0b48c9a | |||
bd01fbf90c | |||
cd19845b6b | |||
5545883c3f | |||
75a380b0ba | |||
35fe7c6a58 | |||
69249b3139 | |||
bf897fd56d | |||
150c6e75f5 | |||
e8bc43dc64 | |||
1213689de2 | |||
c1017e8e27 | |||
7ad9e685f6 | |||
c778aaffaf | |||
b98047cacf | |||
03ace6e4b2 | |||
9b4701bcb7 | |||
174312977a | |||
963d9af817 | |||
af5ec51232 | |||
1cd9fbf6a0 | |||
72639e9e59 | |||
447dcc1480 | |||
564716faa7 | |||
3e5b4457c2 | |||
793e70d909 | |||
5761a306be | |||
adff0f2a0a | |||
4ec45a0c43 | |||
ecf4b046b5 | |||
b0cae93ac9 | |||
53b66678d4 | |||
0b9b65ef88 | |||
8a84d9d792 | |||
c535b8e1ea | |||
234fb6cd39 | |||
8714830b48 | |||
0e07b36691 | |||
ba80d3c38c | |||
e65dc82cfe | |||
bc727521c6 | |||
a8c0c884d3 | |||
b11c7157d3 | |||
578de7bcd4 | |||
cfc6b3ce9e | |||
1c7a354fe7 | |||
40a0941ca3 | |||
0ab4760272 | |||
42b2269e81 | |||
c818d846b3 | |||
3328f40416 | |||
58d10c1908 | |||
2fd0ca6a02 | |||
173028fd0d | |||
62d5bf4436 | |||
63a0d19770 | |||
8244636bf2 | |||
6a01fb361c | |||
ca637b3fb6 | |||
006293bd01 | |||
338b5d79d3 | |||
60dd0daae5 | |||
662b8283a6 | |||
cfc866cf41 | |||
e566badfff | |||
69834c417e | |||
8aa9c62afd | |||
4f29e37fe7 | |||
99e8a36bb5 | |||
669cbe227f | |||
e9156d77f1 | |||
767216c842 | |||
d3018f9061 | |||
37c6ad855b | |||
ca97678358 | |||
3bb0036ba8 | |||
ac9e2a9e7e | |||
52e95e6d0a | |||
c5d2aa7eec | |||
683220e303 | |||
44f09b32fa | |||
d1a0660a3d | |||
ee1987f188 | |||
39e9997d9e | |||
97b8c75043 | |||
7cb8349f29 | |||
6063f4c776 | |||
4899d545f1 | |||
115bf6433d | |||
e5ce1ade89 | |||
9c4174ea8a | |||
cf16957195 | |||
4de369ff95 | |||
ac3ebff8ee | |||
76b01d92d3 | |||
19144163ee | |||
535ffccbad | |||
6f5ada9692 | |||
1c7d9255ae | |||
807e6ea2ad | |||
c76f019fd0 | |||
3c2c925eed | |||
14b54be15e | |||
7fb82f7447 | |||
4a5d44a0f1 | |||
1cba0284df | |||
6e4fe229bf | |||
7033075900 | |||
ded268ff3c | |||
a366f0b7eb | |||
507c8a1bfd | |||
1fb46bfa5d | |||
2e115968d5 | |||
83020797b0 | |||
0c4647e980 | |||
a20d2a04a8 | |||
57b0dccc7d | |||
d1e3bdf29a | |||
bdf7fedd7a | |||
c163662f4a | |||
a2823fd3ec | |||
d717352b84 | |||
e46902e683 | |||
e96ef6697e | |||
6f54197b7b | |||
34b4ac2d9f | |||
f99244603a | |||
523c0af0fb | |||
2206b475c6 | |||
a117dc0382 | |||
cf3e8ff909 | |||
36d1af1e33 | |||
18f83092fe | |||
ee3c796787 | |||
934c3ddf38 | |||
66e6daf78c | |||
97eb107de4 | |||
def205f1fb | |||
5c8f78678b | |||
769f1ca5b4 | |||
cb26a736fc | |||
d28847d5aa | |||
c0902bb119 | |||
26aae0afab | |||
5f3cf75c1a | |||
8a7fbdb55d | |||
b260f80bcc | |||
9ec37975f3 | |||
73c487c2f5 | |||
3cb35ea318 | |||
efe6fd22ce | |||
6ee8d8a899 | |||
c735f13636 | |||
edb0fdc3c1 | |||
14a07ac7f7 | |||
264cd94be5 | |||
2664f4e7fb | |||
3ce2653881 | |||
719860366f | |||
21ded85c7a | |||
c91f67d27e | |||
18eedfec7f | |||
1fe0480a8a | |||
c7f56d92dd | |||
a92f58134f | |||
cc6a8ef76e | |||
88f4a3d88e | |||
f6d668684a | |||
be7c0dc897 | |||
566b7f97e0 | |||
f55dd81a19 | |||
dba5349390 | |||
6a8dfcc664 | |||
59e35d866f | |||
9235c928f1 | |||
3d88f0144a | |||
a6b461ba91 | |||
b96da951db | |||
8235cead07 | |||
30b9d9141d | |||
03b41d9989 | |||
aab3af2153 | |||
600457de61 | |||
17db857e10 | |||
eb45ae2a30 | |||
2eaf70bff3 | |||
226f45f732 | |||
c4990f3a26 | |||
0195a3b18c | |||
3d90aeb122 | |||
0571039bfe | |||
ee668a4c5c | |||
ead4dbfab1 | |||
0b498d09df | |||
2b2c40c22d | |||
ba3a3865b5 | |||
f8402bc40c | |||
c667ffa8eb | |||
6d6065ddf5 | |||
44f55f8e7b | |||
d2c77760b3 | |||
7496710c85 | |||
be6a468507 | |||
88835e63bd | |||
3572cb3cd6 | |||
7fbd1de063 | |||
a4ab07cd08 | |||
9185eaa2b7 | |||
ff3abe1fba | |||
1ac3b70b81 | |||
e946178953 | |||
6589589bee | |||
6ae598b55e | |||
915f7e3763 | |||
cd17d79067 | |||
7e4f4392e9 | |||
3c0e998616 | |||
bd1bf8153d | |||
f2528dcd18 | |||
ec26433925 | |||
43cddd2e5d | |||
eeb2be2912 | |||
3bf8befb1d | |||
948095ce4d | |||
d2330f9ed1 | |||
cc19b00998 | |||
ed5ac75a10 | |||
465b8a1b5e | |||
eccadbdcb9 | |||
31eb734af1 | |||
fa7b59d64f | |||
1e42bfa0d5 | |||
5464e550e7 | |||
c0f27a663d | |||
d1c61c62ab | |||
a9691bff57 | |||
f5d09a43cd | |||
d11e547e11 | |||
bd462aee02 | |||
f633c0468b | |||
e4f61a1242 | |||
96142a002e | |||
6b9a5cd89c | |||
ba2d3d60ec | |||
d1e66bc1a5 | |||
58799915a9 | |||
5f2d55f569 | |||
8d6e51391c | |||
8454b02988 | |||
879d98ef98 | |||
c4e317a290 | |||
7ca4d2d720 | |||
e1e88ec56d | |||
33f7fa3829 | |||
3d516e7c5f | |||
a8507508b7 | |||
008972b3d3 | |||
92b86330a0 | |||
2563c07c6a | |||
1d4b949cf3 | |||
d17e02a930 | |||
a355cbaa79 | |||
bd021c0a2d | |||
a80f676804 | |||
f723c58089 | |||
e27a4e2e31 | |||
b91b72c408 | |||
5cf84d3f1d | |||
7d58b8c120 | |||
851301a336 | |||
ec6fd050f6 | |||
6f81053882 | |||
dbd8a9a08c | |||
256f1abf1b | |||
acd352cb3c | |||
31f927c27c | |||
3d0f16168a | |||
b2d932afab | |||
398175f0b3 | |||
2fb9c6c773 | |||
66608b32e9 | |||
c403683edf | |||
1e6ab46ca3 | |||
02d3220f2d | |||
c86cdc8f84 | |||
84f02dc063 | |||
9145f2fb28 | |||
1164388d78 | |||
06f6094401 | |||
67e11467f7 | |||
c8dfd0ca65 | |||
8b110a835a | |||
7564d95f82 | |||
f12f2b79ef | |||
176d731f9e | |||
1ed39d1d37 | |||
580ca0d584 | |||
73572df7cf | |||
23b42b1a2b | |||
632322e3c2 | |||
4faa5d7f57 | |||
9b967592a9 | |||
e01483cd2b | |||
6d89ff4bbf | |||
126e731117 | |||
32d26ad074 | |||
2bcfec9d0f | |||
c04e63ab7d | |||
79be06820c | |||
ffb94c380f | |||
385b5e9ec6 | |||
8d3a4343cb | |||
6eeb16245b | |||
3961060f90 | |||
a6dfc9126a | |||
e7ddd07b7b | |||
fea351d960 | |||
40e0b2dbed | |||
3def4d0e4a | |||
aa286cc0e7 | |||
8abeb6aed7 | |||
f285880135 | |||
2b5c387313 | |||
8babf4c908 | |||
bfc995e948 | |||
c6a0bc0fba | |||
ae69accf0f | |||
cfcace4c99 | |||
6e07db0813 | |||
5c40c8d51f | |||
d827005154 | |||
76081343cc | |||
f3fb9b6bdf | |||
c125e0b38d | |||
73b2f6b4b1 | |||
fdc0d08e96 | |||
e431a59af7 | |||
41a2dbe60c | |||
6ba67eefdb | |||
3b885ad906 | |||
5574dc0318 | |||
fcea91bfb6 | |||
7316c4e075 | |||
389b7a1463 | |||
09d004423c | |||
97978ff812 | |||
498e21f0ab | |||
257dd514ed | |||
85cbdd4947 | |||
73625611da | |||
d2a5a9ba86 | |||
1cd78215e0 | |||
6d744d0b07 | |||
9d312bcd12 | |||
e22aa847e3 | |||
0d1ca67d5b | |||
c4a5a25f03 | |||
b183d88400 | |||
2b6a2142eb | |||
58b29bf4bb | |||
fc0903a414 | |||
cea23f5d5e | |||
5a9b5e3b08 | |||
52138d41eb | |||
5acdc63068 | |||
b546e4dd97 | |||
e4870916e2 | |||
3ca93448cd | |||
f66395e2d5 | |||
952d782e90 | |||
d53c9b3c50 | |||
2f706c0200 | |||
d64b6deb81 | |||
55fc9b2ade | |||
6c29d0ae27 | |||
f46452f6de | |||
c166ec7597 | |||
7325c79888 | |||
2a29b386eb | |||
23b07f8a41 | |||
6d641b4841 | |||
7b498149b1 | |||
ae5ea0f4e8 | |||
f635f648da | |||
3d4e2cf823 | |||
ef3b630887 | |||
19040ccb6c | |||
8e712ac910 | |||
c401ed35ac | |||
94be97313b | |||
48053ecefc | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 | |||
485a9e944f | |||
1d7a50f007 | |||
64a44e7a5f | |||
c3406603db | |||
f1fa187a58 | |||
6cb2893750 | |||
216172ed4f | |||
3717d8cc0f | |||
8338e2e933 | |||
918c2e912d | |||
be0622ec80 | |||
07eef7bb49 | |||
0892caa155 | |||
fa4e8e7b55 | |||
e624726e44 | |||
f914fa2d8a | |||
c8f5542c8a | |||
a2cad7bf53 | |||
3a871a0003 | |||
e552e36f7b | |||
c325f1158e | |||
f79ccc0c95 | |||
1ec8d9a4ca | |||
427b0d9b41 | |||
cfd790a193 | |||
36f4c1312b | |||
fe7cbf4f74 | |||
4e8b8fe582 | |||
2986e6cea3 | |||
bb6b4b255a | |||
350c4abb96 | |||
fec96b41ee | |||
1dba5cc7c1 | |||
43c6fe672f | |||
486cae1aaa | |||
4d588e51a7 | |||
0035545ce1 | |||
d559b9a5a1 | |||
e2ffc5f068 | |||
75b2fa0e9e | |||
c619ecd41b | |||
7ed01a925b | |||
460022a7cf | |||
d15b54cf40 | |||
c938df2445 | |||
bf1df05606 | |||
e04242db64 | |||
9265588745 | |||
cd8070b1a6 | |||
b17dad8c60 | |||
a254e46118 | |||
6e7a0defb7 | |||
492dbae7f1 | |||
ccc2c20b6d | |||
501882fd26 | |||
e2ff561728 | |||
5544f4a5dd | |||
a3e90182bc | |||
3ac85dcc5f | |||
1ac573c659 | |||
2c922ee6d2 | |||
d9821939d9 | |||
732fc2d539 | |||
535a443d7c | |||
579d68a8f0 | |||
ffac8cb9e5 | |||
0f2780744f | |||
b4495839ca | |||
f45ac42dd3 | |||
112a7b8194 | |||
fa26004648 | |||
ba1ea54d69 | |||
9fb62d92b7 | |||
8780a24fb5 | |||
3d3e91d49d | |||
f6e6d9ce8b | |||
0f9d78ab50 | |||
06f7683837 | |||
83a23d9f30 | |||
ffa181a2c3 | |||
d50d18d492 | |||
0b0fb94834 | |||
c1244c0c98 | |||
213e54feb1 | |||
cc8a8513e9 | |||
42c3236313 | |||
91fd515266 | |||
57cd096612 | |||
854501cf8d | |||
d44afa8c39 | |||
b7500fc2c2 | |||
dc6c8d7472 | |||
5c5be8f7b7 | |||
5bdd6c6034 | |||
a5bade99fc | |||
9c3eb76856 | |||
973f1a9c40 | |||
16ea0c9d6d | |||
73bfe545e8 | |||
f53e658ca2 | |||
b66706e8ee | |||
11e50466d5 | |||
431c4b6e4a | |||
d12490f816 | |||
67b7b8b5d0 | |||
16d1f0f06f | |||
9676ea94cb | |||
df8ce0bbe0 | |||
6437bb7511 | |||
ac96616e4e | |||
2737c9c53c | |||
3b8a46f523 | |||
3ac1994941 | |||
b3a6c6cb0f | |||
6d4faa7b2c | |||
9036ce9af3 | |||
4911db640f | |||
e7999f52a9 | |||
68b61e7424 | |||
329b84d01e | |||
25b1dff5d8 | |||
fb1768b4ca | |||
cbc1e52256 | |||
37c2880996 | |||
835445be2e | |||
52fe7481fc | |||
88072173d0 | |||
fdc2bff063 | |||
4f6f20f469 | |||
50af0760ce | |||
43906d22c8 | |||
43f1188f1d | |||
2629a01c7f | |||
5fc009a6ae | |||
480f84993b | |||
d1fdbd927e | |||
4bfd345b68 | |||
d4a153d2ee | |||
3cff091e3a | |||
b2ad9f1643 | |||
f7623bef85 | |||
af63794571 | |||
65d5bdff08 | |||
23165806aa | |||
3649bafbb1 | |||
c62445a399 | |||
b233ea3e3e | |||
4fe660b3a5 | |||
1f07cd1b1c | |||
bcea5193a1 | |||
8b99cd7170 | |||
1986c9339c | |||
b90c9b0d7e | |||
e28c3f9814 | |||
d054ced541 | |||
c8e4d2c9a6 | |||
9671372b9e | |||
2a4ff75203 | |||
f3d750a024 | |||
a701cd8d4d | |||
e2c0c2f359 | |||
15fc805f89 | |||
0a995ecc49 | |||
1ba992ada2 | |||
e47fd0c887 | |||
af1de34840 | |||
96fb525378 | |||
3d1f16c0ab | |||
6fb58a25fc | |||
e6b85c9cf8 | |||
43b93e7fd4 | |||
a05dcf08b8 | |||
9636985ee7 | |||
023fc57914 | |||
492bdab2fe | |||
941c365259 | |||
fed58278c9 | |||
d74af38bfe | |||
53926067ca | |||
7181dc5401 | |||
e35e13f9a7 | |||
6e0638f3be | |||
d60ec13d5c | |||
731e50a757 | |||
b363d28664 | |||
7ae83d9ce5 | |||
31281549a6 | |||
e86bcc438c | |||
a1cf602f6f | |||
4cd3ef8b91 | |||
e4eb4586f5 | |||
360c6e42f8 | |||
f76702c4e0 | |||
d3586696b4 | |||
f73e3db4de | |||
1f74889386 | |||
743de66138 | |||
8d56fe9678 | |||
3d9d8bf5c8 | |||
8c3df9ae30 | |||
e71184ed3a | |||
caadce6c2b | |||
f45fac6138 | |||
aeff5997d0 | |||
b5028c65cc | |||
f69276e7c9 | |||
9fff9266d4 | |||
0e7f953f72 |
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@ -0,0 +1,37 @@
|
||||
# Reminders:
|
||||
# * Matching rules are different to `.gitignore`
|
||||
# * A pattern without '**' matches in the project's root directory only
|
||||
# * Leading and trailing '/' are discarded (it is not possible to
|
||||
# distinguish between files and directories)
|
||||
# * More details: https://github.com/balena-io-modules/dockerignore
|
||||
|
||||
# development and testing tools or IDEs
|
||||
**/*.log
|
||||
**/*.pid
|
||||
**/*.seed
|
||||
.idea
|
||||
.lock-wscript
|
||||
.nvmrc
|
||||
.nyc_output
|
||||
.vscode
|
||||
coverage
|
||||
lib-cov
|
||||
logs
|
||||
pids
|
||||
|
||||
# OS cache files
|
||||
**/.DS_Store
|
||||
|
||||
# balena CLI config and build files
|
||||
**/.balenaconf
|
||||
**/.fast-boot.json
|
||||
**/.resinconf
|
||||
balenarc.yml
|
||||
build
|
||||
build-bin
|
||||
dist
|
||||
node_modules
|
||||
oclif.manifest.json
|
||||
package-lock.json
|
||||
resinrc.yml
|
||||
tmp
|
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
/completion/*
|
||||
/bin/*
|
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.dev.json',
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
ignoreDefinitionFiles: 0,
|
||||
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
};
|
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -4,9 +4,13 @@
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
.dockerignore eol=lf
|
||||
Dockerfile eol=lf
|
||||
Dockerfile.* eol=lf
|
||||
* text=auto eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
doc/cli.markdown text eol=lf
|
||||
docs/balena-cli.md text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @balena-io/balena-cli
|
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@ -32,11 +32,11 @@ Please describe what actually happened instead:
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myApp
|
||||
balena push myFleet
|
||||
balena push 192.168.0.12
|
||||
balena deploy myApp
|
||||
balena deploy myApp --build
|
||||
balena build . -a myApp
|
||||
balena deploy myFleet
|
||||
balena deploy myFleet --build
|
||||
balena build . -f myFleet
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
@ -48,7 +48,7 @@ additional information. The `--logs` option reveals additional information for t
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myApp --build --logs
|
||||
balena deploy myFleet --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
143
.github/actions/publish/action.yml
vendored
Normal file
143
.github/actions/publish/action.yml
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
---
|
||||
name: package and draft GitHub release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||
required: true
|
||||
secrets:
|
||||
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||
required: true
|
||||
variables:
|
||||
description: 'JSON stringified object containing all the variables from the calling workflow'
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: 'accounts+apple@balena.io'
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Set up Python 3.11
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
choco install yq
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
brew install coreutils
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:SM_CLIENT_CERT_FILE_B64
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
env:
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
|
||||
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
SM_HOST=${{ fromJSON(inputs.secrets).SM_HOST }}
|
||||
SM_API_KEY=${{ fromJSON(inputs.secrets).SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE='${{ runner.temp }}\Certificate_pkcs12.p12'
|
||||
SM_CLIENT_CERT_PASSWORD=${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH=${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
|
||||
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
|
||||
-H "x-api-key:$SM_API_KEY" \
|
||||
-o smtools-windows-x64.msi
|
||||
msiexec -i smtools-windows-x64.msi -qn
|
||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||
smksp_registrar.exe list
|
||||
smctl.exe keypair ls
|
||||
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||
smksp_cert_sync.exe
|
||||
|
||||
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||
fi
|
||||
|
||||
npm run package
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
|
||||
env:
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
# https://docs.digicert.com/es/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html
|
||||
TIMESTAMP_SERVER: http://timestamp.digicert.com
|
||||
# Apple notarization (automation/build-bin.ts)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||
path: dist
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
65
.github/actions/test/action.yml
vendored
Normal file
65
.github/actions/test/action.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
variables:
|
||||
description: "JSON stringified object containing all the variables from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Set up Python 3.11
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Test release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
|
||||
npm run build
|
||||
npm run test:core
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
4
.github/renovate.json
vendored
Normal file
4
.github/renovate.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["github>balena-io/renovate-config"],
|
||||
"postUpdateOptions": ["npmDedupe"]
|
||||
}
|
45
.github/workflows/flowzone.yml
vendored
Normal file
45
.github/workflows/flowzone.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Flowzone
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflow executions for pull_request and pull_request_target
|
||||
if: |
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event_name == 'pull_request'
|
||||
) || (
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
github.event_name == 'pull_request_target'
|
||||
)
|
||||
secrets: inherit
|
||||
with:
|
||||
custom_test_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-12"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
custom_publish_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-12"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
github_prerelease: false
|
||||
restrict_custom_actions: false
|
69
.gitignore
vendored
69
.gitignore
vendored
@ -1,47 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# Reminders:
|
||||
# * A pattern without '/' matches in subdirectories as well (files and directories)
|
||||
# * A leading '/' anchors matching to the directory where `.gitignore` is defined
|
||||
# * A trailing '/' makes the pattern match against directories only
|
||||
# More details: https://git-scm.com/docs/gitignore
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
# development and testing tools or IDEs
|
||||
*.log
|
||||
*.pid
|
||||
*.seed
|
||||
/.idea/
|
||||
/.lock-wscript
|
||||
/.nvmrc
|
||||
/.nyc_output/
|
||||
/.vscode/
|
||||
/coverage/
|
||||
/lib-cov/
|
||||
/logs
|
||||
/pids
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# Commenting this out is preferred by some people, see
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
|
||||
node_modules
|
||||
|
||||
package-lock.json
|
||||
.resinconf
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
|
||||
# OS cache files
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
# balena CLI config and build files
|
||||
.balenaconf
|
||||
.fast-boot.json
|
||||
.resinconf
|
||||
/balenarc.yml
|
||||
/build/
|
||||
/build-bin/
|
||||
/dist/
|
||||
/node_modules
|
||||
/oclif.manifest.json
|
||||
/package-lock.json
|
||||
/resinrc.yml
|
||||
/tmp/
|
||||
|
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
node automation/check-npm-version.js && ts-node automation/check-doc.ts
|
@ -1,8 +1,10 @@
|
||||
module.exports = {
|
||||
spec: 'tests/commands/app/create.spec.ts',
|
||||
reporter: 'spec',
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
timeout: 12000,
|
||||
// To test only, say, 'push.spec.ts', do it as follows so that
|
||||
// requests are authenticated:
|
||||
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
||||
spec: 'tests/**/*.spec.ts',
|
||||
};
|
||||
|
20
.resinci.yml
20
.resinci.yml
@ -1,20 +0,0 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
publish: false
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "10"
|
||||
matrix:
|
||||
exclude:
|
||||
node_js: "10"
|
||||
script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run ci
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
notifications:
|
||||
email: false
|
||||
deploy:
|
||||
- provider: script
|
||||
script: npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: balena-io/balena-cli
|
File diff suppressed because it is too large
Load Diff
4657
CHANGELOG.md
4657
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -105,33 +105,84 @@ npm run update balena-sdk ^13.0.0 major
|
||||
|
||||
## Editing documentation files (README, INSTALL, Reference website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
The `docs/balena-cli.md` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
|
||||
Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
The content sources for the auto generation of `docs/balena-cli.md` are:
|
||||
|
||||
* [Selected
|
||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||
of the README file.
|
||||
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
|
||||
* `lib/commands/push.ts`
|
||||
* `lib/commands/env/add.ts`
|
||||
* The CLI's command documentation in source code (`src/commands/` folder), for example:
|
||||
* `src/commands/push.ts`
|
||||
* `src/commands/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`doc/cli.markdown` by the `getCapitanoDoc()` function in
|
||||
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
**IMPORTANT**
|
||||
|
||||
The file [`capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts) lists
|
||||
commands to generate documentation from. At the moment, it's manually updated and maintained alphabetically.
|
||||
|
||||
To add a new command to be documented,
|
||||
|
||||
1. Find the resource which it is part of or create a new one.
|
||||
2. List the location of the build file
|
||||
3. Make sure to add your files in alphabetical order
|
||||
4. Resources with plural names needs to have 2 sections if they have commands like: "fleet, fleets" or "device, devices" or "tag, tags"
|
||||
|
||||
Once added, run the command `npm run build` to generate the documentation
|
||||
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Patches folder
|
||||
|
||||
The `patches` folder contains patch files created with the
|
||||
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
|
||||
third-party modules can be made by directly editing Javascript files under the `node_modules`
|
||||
folder and then running `patch-package` to create the patch files. The patch files are then
|
||||
applied immediately after `npm install`, through the `postinstall` script defined in
|
||||
`package.json`.
|
||||
|
||||
The subfolders of the `patches` folder are documented in the
|
||||
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
|
||||
script.
|
||||
|
||||
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
|
||||
not even for a "single character change" because the hash values in the patch files also need
|
||||
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
|
||||
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
|
||||
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
|
||||
run:
|
||||
|
||||
```sh
|
||||
$ npx patch-package --patch-dir patches/all exit-hook
|
||||
```
|
||||
|
||||
That said, these kinds of patches should be avoided in favour of creating pull requests
|
||||
upstream. Patch files create additional maintenance work over time as the patches need to be
|
||||
updated when the dependencies are updated, and they prevent the compounding community benefit
|
||||
that sharing fixes upstream have on open source projects like the balena CLI. The typical
|
||||
scenario where these patches are used is when the upstream maintainers are unresponsive or
|
||||
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
|
||||
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
|
||||
the patches.
|
||||
|
||||
## Windows
|
||||
|
||||
The `npm run build:installer` script (which generates the `.exe` executable installer on Windows)
|
||||
specifically requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the
|
||||
standard Command Prompt or PowerShell can be used (though MSYS2 is still handy, as it provides
|
||||
'git' and a number of common unix utilities). If changes are made to npm scripts in `package.json`,
|
||||
check that they also run on a standard Windows Command Prompt.
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
that produces the `.exe` graphical installer on Windows also requires
|
||||
[NSIS](https://sourceforge.net/projects/nsis/) and [MSYS2](https://www.msys2.org/) to be
|
||||
installed. Be sure to add `C:\Program Files (x86)\NSIS` to the PATH, so that `makensis`
|
||||
is available. MSYS2 is recommended when developing the balena CLI on Windows.
|
||||
|
||||
If changes are made to npm scripts in `package.json`, don't assume that a Unix shell like
|
||||
bash is available. For example, some Windows shells don't have the `cp` and `rm` commands,
|
||||
which is why you'll often find `ncp` and `rimraf` used in `package.json` scripts.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
@ -164,6 +215,24 @@ Optionally, these steps may be automated by installing the
|
||||
npx npm-merge-driver install -g
|
||||
```
|
||||
|
||||
## `fast-boot` and `npm link` - modifying the `node_modules` folder
|
||||
|
||||
During development or debugging, it is sometimes useful to temporarily modify the `node_modules`
|
||||
folder (with or without making the respective changes to the `npm-shrinkwrap.json` file),
|
||||
replacing dependencies with different versions. This can be achieved with the `npm link`
|
||||
command, or by manually editing or copying files to the `node_modules` folder.
|
||||
|
||||
Unexpected behavior may then be observed because of the CLI's use of the
|
||||
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
||||
`fast-boot2` is configured in `src/fast-boot.ts` to automatically invalidate the cache if
|
||||
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
||||
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||
`node_modules` folder. In this situation:
|
||||
|
||||
* Manually delete the module cache file (typically `~/.balena/cli-module-cache.json`), or
|
||||
* Use the `bin/balena-dev` entry point (instead of `bin/balena`) as it does not activate
|
||||
`fast-boot2`.
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
@ -237,3 +306,11 @@ gotchas to bear in mind:
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
## Further debugging notes
|
||||
|
||||
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||
|
||||
replace: `spec: 'tests/**/*.spec.ts',`
|
||||
|
||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||
|
@ -40,7 +40,7 @@ By default, the CLI is installed to the following folders:
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
@ -60,7 +60,7 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> For these, consider the [NPM Installation](#npm-installation) option.
|
||||
@ -76,58 +76,76 @@ as described above.
|
||||
|
||||
If you are a Node.js developer, you may wish to install the balena CLI via [npm](https://www.npmjs.com).
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some additional development tools to be installed first:
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
* [Node.js](https://nodejs.org/) version 10 (min **10.20.0**) or 12 (version 14 is not yet fully supported)
|
||||
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
|
||||
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
|
||||
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
|
||||
distributions like Ubuntu, users often report permission or compilation errors when running
|
||||
"npm install". This [sample
|
||||
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
|
||||
installation steps on an Ubuntu 18.04 base image.
|
||||
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
|
||||
* **Linux** and **Windows Subsystem for Linux (WSL):**
|
||||
`sudo apt-get install -y python git make g++`
|
||||
* **macOS:** install Apple's Command Line Tools by running on a Terminal window:
|
||||
`xcode-select --install`
|
||||
> **The balena CLI currently requires Node.js version ^20.6.0**
|
||||
> **Versions 21 and later are not yet fully supported.**
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
### Install development tools
|
||||
|
||||
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `ssh`, `rsync`
|
||||
and more:
|
||||
* `pacman -S git openssh rsync gcc make`
|
||||
#### **Linux or WSL** (Windows Subsystem for Linux)
|
||||
|
||||
```sh
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script) to install
|
||||
Node.js, instead of using `apt-get`. Installing Node.js through `apt-get` is a common source of
|
||||
problems from permission errors to conflict with other system packages, and therefore not
|
||||
recommended.
|
||||
|
||||
#### **macOS**
|
||||
|
||||
* Download and install Apple's Command Line Tools from https://developer.apple.com/downloads/
|
||||
* Install Node.js through [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script):
|
||||
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
* `pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
|
||||
interactive CLI menus to misbehave. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
|
||||
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-refreshed-wdk-for-windows-10-version-2004)
|
||||
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (which
|
||||
provides Python 2.7 and more), by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
|
||||
`npm install -g --production windows-build-tools`
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package,
|
||||
by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
`npm install --global --production windows-build-tools`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
### Install the balena CLI
|
||||
|
||||
After installing the development tools, install the balena CLI with:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --production --unsafe-perm
|
||||
$ npm install balena-cli --global --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is required when `npm install` is executed as the root user, or on systems where
|
||||
the global install directory is not user-writable. It allows npm install steps to download and save
|
||||
prebuilt native binaries, and also allows the execution of npm scripts like `postinstall` that are
|
||||
used to patch dependencies. It is usually possible to omit `--unsafe-perm` if installing under a
|
||||
regular (non-root) user account, especially if using a user-managed node installation such as
|
||||
[nvm](https://github.com/creationix/nvm).
|
||||
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
|
||||
container) in order to allow npm scripts like `postinstall` to be executed.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy`, `preload` and `os configure` commands may require
|
||||
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
@ -135,9 +153,9 @@ system:
|
||||
* [macOS](./INSTALL-MAC.md#additional-dependencies)
|
||||
* [Linux](./INSTALL-LINUX.md#additional-dependencies)
|
||||
|
||||
The `build` and `deploy` commands are also capable of using Docker or balenaEngine on a remote
|
||||
server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)). Reasons why this
|
||||
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
|
||||
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images). Reasons why this
|
||||
may be desirable include:
|
||||
|
||||
* To avoid having to install Docker on the development machine / laptop.
|
||||
@ -145,6 +163,7 @@ may be desirable include:
|
||||
* To build or run images "natively" on an ARM device, avoiding the need for QEMU emulation.
|
||||
|
||||
To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machine's IP address and
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. For more details,
|
||||
check `balena help build` or the [online
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. The `preload` command
|
||||
has additional requirements because the bind mount feature is used. For more details, see
|
||||
`balena help` for each command or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
|
@ -1,8 +1,10 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are for the recommended installation option. They are suitable for most Linux
|
||||
distributions, except notably for **Linux Alpine** or **Busybox**. For these distros, see [advanced
|
||||
installation options](./INSTALL-ADVANCED.md).
|
||||
These instructions are suitable for most Linux distributions on Intel x86, such as
|
||||
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
|
||||
For the ARM architecture and for Linux distributions not based on glibc, such as
|
||||
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
|
||||
method.
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
@ -11,36 +13,53 @@ Selected operating system: **Linux**
|
||||
with "-standalone.zip", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
|
||||
The extracted contents will include a `balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable. There are several
|
||||
ways of achieving this on Linux: See this [StackOverflow post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix). Close and reopen the terminal window
|
||||
so that the changes to PATH can take effect.
|
||||
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
|
||||
Check this [StackOverflow
|
||||
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
|
||||
for instructions. Close and reopen the terminal window so that the changes to `PATH`
|
||||
can take effect.
|
||||
|
||||
4. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
terminal window:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as described
|
||||
below.
|
||||
|
||||
To update the balena CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## sudo configuration
|
||||
|
||||
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
|
||||
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
|
||||
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
|
||||
the ***pre-existing*** `secure_path` setting, for example:
|
||||
|
||||
```text
|
||||
Defaults secure_path="/home/james/balena-cli:<pre-existing entries go here>"
|
||||
```
|
||||
|
||||
If an `/etc/sudoers` file does not exist, or if it does not contain a pre-existing
|
||||
`secure_path` setting, do not change it.
|
||||
|
||||
If you also have Docker installed, ensure that it can be executed ***without*** `sudo`, so that
|
||||
CLI commands like `balena build` and `balena preload` can also be executed without `sudo`.
|
||||
Check Docker's [post-installation
|
||||
steps](https://docs.docker.com/engine/install/linux-postinstall/) on how to achieve this.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build, deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
|
@ -10,30 +10,36 @@ Selected operating system: **macOS**
|
||||
Look for a file name that ends with "-installer.pkg":
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- [Open the Terminal
|
||||
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
|
||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described below.
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
in the next section.
|
||||
|
||||
To update the balena CLI, repeat the steps above for the new version.
|
||||
To uninstall it, run the following command on a terminal prompt:
|
||||
|
||||
```text
|
||||
sudo /usr/local/src/balena-cli/bin/uninstall
|
||||
```
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -52,17 +58,17 @@ command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
@ -10,19 +10,17 @@ Selected operating system: **Windows**
|
||||
Look for a file name that ends with "-installer.exe":
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- Click on the Windows Start Menu, type PowerShell, and then click
|
||||
on Windows PowerShell.
|
||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy`, `preload` and `os configure` commands may require additional software to be installed, as
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
described below.
|
||||
|
||||
## Additional Dependencies
|
||||
@ -30,11 +28,11 @@ described below.
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -59,24 +57,17 @@ Otherwise, Bonjour for Windows can be downloaded and installed from: https://sup
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
|
||||
### balena os configure
|
||||
|
||||
* The `balena os configure` command is currently not supported on Windows natively, but works with
|
||||
the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL). When
|
||||
using WSL, [install the balena CLI for
|
||||
Linux](https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md).
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
31
README.md
31
README.md
@ -28,18 +28,20 @@ are supported. Alternative shells include:
|
||||
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git openssh rsync`
|
||||
`pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
|
||||
menus to break. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
|
||||
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS)
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
|
||||
actually uses MSYS2). For a screenshot, check this
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** should be selected. See
|
||||
@ -48,14 +50,14 @@ are supported. Alternative shells include:
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. Optionally, `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
## Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
new release to your fleet. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ balena login
|
||||
@ -75,7 +77,6 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
|
||||
file](https://www.npmjs.com/package/balena-settings-client#documentation). It may be:
|
||||
* A string in URL format, e.g. `proxy: 'http://localhost:8000'`
|
||||
* An object in the format:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
protocol: 'http'
|
||||
@ -143,7 +144,7 @@ To learn more, troubleshoot issues, or to contact us for support:
|
||||
|
||||
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud)
|
||||
* Ask us a question in the [balena forums](https://forums.balena.io/c/product-support)
|
||||
|
||||
For CLI bug reports or feature requests, check the
|
||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||
@ -155,14 +156,18 @@ of major, minor and patch version releases.
|
||||
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
|
@ -31,6 +31,11 @@ command again.
|
||||
|
||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||
|
||||
## I get `connect ETIMEDOUT` with `balena tunnel`
|
||||
|
||||
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
||||
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
||||
|
||||
## I get EINVAL errors on Cygwin
|
||||
|
||||
The errors may look something like this:
|
||||
|
43
appveyor.yml
43
appveyor.yml
@ -1,43 +0,0 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%AppData%\npm-cache'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
# what combinations to test
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm config set python 'C:\Python27\python.exe'
|
||||
- npm --version
|
||||
# - npm install
|
||||
|
||||
build: off
|
||||
test: off
|
||||
deploy: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm test
|
||||
|
||||
deploy_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
# - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
# - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
@ -15,31 +15,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { JsonVersions } from '../lib/commands/version';
|
||||
import type { JsonVersions } from '../src/commands/version/index';
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import { run as oclifRun } from '@oclif/core';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
import { exec, execFile } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import type { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as _ from 'lodash';
|
||||
import * as klaw from 'klaw';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as util from 'util';
|
||||
import { promisify } from 'util';
|
||||
import { notarize } from '@electron/notarize';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
@ -58,9 +61,13 @@ const standaloneZips: PathByPlatform = {
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||
};
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
|
||||
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
||||
const sha = stdout.trim();
|
||||
return {
|
||||
darwin: dPath('macos', `balena-${version}-${sha}-${arch}.pkg`),
|
||||
win32: dPath('win32', `balena-${version}-${sha}-${arch}.exe`),
|
||||
};
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
@ -85,13 +92,14 @@ async function diffPkgOutput(pkgOut: string) {
|
||||
'tests',
|
||||
'test-data',
|
||||
'pkg',
|
||||
`expected-warnings-${process.platform}.txt`,
|
||||
`expected-warnings-${process.platform}-${arch}.txt`,
|
||||
);
|
||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||
const ignoreStartsWith = [
|
||||
'> pkg@',
|
||||
'> Fetching base Node.js binaries',
|
||||
' fetched-',
|
||||
'prebuild-install WARN install No prebuilt binaries found',
|
||||
];
|
||||
const modulesRE =
|
||||
process.platform === 'win32'
|
||||
@ -152,7 +160,7 @@ ${sep}
|
||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
||||
*/
|
||||
async function execPkg(...args: any[]) {
|
||||
const { exec: pkgExec } = await import('pkg');
|
||||
const { exec: pkgExec } = await import('@yao-pkg/pkg');
|
||||
const outTap = new StdOutTap(true);
|
||||
try {
|
||||
outTap.tap();
|
||||
@ -177,9 +185,18 @@ async function execPkg(...args: any[]) {
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
// https://github.com/vercel/pkg#targets
|
||||
let targets = `linux-${arch}`;
|
||||
if (process.platform === 'darwin') {
|
||||
targets = `macos-${arch}`;
|
||||
}
|
||||
// TBC: not yet possible to build for Windows arm64 on x64 nodes
|
||||
if (process.platform === 'win32') {
|
||||
targets = `win-x64`;
|
||||
}
|
||||
const args = [
|
||||
'--target',
|
||||
'host',
|
||||
'--targets',
|
||||
targets,
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
@ -194,7 +211,6 @@ async function buildPkg() {
|
||||
const paths: Array<[string, string[], string[]]> = [
|
||||
// [platform, [source path], [destination path]]
|
||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||
];
|
||||
await Promise.all(
|
||||
@ -243,7 +259,17 @@ async function testPkg() {
|
||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
||||
// reported Node.js major version matches semver.major(process.version)
|
||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
||||
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||
'version',
|
||||
'-j',
|
||||
]);
|
||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: stderr.split(/\r?\n/),
|
||||
out: stdout.split(/\r?\n/),
|
||||
});
|
||||
stdout = filtered.out.join('\n');
|
||||
stderr = filtered.err.join('\n');
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
@ -260,6 +286,10 @@ async function testPkg() {
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
if (filtered.err.length > 0) {
|
||||
const err = filtered.err.join('\n');
|
||||
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
@ -292,10 +322,100 @@ async function zipPkg() {
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
archive.finalize().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export async function signFilesForNotarization() {
|
||||
console.log('Signing files for notarization');
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
console.log('Deleting unneeded zip files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node.bak')) {
|
||||
console.log('Removing pkg .node.bak file', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
if (
|
||||
path.basename(item.path).endsWith('.zip') &&
|
||||
path.dirname(item.path).includes('test')
|
||||
) {
|
||||
console.log('Removing zip', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
// Sign all .node files first
|
||||
console.log('Signing .node files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', async (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node')) {
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log('Signing other binaries...');
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
@ -310,6 +430,7 @@ export async function buildStandaloneZip() {
|
||||
}
|
||||
|
||||
async function renameInstallerFiles() {
|
||||
const oclifInstallers = await getOclifInstallersOriginalNames();
|
||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||
await fs.rename(
|
||||
oclifInstallers[process.platform],
|
||||
@ -320,22 +441,30 @@ async function renameInstallerFiles() {
|
||||
|
||||
/**
|
||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||
*/
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
'-f',
|
||||
exeName,
|
||||
// trust ...
|
||||
await execFileAsync('signtool.exe', [
|
||||
'sign',
|
||||
'-sha1',
|
||||
process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
|
||||
'-tr',
|
||||
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||
'-td',
|
||||
'SHA256',
|
||||
'-fd',
|
||||
'SHA256',
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
exeName,
|
||||
]);
|
||||
// ... but verify
|
||||
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
@ -344,7 +473,27 @@ async function signWindowsInstaller() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* Wait for Apple Installer Notarization to continue
|
||||
*/
|
||||
async function notarizeMacInstaller(): Promise<void> {
|
||||
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
|
||||
const appleId =
|
||||
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||
|
||||
if (appleIdPassword && teamId) {
|
||||
await notarize({
|
||||
tool: 'notarytool',
|
||||
teamId,
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
@ -354,9 +503,10 @@ export async function buildOclifInstaller() {
|
||||
let packOpts = ['-r', ROOT];
|
||||
if (process.platform === 'darwin') {
|
||||
packOS = 'macos';
|
||||
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
|
||||
} else if (process.platform === 'win32') {
|
||||
packOS = 'win';
|
||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||
packOpts = packOpts.concat('--targets', 'win32-x64');
|
||||
}
|
||||
if (packOS) {
|
||||
console.log(`Building oclif installer for CLI ${version}`);
|
||||
@ -370,10 +520,11 @@ export async function buildOclifInstaller() {
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||
await renameInstallerFiles();
|
||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||
// The macOS installer is automatically signed by oclif (which runs the
|
||||
@ -381,6 +532,10 @@ export async function buildOclifInstaller() {
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
} else if (process.platform === 'darwin') {
|
||||
console.log('Notarizing package...');
|
||||
await notarizeMacInstaller(); // Notarize
|
||||
console.log('Package notarized.');
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
import * as path from 'path';
|
||||
import { MarkdownFileParser } from './utils';
|
||||
import { GlobSync } from 'glob';
|
||||
|
||||
/**
|
||||
* This is the skeleton of CLI documentation/reference web page at:
|
||||
@ -24,163 +25,120 @@ import { MarkdownFileParser } from './utils';
|
||||
*
|
||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||
* some content to this object.
|
||||
*
|
||||
* IMPORTANT
|
||||
*
|
||||
* All commands need to be stored under a folder in src/commands to maintain uniformity
|
||||
* Generating docs will error out if directive not followed
|
||||
* To add a custom heading for command docs, add the heading next to the folder name
|
||||
* in the `commandHeadings` dictionary.
|
||||
*
|
||||
* This dictionary is the source of truth that creates the docs config which is used
|
||||
* to generate the CLI documentation. By default, the folder name will be used.
|
||||
*
|
||||
* Resources with plural names needs to have 2 sections if they have commands like:
|
||||
* "fleet, fleets" or "device, devices" or "tag, tags"
|
||||
*
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: [
|
||||
'build/commands/apps.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [
|
||||
'build/commands/login.js',
|
||||
'build/commands/logout.js',
|
||||
'build/commands/whoami.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [
|
||||
'build/commands/devices/index.js',
|
||||
'build/commands/devices/supported.js',
|
||||
'build/commands/device/index.js',
|
||||
'build/commands/device/identify.js',
|
||||
'build/commands/device/init.js',
|
||||
'build/commands/device/move.js',
|
||||
'build/commands/device/os-update.js',
|
||||
'build/commands/device/public-url.js',
|
||||
'build/commands/device/purge.js',
|
||||
'build/commands/device/reboot.js',
|
||||
'build/commands/device/register.js',
|
||||
'build/commands/device/rename.js',
|
||||
'build/commands/device/restart.js',
|
||||
'build/commands/device/rm.js',
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/commands/envs.js',
|
||||
'build/commands/env/add.js',
|
||||
'build/commands/env/rename.js',
|
||||
'build/commands/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: [
|
||||
'build/commands/tags.js',
|
||||
'build/commands/tag/rm.js',
|
||||
'build/commands/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['help', 'build/commands/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [
|
||||
'build/commands/keys.js',
|
||||
'build/commands/key/index.js',
|
||||
'build/commands/key/add.js',
|
||||
'build/commands/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/commands/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/commands/scan.js',
|
||||
'build/commands/ssh.js',
|
||||
'build/commands/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/commands/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [
|
||||
'build/commands/os/build-config.js',
|
||||
'build/commands/os/configure.js',
|
||||
'build/commands/os/versions.js',
|
||||
'build/commands/os/download.js',
|
||||
'build/commands/os/initialize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [
|
||||
'build/commands/config/generate.js',
|
||||
'build/commands/config/inject.js',
|
||||
'build/commands/config/read.js',
|
||||
'build/commands/config/reconfigure.js',
|
||||
'build/commands/config/write.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/commands/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/commands/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/commands/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [
|
||||
'build/commands/local/configure.js',
|
||||
'build/commands/local/flash.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/commands/build.js', 'build/commands/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/commands/join.js', 'build/commands/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/commands/util/available-drives.js'],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
files: ['build/commands/support.js'],
|
||||
},
|
||||
],
|
||||
|
||||
interface Category {
|
||||
title: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
interface Documentation {
|
||||
title: string;
|
||||
introduction: string;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
// Mapping folders names to custom headings in the docs
|
||||
const commandHeadings: { [key: string]: string } = {
|
||||
'api-key': 'API Keys',
|
||||
'api-keys': 'API Keys',
|
||||
login: 'Authentication',
|
||||
whoami: 'Authentication',
|
||||
logout: 'Authentication',
|
||||
env: 'Environment Variables',
|
||||
envs: 'Environment Variables',
|
||||
help: 'Help and Version',
|
||||
key: 'SSH Keys',
|
||||
keys: 'SSH Keys',
|
||||
orgs: 'Organizations',
|
||||
os: 'OS',
|
||||
util: 'Utilities',
|
||||
ssh: 'Network',
|
||||
scan: 'Network',
|
||||
tunnel: 'Network',
|
||||
build: 'Deploy',
|
||||
join: 'Platform',
|
||||
leave: 'Platform',
|
||||
app: 'Apps',
|
||||
block: 'Blocks',
|
||||
device: 'Devices',
|
||||
fleet: 'Fleets',
|
||||
release: 'Releases',
|
||||
tag: 'Tags',
|
||||
};
|
||||
|
||||
// Fetch all available commands
|
||||
const allCommandsPaths = new GlobSync('build/commands/**/*.js', {
|
||||
ignore: 'build/commands/internal/**',
|
||||
}).found;
|
||||
|
||||
// Throw error if any commands found outside of command directories
|
||||
const illegalCommandPaths = allCommandsPaths.filter((commandPath: string) =>
|
||||
/^build\/commands\/[^/]+\.js$/.test(commandPath),
|
||||
);
|
||||
|
||||
if (illegalCommandPaths.length !== 0) {
|
||||
throw new Error(
|
||||
`Found the following commands without a command directory: ${illegalCommandPaths}\n
|
||||
To resolve this error, move the respective commands to their resource directories or create new ones.\n
|
||||
Refer to the automation/capitanodoc/capitanodoc.ts file for more information.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Docs config template
|
||||
const capitanoDoc: Documentation = {
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
// Helper function to capitalize each word of directory name
|
||||
function formatTitle(dir: string): string {
|
||||
return dir.replace(/(^\w|\s\w)/g, (word) => word.toUpperCase());
|
||||
}
|
||||
|
||||
// Create a map to track the categories for faster lookup
|
||||
const categoriesMap: { [key: string]: Category } = {};
|
||||
|
||||
for (const commandPath of allCommandsPaths) {
|
||||
const commandDir = path.basename(path.dirname(commandPath));
|
||||
const heading = commandHeadings[commandDir] || formatTitle(commandDir);
|
||||
|
||||
if (!categoriesMap[heading]) {
|
||||
categoriesMap[heading] = { title: heading, files: [] };
|
||||
capitanoDoc.categories.push(categoriesMap[heading]);
|
||||
}
|
||||
|
||||
categoriesMap[heading].files.push(commandPath);
|
||||
}
|
||||
|
||||
// Sort Category titles alphabetically
|
||||
capitanoDoc.categories = capitanoDoc.categories.sort((a, b) =>
|
||||
a.title.localeCompare(b.title),
|
||||
);
|
||||
|
||||
// Sort Category file paths alphabetically
|
||||
capitanoDoc.categories.forEach((category) => {
|
||||
category.files.sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
/**
|
||||
* Modify and return the `capitanoDoc` object above in order to render the
|
||||
* CLI documentation/reference web page at:
|
||||
* https://www.balena.io/docs/reference/cli/
|
||||
* Modify and return the `capitanoDoc` object above in order to generate the
|
||||
* CLI documentation at docs/balena-cli.md
|
||||
*
|
||||
* This function parses the README.md file to extract relevant sections
|
||||
* for the documentation web page.
|
||||
|
2
automation/capitanodoc/doc-types.d.ts
vendored
2
automation/capitanodoc/doc-types.d.ts
vendored
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import type { Command as OclifCommandClass } from '@oclif/core';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
|
@ -16,9 +16,9 @@
|
||||
*/
|
||||
import * as path from 'path';
|
||||
import { getCapitanoDoc } from './capitanodoc';
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
import type { Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
import { stripIndent } from '../../lib/utils/lazy';
|
||||
import { stripIndent } from '../../src/utils/lazy';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
@ -39,7 +39,7 @@ export async function renderMarkdown(): Promise<string> {
|
||||
};
|
||||
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(...importOclifCommands(jsFilename));
|
||||
category.commands.push(...(await importOclifCommands(jsFilename)));
|
||||
}
|
||||
result.categories.push(category);
|
||||
}
|
||||
@ -58,16 +58,15 @@ class FakeHelpCommand {
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help apps',
|
||||
'$ balena help login',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
args = [
|
||||
{
|
||||
name: 'command',
|
||||
args = {
|
||||
command: {
|
||||
description: 'command to show help for',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
usage = 'help [command]';
|
||||
|
||||
@ -79,15 +78,18 @@ class FakeHelpCommand {
|
||||
};
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
async function importOclifCommands(
|
||||
jsFilename: string,
|
||||
): Promise<OclifCommand[]> {
|
||||
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||
// an error when parsed. This should be improved so that `usage` does not have
|
||||
// to be overridden if not necessary.
|
||||
|
||||
const command: OclifCommand =
|
||||
jsFilename === 'help'
|
||||
? ((new FakeHelpCommand() as unknown) as OclifCommand)
|
||||
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
|
||||
? (new FakeHelpCommand() as unknown as OclifCommand)
|
||||
: ((await import(path.join(process.cwd(), jsFilename)))
|
||||
.default as OclifCommand);
|
||||
|
||||
return [command];
|
||||
}
|
||||
@ -101,8 +103,9 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
printMarkdown();
|
||||
|
@ -14,13 +14,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import { Parser } from '@oclif/core';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
import { getManualSortCompareFunction } from '../../src/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../src/utils/oclif-utils';
|
||||
import type { Category, Document, OclifCommand } from './doc-types';
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage || '')}`];
|
||||
@ -37,8 +37,8 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
for (const [name, arg] of Object.entries(command.args!)) {
|
||||
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
const flagUsage = Parser.flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
|
@ -15,41 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { OptionDefinition } from 'capitano';
|
||||
import * as ent from 'ent';
|
||||
import * as fs from 'fs';
|
||||
import * as readline from 'readline';
|
||||
|
||||
export function getOptionPrefix(signature: string) {
|
||||
if (signature.length > 1) {
|
||||
return '--';
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionSignature(signature: string) {
|
||||
return `${getOptionPrefix(signature)}${signature}`;
|
||||
}
|
||||
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (Array.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (typeof option.alias === 'string') {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
if (option.parameter) {
|
||||
result += ` <${option.parameter}>`;
|
||||
}
|
||||
|
||||
return ent.encode(result);
|
||||
}
|
||||
|
||||
export class MarkdownFileParser {
|
||||
constructor(public mdFilePath: string) {}
|
||||
|
||||
|
@ -15,24 +15,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const stripIndent = require('common-tags/lib/stripIndent');
|
||||
const _ = require('lodash');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { simpleGit } from 'simple-git';
|
||||
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
/**
|
||||
* Compare the timestamp of cli.markdown with the timestamp of staged files,
|
||||
* issuing an error if cli.markdown is older.
|
||||
* If cli.markdown does not require updating and the developer cannot run
|
||||
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
|
||||
* issuing an error if balena-cli.md is older.
|
||||
* If balena-cli.md does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
||||
const git = simpleGit(ROOT);
|
||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
@ -42,8 +43,8 @@ async function checkBuildTimestamps() {
|
||||
...gitStatus.staged,
|
||||
...gitStatus.renamed.map((o) => o.to),
|
||||
])
|
||||
// select only staged files that start with lib/ or typings/
|
||||
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
||||
// select only staged files that start with src/ or typings/
|
||||
.filter((f) => f.match(/^(src|typings)[/\\]/))
|
||||
.map((f) => path.join(ROOT, f));
|
||||
|
||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||
@ -81,4 +82,5 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
@ -6,6 +6,8 @@
|
||||
*
|
||||
* We don't `require('semver')` to allow this script to be run as a npm
|
||||
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
@ -16,9 +18,13 @@ function parseSemver(version) {
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} v1
|
||||
* @param {string} v2
|
||||
*/
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
const v1Array = parseSemver(v1);
|
||||
const v2Array = parseSemver(v2);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
@ -41,17 +47,25 @@ function checkNpmVersion() {
|
||||
// the reason is that it would unnecessarily prevent end users from
|
||||
// using npm v6.4.1 that ships with Node 8. (It is OK for the
|
||||
// shrinkwrap file to get damaged if it is not going to be reused.)
|
||||
console.error(`\
|
||||
-------------------------------------------------------------------------------
|
||||
throw new Error(`\
|
||||
-----------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
-----------------------------------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
||||
function main() {
|
||||
try {
|
||||
checkNpmVersion();
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -15,58 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { throttling } from '@octokit/plugin-throttling';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
|
||||
/**
|
||||
* Create or update a release in GitHub's releases page, uploading the
|
||||
* installer files (standalone zip + native oclif installers).
|
||||
*/
|
||||
export async function createGitHubRelease() {
|
||||
console.log(`Publishing release ${version} to GitHub`);
|
||||
const publishRelease = await import('publish-release');
|
||||
const ghRelease = await Bluebird.fromCallback(
|
||||
publishRelease.bind(null, {
|
||||
token: GITHUB_TOKEN || '',
|
||||
owner: 'balena-io',
|
||||
repo: 'balena-cli',
|
||||
tag: version,
|
||||
name: `balena-CLI ${version}`,
|
||||
reuseRelease: true,
|
||||
assets: finalReleaseAssets[process.platform],
|
||||
}),
|
||||
);
|
||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level function to create a CLI release in GitHub's releases page:
|
||||
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
||||
* display on the releases page, and call createGitHubRelease() to upload
|
||||
* the files.
|
||||
*/
|
||||
export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
|
||||
(require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling'))
|
||||
.throttling,
|
||||
);
|
||||
return new Octokit({
|
||||
const OctokitConstructor = Octokit.plugin(throttling);
|
||||
return new OctokitConstructor({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
@ -102,15 +61,16 @@ const getOctokit = _.once(function () {
|
||||
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
||||
* (3rd, 4th, 5th...) of the first item in the current page.
|
||||
*/
|
||||
function getPageNumbers(
|
||||
async function getPageNumbers(
|
||||
response: any,
|
||||
perPageDefault: number,
|
||||
): { page: number; pages: number; ordinal: number } {
|
||||
): Promise<{ page: number; pages: number; ordinal: number }> {
|
||||
const res = { page: 1, pages: 1, ordinal: 1 };
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = require('parse-link-header') as typeof import('parse-link-header');
|
||||
const parse = await import('parse-link-header');
|
||||
|
||||
const parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||
@ -120,13 +80,13 @@ function getPageNumbers(
|
||||
if (parsed.next.per_page) {
|
||||
perPage = parseInt(parsed.next.per_page, 10);
|
||||
}
|
||||
res.page = parseInt(parsed.next.page, 10) - 1;
|
||||
res.pages = parseInt(parsed.last.page, 10);
|
||||
res.page = parseInt(parsed.next.page!, 10) - 1;
|
||||
res.pages = parseInt(parsed.last!.page!, 10);
|
||||
} else {
|
||||
if (parsed.prev.per_page) {
|
||||
perPage = parseInt(parsed.prev.per_page, 10);
|
||||
if (parsed.prev!.per_page) {
|
||||
perPage = parseInt(parsed.prev!.per_page, 10);
|
||||
}
|
||||
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
||||
res.page = res.pages = parseInt(parsed.prev!.page!, 10) + 1;
|
||||
}
|
||||
res.ordinal = (res.page - 1) * perPage + 1;
|
||||
return res;
|
||||
@ -152,17 +112,20 @@ async function updateGitHubReleaseDescriptions(
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||
const options = octokit.repos.listReleases.endpoint.merge({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
for await (const response of octokit.paginate.iterator(options)) {
|
||||
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
|
||||
response,
|
||||
perPage,
|
||||
);
|
||||
type Release =
|
||||
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
||||
const {
|
||||
page: thisPage,
|
||||
pages: totalPages,
|
||||
ordinal,
|
||||
} = await getPageNumbers(response, perPage);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
|
@ -21,13 +21,10 @@ import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
catchUncommitted,
|
||||
signFilesForNotarization,
|
||||
testShrinkwrap,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
|
||||
import { updateDescriptionOfReleasesAffectedByIssue1359 } from './deploy-bin';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
@ -36,88 +33,57 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
* 'build:installer' (to build a native oclif installer)
|
||||
* 'build:standalone' (to build a standalone pkg package)
|
||||
* 'release' (to create/update a GitHub release)
|
||||
*
|
||||
* In the case of 'build:installer', also call runUnderMsys() to switch the
|
||||
* shell from cmd.exe to MSYS2 bash.exe.
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
async function parse(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
|
||||
console.error(`[debug] automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
throw new Error('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'sign:binaries': signFilesForNotarization,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
'test-shrinkwrap': testShrinkwrap,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
if (!Object.hasOwn(commands, arg)) {
|
||||
throw new Error(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If runUnderMsys() is called to re-execute this script under MSYS2,
|
||||
// the current working dir becomes the MSYS2 homedir, so we change back.
|
||||
process.chdir(ROOT);
|
||||
|
||||
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||
// (patched) to copy/extract the CLI files, run npm install and then
|
||||
// create the NSIS executable installer for Windows. This was necessary
|
||||
// to avoid issues with a 260-char limit on Windows paths (possibly a
|
||||
// limitation of some library used by NSIS), as the "current working dir"
|
||||
// provided by balena CI is a rather long path to start with.
|
||||
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
|
||||
const randID = (await import('crypto'))
|
||||
.randomBytes(6)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_'); // base64url (RFC 4648)
|
||||
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
try {
|
||||
if (arg === 'build:installer' && process.platform === 'win32') {
|
||||
// ensure running under MSYS2
|
||||
if (!process.env.MSYSTEM) {
|
||||
process.env.MSYS2_PATH_TYPE = 'inherit';
|
||||
await runUnderMsys([
|
||||
fixPathForMsys(process.argv[0]),
|
||||
fixPathForMsys(process.argv[1]),
|
||||
arg,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
|
||||
throw new Error(
|
||||
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
|
||||
);
|
||||
}
|
||||
}
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
return exitWithError(`"${arg}": ${err}`);
|
||||
if (typeof err === 'object') {
|
||||
err.message = `"${arg}": ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See jsdoc for parse() function above */
|
||||
export async function run(args?: string[]) {
|
||||
try {
|
||||
await parse(args);
|
||||
} catch (e) {
|
||||
console.error(e.message ? `Error: ${e.message}` : e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
||||
|
@ -10,7 +10,10 @@ npm i
|
||||
if ! diff -q npm-shrinkwrap.json npm-shrinkwrap.json.old > /dev/null; then
|
||||
rm npm-shrinkwrap.json.old
|
||||
echo "** npm-shrinkwrap.json was not deduplicated or not fully committed - FAIL **";
|
||||
echo "** Please run 'npm ci', followed by 'npm dedupe' **";
|
||||
echo "** This can usually be fixed with: **";
|
||||
echo "** git checkout master -- npm-shrinkwrap.json **";
|
||||
echo "** rm -rf node_modules **";
|
||||
echo "** npm install && npm dedupe && npm install **";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
|
@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Invalid change type: '${maybeChangeType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -37,8 +36,8 @@ const run = async (cmd: string) => {
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout.pipe(process.stdout);
|
||||
p.stderr.pipe(process.stderr);
|
||||
p.stdout?.pipe(process.stdout);
|
||||
p.stderr?.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
@ -58,31 +57,24 @@ const getUpstreams = async () => {
|
||||
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
|
||||
|
||||
const yaml = await import('js-yaml');
|
||||
const { upstream } = yaml.safeLoad(repoYaml) as {
|
||||
const { upstream } = yaml.load(repoYaml) as {
|
||||
upstream: Upstream[];
|
||||
};
|
||||
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`,
|
||||
);
|
||||
return process.exit(1);
|
||||
};
|
||||
`;
|
||||
|
||||
// TODO: Drop the wrapper function once we move to TS 3.8,
|
||||
// which will support top level await.
|
||||
async function main() {
|
||||
async function $main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
throw new Error(getUsage(upstreams, '$upstreamName'));
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
@ -90,16 +82,15 @@ async function main() {
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
throw new Error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
throw new Error(getUsage(upstreams, upstreamName));
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
@ -108,8 +99,7 @@ async function main() {
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
console.error(`Already on version '${newVersion}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Already on version '${newVersion}'`);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
@ -117,11 +107,11 @@ async function main() {
|
||||
|
||||
const changeType = process.argv[4]
|
||||
? // if the caller specified a change type, use that one
|
||||
validateChangeType(process.argv[4])
|
||||
validateChangeType(process.argv[4])
|
||||
: // use the same change type as in the dependency, but avoid major bumps
|
||||
semverChangeType && semverChangeType !== 'major'
|
||||
? semverChangeType
|
||||
: 'minor';
|
||||
semverChangeType && semverChangeType !== 'major'
|
||||
? semverChangeType
|
||||
: 'minor';
|
||||
console.log(`Using Change-type: ${changeType}`);
|
||||
|
||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||
@ -137,4 +127,14 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
main();
|
||||
|
@ -16,12 +16,10 @@
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
import * as fs from 'fs';
|
||||
import { diffTrimmedLines } from 'diff';
|
||||
|
||||
export const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
@ -68,7 +66,6 @@ export class StdOutTap {
|
||||
* https://www.npmjs.com/package/diff
|
||||
*/
|
||||
export function diffLines(str1: string, str2: string): string {
|
||||
const { diffTrimmedLines } = require('diff');
|
||||
const diffObjs = diffTrimmedLines(str1, str2);
|
||||
const prefix = (chunk: string, char: string) =>
|
||||
chunk
|
||||
@ -80,102 +77,18 @@ export function diffLines(str1: string, str2: string): string {
|
||||
return part.added
|
||||
? prefix(part.value, '+')
|
||||
: part.removed
|
||||
? prefix(part.value, '-')
|
||||
: prefix(part.value, ' ');
|
||||
? prefix(part.value, '-')
|
||||
: prefix(part.value, ' ');
|
||||
})
|
||||
.join('\n');
|
||||
return diffStr;
|
||||
}
|
||||
|
||||
export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
const packageJsonPath = path.join(ROOT, 'package.json');
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', (code) => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
(line) => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
return JSON.parse(packageJson);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,7 +120,7 @@ export async function which(program: string): Promise<string> {
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args?: string[],
|
||||
args: string[] = [],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
|
@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
scan ssh util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
22
bin/balena
22
bin/balena
@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: __dirname + '/.fast-boot.json',
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
1
bin/balena
Symbolic link
1
bin/balena
Symbolic link
@ -0,0 +1 @@
|
||||
run.js
|
@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
// Allow balena-dev to work with oclif by temporarily
|
||||
// pointing oclif config options to lib/ instead of build/
|
||||
modifyOclifPaths();
|
||||
// Undo changes on exit
|
||||
process.on('exit', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
// Undo changes in case of ctrl-c
|
||||
process.on('SIGINT', function () {
|
||||
modifyOclifPaths(true);
|
||||
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||
// but without it commands can not be exited.
|
||||
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||
// Ideally a better solution is needed.
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||
// it is supposed to run faster. We still benefit from type checking when
|
||||
// running 'npm run build'.
|
||||
require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../lib/app').run();
|
||||
|
||||
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||
function modifyOclifPaths(revert) {
|
||||
const fs = require('fs');
|
||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageObj = JSON.parse(packageJson);
|
||||
|
||||
if (!packageObj.oclif) {
|
||||
return;
|
||||
}
|
||||
|
||||
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||
if (!revert) {
|
||||
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
|
||||
} else {
|
||||
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
|
||||
}
|
||||
|
||||
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
1
bin/balena-dev
Symbolic link
1
bin/balena-dev
Symbolic link
@ -0,0 +1 @@
|
||||
dev.js
|
3
bin/dev.cmd
Normal file
3
bin/dev.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
87
bin/dev.js
Executable file
87
bin/dev.js
Executable file
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||
// behavior when changes are made to dependencies during development. This is
|
||||
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||
// `package.json` to modify oclif paths, and this results in cache
|
||||
// invalidation and a performance hit rather than speedup.
|
||||
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||
// when the `node_modules` folder is manually modified (affecting transitive
|
||||
// dependencies) during development (e.g. bug investigations). A workaround
|
||||
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||
// `CONTRIBUTING.md`.
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
// Allow balena-dev to work with oclif by temporarily
|
||||
// pointing oclif config options to src/ instead of build/
|
||||
modifyOclifPaths();
|
||||
// Undo changes on exit
|
||||
process.on('exit', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
// Undo changes in case of ctrl-c
|
||||
process.on('SIGINT', function () {
|
||||
modifyOclifPaths(true);
|
||||
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||
// but without it commands can not be exited.
|
||||
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||
// Ideally a better solution is needed.
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||
// it is supposed to run faster. We still benefit from type checking when
|
||||
// running 'npm run build'.
|
||||
require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../src/app').run(undefined, { dir: __dirname, development: true });
|
||||
|
||||
// Modify package.json oclif paths from build/ -> src/, or vice versa
|
||||
function modifyOclifPaths(revert) {
|
||||
const fs = require('fs');
|
||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageObj = JSON.parse(packageJson);
|
||||
|
||||
if (!packageObj.oclif) {
|
||||
return;
|
||||
}
|
||||
|
||||
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||
if (!revert) {
|
||||
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/src/');
|
||||
} else {
|
||||
oclifSectionText = oclifSectionText.replace(/\/src\//g, '/build/');
|
||||
}
|
||||
|
||||
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
3
bin/run.cmd
Normal file
3
bin/run.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
21
bin/run.js
Executable file
21
bin/run.js
Executable file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
async function run() {
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
await require('../build/fast-boot').start();
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Run the CLI
|
||||
await require('../build/app').run(undefined, { dir: __dirname });
|
||||
}
|
||||
|
||||
run();
|
91
completion/_balena
Normal file
91
completion/_balena
Normal file
@ -0,0 +1,91 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( api-key api-keys app block build config deploy device devices env envs fleet fleets internal join key keys leave local login logout logs notes orgs os preload push release releases scan settings ssh support tag tags tunnel util version whoami )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate revoke )
|
||||
app_cmds=( create )
|
||||
block_cmds=( create )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( export finalize import invalidate validate )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
"api-key")
|
||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
||||
;;
|
||||
"app")
|
||||
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
||||
;;
|
||||
"block")
|
||||
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
|
||||
;;
|
||||
"config")
|
||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||
;;
|
||||
"device")
|
||||
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
||||
;;
|
||||
"devices")
|
||||
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
|
||||
;;
|
||||
"env")
|
||||
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
||||
;;
|
||||
"fleet")
|
||||
_describe -t fleet_cmds 'fleet_cmd' fleet_cmds "$@" && ret=0
|
||||
;;
|
||||
"internal")
|
||||
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
||||
;;
|
||||
"key")
|
||||
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
|
||||
;;
|
||||
"local")
|
||||
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
||||
;;
|
||||
"os")
|
||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||
;;
|
||||
"release")
|
||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||
;;
|
||||
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
88
completion/balena-completion.bash
Normal file
88
completion/balena-completion.bash
Normal file
@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="api-key api-keys app block build config deploy device devices env envs fleet fleets internal join key keys leave local login logout logs notes orgs os preload push release releases scan settings ssh support tag tags tunnel util version whoami"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate revoke"
|
||||
app_cmds="create"
|
||||
block_cmds="create"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="export finalize import invalidate validate"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
api-key)
|
||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||
;;
|
||||
app)
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
block)
|
||||
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
|
||||
;;
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
device)
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
devices)
|
||||
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
fleet)
|
||||
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
|
||||
;;
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
key)
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
local)
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
175
completion/generate-completion.js
Normal file
175
completion/generate-completion.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const fs = require('fs');
|
||||
const manifestFile = 'oclif.manifest.json';
|
||||
|
||||
commandsFilePath = path.join(rootDir, manifestFile);
|
||||
if (fs.existsSync(commandsFilePath)) {
|
||||
console.log('Generating shell auto completion files...');
|
||||
} else {
|
||||
console.error(`generate-completion.js: Could not find "${manifestFile}"`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||
|
||||
const mainCommands = [];
|
||||
const additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands).sort()) {
|
||||
const cmd = key.split(':');
|
||||
if (cmd.length > 1) {
|
||||
additionalCommands.push(cmd);
|
||||
if (!mainCommands.includes(cmd[0])) {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
} else {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
}
|
||||
const mainCommandsStr = mainCommands.join(' ');
|
||||
|
||||
// GENERATE BASH COMPLETION FILE
|
||||
bashFilePathIn = path.join(__dirname, '/templates/bash.template');
|
||||
bashFilePathOut = path.join(__dirname, 'balena-completion.bash');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(bashFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR BASH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands="' + mainCommandsStr + '"',
|
||||
);
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' ' +
|
||||
prevElement +
|
||||
')\n COMPREPLY=( $(compgen -W "$' +
|
||||
prevElement2 +
|
||||
'" -- $cur) )\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(bashFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GENERATE ZSH COMPLETION FILE
|
||||
zshFilePathIn = path.join(__dirname, '/templates/zsh.template');
|
||||
zshFilePathOut = path.join(__dirname, '_balena');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(zshFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR ZSH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands=( ' + mainCommandsStr + ' )',
|
||||
);
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' "' +
|
||||
prevElement +
|
||||
'")\n _describe -t ' +
|
||||
prevElement2 +
|
||||
" '" +
|
||||
prevElement +
|
||||
"_cmd' " +
|
||||
prevElement2 +
|
||||
' "$@" && ret=0\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(zshFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
32
completion/templates/bash.template
Normal file
32
completion/templates/bash.template
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
#TEMPLATE FILE FOR BASH COMPLETION#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
$sub_cmds_prev$
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
35
completion/templates/zsh.template
Normal file
35
completion/templates/zsh.template
Normal file
@ -0,0 +1,35 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#TEMPLATE FILE FOR ZSH COMPLETION#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
$sub_cmds_prev$
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
@ -1,112 +0,0 @@
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
It requires collecting some preliminary information _once_.
|
||||
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
You can run this command as many times as you need, putting the new medium (SD card / USB stick) each time.
|
||||
|
||||
But before you can run it you need to collect the parameters and build the configuration file. Keep reading to figure out how to do it.
|
||||
|
||||
|
||||
## Collect all the required parameters.
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
1. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
balena os versions DEVICE_TYPE
|
||||
```
|
||||
and pick the version that you need, like _v2.0.6+rev1.prod_.
|
||||
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
|
||||
guarantees full compatibility between the steps.
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
The balena CLI will not display the system drives to protect you,
|
||||
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
|
||||
|
||||
Now we have all the parameters -- time to build the config file.
|
||||
|
||||
## Build the config file
|
||||
|
||||
Interactive device provisioning process often includes collecting some extra device configuration, like the networking mode and wifi credentials.
|
||||
|
||||
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
|
||||
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
|
||||
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
|
||||
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
```
|
||||
|
||||
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
|
||||
|
||||
```bash
|
||||
cat CONFIG_FILE
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
Now you're ready to run the command in the beginning of this guide.
|
||||
|
||||
Please note again that all of these steps only need to be done once (unless you need to change something), and once all the parameters are collected the main init command can be run unchanged.
|
||||
|
||||
But there are still some nuances to cover, please read below.
|
||||
|
||||
## Nuances
|
||||
|
||||
### `sudo` password on *nix systems
|
||||
|
||||
In order to write the image to the raw device we need the root permissions, this is unavoidable.
|
||||
|
||||
To improve the security we only run the minimal subcommand with `sudo`.
|
||||
|
||||
This means that with the default setup you're interrupted closer to the end of the device init process to enter your sudo password for this subcommand to work.
|
||||
|
||||
There are several ways to eliminate it and make the process fully non-interactive.
|
||||
|
||||
#### Option 1: make passwordless sudo.
|
||||
|
||||
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
|
||||
|
||||
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
|
||||
### Extra initialization config
|
||||
|
||||
As of June 2017 all the supported devices should not require any other interactive configuration.
|
||||
|
||||
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
|
||||
|
||||
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
File diff suppressed because it is too large
Load Diff
15
gulpfile.js
15
gulpfile.js
@ -1,15 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const inlinesource = require('gulp-inline-source');
|
||||
|
||||
const OPTIONS = {
|
||||
files: {
|
||||
pages: 'lib/auth/pages/*.ejs',
|
||||
},
|
||||
};
|
||||
|
||||
gulp.task('pages', () =>
|
||||
gulp
|
||||
.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages')),
|
||||
);
|
@ -1,152 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an application.
|
||||
|
||||
Create a new balena application.
|
||||
|
||||
You can specify the organization the application should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
The application's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --organization mmyorg',
|
||||
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description:
|
||||
'handle of the organization the application should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'application device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
let application: Application;
|
||||
try {
|
||||
application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: application "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create applications in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Output
|
||||
const { isV13 } = await import('../../utils/version');
|
||||
console.log(
|
||||
isV13()
|
||||
? `Application created: slug "${application.slug}", device type "${deviceType}"`
|
||||
: `Application created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
}
|
||||
|
||||
export default class AppCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single application.
|
||||
|
||||
Display detailed information about a single balena application.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = ['$ balena app MyApp', '$ balena app myorg/myapp'];
|
||||
|
||||
public static args = [ca.applicationRequired];
|
||||
|
||||
public static usage = 'app <nameOrSlug>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(
|
||||
getBalenaSdk(),
|
||||
params.application,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
},
|
||||
)) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
// For display purposes:
|
||||
device_type: string;
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export default class AppsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all applications.
|
||||
|
||||
list all your balena applications.
|
||||
|
||||
For detailed information on a particular application,
|
||||
use \`balena app <application>\` instead.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena apps'];
|
||||
|
||||
public static usage = 'apps';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'No-op since release v12.0.0',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(AppsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications = (await balena.models.application.getAll({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
const _ = await import('lodash');
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
// @ts-expect-error
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ConfigReadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Read the configuration of a device or OS image.
|
||||
|
||||
Read the config.json file from the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config read --type raspberrypi3',
|
||||
'$ balena config read --type raspberrypi3 --drive /dev/disk2',
|
||||
];
|
||||
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.info(prettyjson.render(configJSON));
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class ConfigWriteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Write a key-value pair to configuration of a device or OS image.
|
||||
|
||||
Write a key-value pair to the config.json file on the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config write --type raspberrypi3 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 --drive /dev/disk2 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 files.network/settings "..."',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'key',
|
||||
description: 'the key of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
description: 'the value of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigWriteCmd,
|
||||
);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
const _ = await import('lodash');
|
||||
_.set(configJSON, params.key, params.value);
|
||||
|
||||
await umountAsync(drive);
|
||||
|
||||
await config.write(drive, options.type, configJSON);
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application, BalenaSDK } from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
application_name?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceMoveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move one or more devices to another application.
|
||||
|
||||
Move one or more devices to another application.
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device move 7cf02a6',
|
||||
'$ balena device move 7cf02a6,dc39e52',
|
||||
'$ balena device move 7cf02a6 --application MyNewApp',
|
||||
'$ balena device move 7cf02a6 -a myorg/mynewapp',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device move <uuid(s)>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceMoveCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const { expandForAppName } = await import('../../utils/helpers');
|
||||
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
.split(',')
|
||||
.map((id) => tryAsInteger(id));
|
||||
|
||||
// Get devices
|
||||
const devices = await Promise.all(
|
||||
deviceIds.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, expandForAppName) as Promise<
|
||||
ExtendedDevice
|
||||
>,
|
||||
),
|
||||
);
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
}
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
// Get destination application
|
||||
const application = options.application
|
||||
? await getApplication(balena, options.application)
|
||||
: await this.interactivelySelectApplication(balena, devices);
|
||||
|
||||
// Move each device
|
||||
for (const uuid of deviceIds) {
|
||||
try {
|
||||
await balena.models.device.move(uuid, application.id);
|
||||
console.info(
|
||||
`Device ${uuid} was moved to application ${application.slug}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async interactivelySelectApplication(
|
||||
balena: BalenaSDK,
|
||||
devices: ExtendedDevice[],
|
||||
) {
|
||||
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
|
||||
Promise.all(
|
||||
devices.map((device) =>
|
||||
balena.models.device.getManifestBySlug(
|
||||
device.is_of__device_type[0].slug,
|
||||
),
|
||||
),
|
||||
),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
|
||||
deviceDeviceTypes.every(
|
||||
(deviceDeviceType) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
),
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypes.some(
|
||||
(dt) => dt.slug === app.is_for__device_type[0].slug,
|
||||
) &&
|
||||
// @ts-ignore using the extended device object prop
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string | null;
|
||||
device_type?: string | null;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
|
||||
list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${jsonInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
'$ balena devices --application MyApp',
|
||||
'$ balena devices --app MyApp',
|
||||
'$ balena devices -a MyApp',
|
||||
'$ balena devices -a myorg/myapp',
|
||||
];
|
||||
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
let devices;
|
||||
|
||||
if (options.application != null) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, options.application);
|
||||
devices = (await balena.models.device.getAllByApplication(
|
||||
application.id,
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
} else {
|
||||
devices = (await balena.models.device.getAll(
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
}
|
||||
|
||||
devices = devices.map(function (device) {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]?.app_name || null;
|
||||
|
||||
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
|
||||
|
||||
device.device_type = device.is_of__device_type?.[0]?.slug || null;
|
||||
return device;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
const _ = await import('lodash');
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
devices.map((device) => _.pick(device, fields)),
|
||||
null,
|
||||
4,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Command from '../../command';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
// 'Internal' commands are called during the execution of other commands.
|
||||
// `scandevices` is called during by `join`,`leave'.
|
||||
// TODO: These should be refactored to modules/functions, and removed
|
||||
// See previous `internal sudo` refactor:
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455/files
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||
|
||||
export default class ScandevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for local balena-enabled devices and show a picker to choose one.
|
||||
|
||||
Don't use this command directly!
|
||||
`;
|
||||
|
||||
public static usage = 'internal scandevices';
|
||||
|
||||
public static root = true;
|
||||
public static hidden = true;
|
||||
|
||||
public async run() {
|
||||
const { forms } = await import('balena-sync');
|
||||
try {
|
||||
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
return console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('could not find any')) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
output: string;
|
||||
version?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class OsDownloadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Download an unconfigured OS image.
|
||||
|
||||
Download an unconfigured OS image for a certain device type.
|
||||
Check available types with \`balena devices supported\`
|
||||
|
||||
Note: Currently this command only works with balenaCloud, not openBalena.
|
||||
If using openBalena, please download the OS from: https://www.balena.io/os/
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded (if available), otherwise the newest version (if all existing
|
||||
versions for the given device type are pre-release).
|
||||
|
||||
You can pass \`--version menu\` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
|
||||
To download a development image append \`.dev\` to the version or select from
|
||||
the interactive menu.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'type',
|
||||
description: 'the device type',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'os download <type>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
output: flags.string({
|
||||
description: 'output path',
|
||||
char: 'o',
|
||||
required: true,
|
||||
}),
|
||||
version: flags.string({
|
||||
description: stripIndent`
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)
|
||||
`,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsDownloadCmd,
|
||||
);
|
||||
|
||||
const { downloadOSImage } = await import('../../utils/cloud');
|
||||
|
||||
await downloadOSImage(params.type, options.output, options.version);
|
||||
}
|
||||
}
|
@ -1,384 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
port?: number;
|
||||
tty: boolean;
|
||||
verbose: boolean;
|
||||
noproxy: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export default class SshCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
SSH into the host or application container of a device.
|
||||
|
||||
Start a shell on a local or remote device. If a service name is not provided,
|
||||
a shell will be opened on the host OS.
|
||||
|
||||
If an application is provided, an interactive menu will be presented
|
||||
for the selection of an online device. A shell will then be opened for the
|
||||
host OS or service container of the chosen device.
|
||||
|
||||
For local devices, the IP address and .local domain name are supported.
|
||||
If the device is referenced by IP or \`.local\` address, the connection
|
||||
is initiated directly to balenaOS on port \`22222\` via an
|
||||
openssh-compatible client. Otherwise, any connection initiated remotely
|
||||
traverses the balenaCloud VPN.
|
||||
|
||||
Commands may be piped to the standard input for remote execution (see examples).
|
||||
Note however that remote command execution on service containers (as opposed to
|
||||
the host OS) is not currently possible when a device UUID is used (instead of
|
||||
an IP address) because of a balenaCloud backend limitation.
|
||||
|
||||
Note: \`balena ssh\` requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check:
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies,
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena ssh MyApp',
|
||||
'$ balena ssh f49cefd',
|
||||
'$ balena ssh f49cefd my-service',
|
||||
'$ balena ssh f49cefd --port <port>',
|
||||
'$ balena ssh 192.168.0.1 --verbose',
|
||||
'$ balena ssh f49cefd.local my-service',
|
||||
'$ echo "uptime; exit;" | balena ssh f49cefd',
|
||||
'$ echo "uptime; exit;" | balena ssh 192.168.0.1 myService',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'applicationOrDevice',
|
||||
description:
|
||||
'application name/slug/id, device uuid, or address of local device',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'service',
|
||||
description: 'service name, if connecting to a container',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'ssh <applicationOrDevice> [service]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
port: flags.integer({
|
||||
description: stripIndent`
|
||||
SSH server port number (default 22222) if the target is an IP address or .local
|
||||
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
|
||||
char: 'p',
|
||||
parse: (p) => parseAsInteger(p, 'port'),
|
||||
}),
|
||||
tty: flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
|
||||
char: 't',
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
description: 'increase verbosity',
|
||||
char: 'v',
|
||||
}),
|
||||
noproxy: flags.boolean({
|
||||
default: false,
|
||||
description: 'bypass global proxy configuration for the ssh connection',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
SshCmd,
|
||||
);
|
||||
|
||||
// Local connection
|
||||
if (validateLocalHostnameOrIp(params.applicationOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: params.applicationOrDevice,
|
||||
port: options.port,
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.service,
|
||||
});
|
||||
}
|
||||
|
||||
// Remote connection
|
||||
const { getProxyConfig, which } = await import('../utils/helpers');
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
const proxyConfig = getProxyConfig();
|
||||
const useProxy = !!proxyConfig && !options.noproxy;
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
await Command.checkLoggedIn();
|
||||
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||
sdk,
|
||||
params.applicationOrDevice,
|
||||
);
|
||||
|
||||
const device = await sdk.models.device.get(deviceUuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
|
||||
const deviceId = device.id;
|
||||
const supervisorVersion = device.supervisor_version;
|
||||
|
||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? which('proxytunnel', false) : undefined,
|
||||
sdk.auth.whoami(),
|
||||
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
|
||||
// service, currently "balena-devices.com", rather than some
|
||||
// local proxy server URL
|
||||
sdk.settings.get('proxyUrl'),
|
||||
]);
|
||||
|
||||
const getSshProxyCommand = () => {
|
||||
if (!proxyConfig) {
|
||||
return;
|
||||
}
|
||||
if (!whichProxytunnel) {
|
||||
console.warn(stripIndent`
|
||||
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
|
||||
Please install it if you want to route the \`balena ssh\` requests through the proxy.
|
||||
Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config
|
||||
for the \`ssh\` requests.
|
||||
|
||||
Attempting the unproxied request for now.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const p = proxyConfig;
|
||||
if (p.username && p.password) {
|
||||
// proxytunnel understands these variables for proxy authentication.
|
||||
// Setting the variables instead of command-line options avoids the
|
||||
// need for shell-specific escaping of special characters like '$'.
|
||||
process.env.PROXYUSER = p.username;
|
||||
process.env.PROXYPASS = p.password;
|
||||
}
|
||||
|
||||
return [
|
||||
'proxytunnel',
|
||||
`--proxy=${p.host}:${p.port}`,
|
||||
// ssh replaces these %h:%p variables in the ProxyCommand option
|
||||
// https://linux.die.net/man/5/ssh_config
|
||||
'--dest=%h:%p',
|
||||
...(options.verbose ? ['--verbose'] : []),
|
||||
];
|
||||
};
|
||||
|
||||
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
|
||||
|
||||
// At this point, we have a long uuid of a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.service != null) {
|
||||
containerId = await this.getContainerId(
|
||||
sdk,
|
||||
deviceUuid,
|
||||
params.service,
|
||||
{
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
},
|
||||
supervisorVersion,
|
||||
deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
if (containerId != null) {
|
||||
accessCommand = `enter ${deviceUuid} ${containerId}`;
|
||||
} else {
|
||||
accessCommand = `host ${deviceUuid}`;
|
||||
}
|
||||
|
||||
const command = this.generateVpnSshCommand({
|
||||
uuid: deviceUuid,
|
||||
command: accessCommand,
|
||||
verbose: options.verbose,
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
|
||||
return spawnSshAndThrowOnError(command);
|
||||
}
|
||||
|
||||
async getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
uuid: string,
|
||||
serviceName: string,
|
||||
sshOpts: {
|
||||
port?: number;
|
||||
proxyCommand?: string[];
|
||||
proxyUrl: string;
|
||||
username: string;
|
||||
},
|
||||
version?: string,
|
||||
id?: number,
|
||||
): Promise<string> {
|
||||
const semver = await import('balena-semver');
|
||||
|
||||
if (version == null || id == null) {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version'],
|
||||
});
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
}
|
||||
|
||||
let containerId: string | undefined;
|
||||
if (semver.gte(version, '8.6.0')) {
|
||||
const apiUrl = await sdk.settings.get('apiUrl');
|
||||
// TODO: Move this into the SDKs device model
|
||||
const request = await sdk.request.send({
|
||||
method: 'POST',
|
||||
url: '/supervisor/v2/containerId',
|
||||
baseUrl: apiUrl,
|
||||
body: {
|
||||
method: 'GET',
|
||||
deviceId: id,
|
||||
},
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
} else {
|
||||
console.error(stripIndent`
|
||||
Using legacy method to detect container ID. This will be slow.
|
||||
To speed up this process, please update your device to an OS
|
||||
which has a supervisor version of at least v8.6.0.
|
||||
`);
|
||||
// We need to execute a balena ps command on the device,
|
||||
// and parse the output, looking for a specific
|
||||
// container
|
||||
const childProcess = await import('child_process');
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const { which } = await import('../utils/helpers');
|
||||
const { deviceContainerEngineBinary } = await import(
|
||||
'../utils/device/ssh'
|
||||
);
|
||||
|
||||
const sshBinary = await which('ssh');
|
||||
const sshArgs = this.generateVpnSshCommand({
|
||||
uuid,
|
||||
verbose: false,
|
||||
port: sshOpts.port,
|
||||
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
|
||||
proxyCommand: sshOpts.proxyCommand,
|
||||
proxyUrl: sshOpts.proxyUrl,
|
||||
username: sshOpts.username,
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
|
||||
}
|
||||
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
|
||||
stdio: [null, 'pipe', null],
|
||||
});
|
||||
const containers = await new Promise<string>((resolve, reject) => {
|
||||
const output: string[] = [];
|
||||
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
|
||||
subProcess.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Non-zero error code when looking for service container: ${code}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve(output.join(''));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lines = containers.split('\n');
|
||||
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
|
||||
for (const container of lines) {
|
||||
const [cId, name] = container.split(' ');
|
||||
if (regex.test(name)) {
|
||||
containerId = cId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
throw new Error(
|
||||
`Could not find a service ${serviceName} on device ${uuid}.`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
generateVpnSshCommand(opts: {
|
||||
uuid: string;
|
||||
command: string;
|
||||
verbose: boolean;
|
||||
port?: number;
|
||||
username: string;
|
||||
proxyUrl: string;
|
||||
proxyCommand?: string[];
|
||||
}) {
|
||||
return [
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(opts.proxyCommand && opts.proxyCommand.length
|
||||
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
...(opts.port ? ['-p', opts.port.toString()] : []),
|
||||
`${opts.username}@ssh.${opts.proxyUrl}`,
|
||||
opts.command,
|
||||
];
|
||||
}
|
||||
}
|
124
lib/events.ts
124
lib/events.ts
@ -1,124 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as Mixpanel from 'mixpanel';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { getBalenaSdk } from './utils/lazy';
|
||||
|
||||
const getMixpanel = _.once((balenaUrl: string) => {
|
||||
return Mixpanel.init('balena-main', {
|
||||
host: `api.${balenaUrl}`,
|
||||
path: '/mixpanel',
|
||||
protocol: 'https',
|
||||
});
|
||||
});
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixpanel.com analytics tracking (information on balena CLI usage).
|
||||
*
|
||||
* @param commandSignature A string like, for example:
|
||||
* "push <applicationOrDevice>"
|
||||
* That's literally so: "applicationOrDevice" is NOT replaced with the actual
|
||||
* application ID or device ID. The purpose is to find out the most / least
|
||||
* used command verbs, so we can focus our development effort where it is most
|
||||
* beneficial to end users.
|
||||
*
|
||||
* The username and command signature are also added as extra context
|
||||
* information in Sentry.io error reporting, for CLI debugging purposes
|
||||
* (mainly unexpected/unhandled exceptions -- see also `lib/errors.ts`).
|
||||
*
|
||||
* For more details on the data collected by balena generally, check this page:
|
||||
* https://www.balena.io/docs/learn/more/collected-data/
|
||||
*/
|
||||
export async function trackCommand(commandSignature: string) {
|
||||
try {
|
||||
let Sentry: typeof import('@sentry/node');
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry = await import('@sentry/node');
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get('balenaUrl') as string;
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get('dataDirectory') as string;
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token;
|
||||
try {
|
||||
token = await storage.get('token');
|
||||
} catch {
|
||||
// If we can't get a token then we can't get a username
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await storage.get('cachedUsername')) as CachedUsername;
|
||||
if (result.token === token) {
|
||||
return result.username;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const balena = getBalenaSdk();
|
||||
const $username = await balena.auth.whoami();
|
||||
await storage.set('cachedUsername', {
|
||||
token,
|
||||
username: $username,
|
||||
} as CachedUsername);
|
||||
return $username;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
const mixpanel = getMixpanel(balenaUrl);
|
||||
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
id: username,
|
||||
username,
|
||||
});
|
||||
});
|
||||
}
|
||||
// Don't actually call mixpanel.track() while running test cases, or if suppressed
|
||||
if (
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
await mixpanel.track(`[CLI] ${commandSignature}`, {
|
||||
distinct_id: username,
|
||||
version: packageJSON.version,
|
||||
node: process.version,
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
platform: process.platform,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
199
lib/help.ts
199
lib/help.ts
@ -1,199 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Help from '@oclif/plugin-help';
|
||||
import * as indent from 'indent-string';
|
||||
import { getChalk } from './utils/lazy';
|
||||
import { renderList } from '@oclif/plugin-help/lib/list';
|
||||
import { ExpectedError } from './errors';
|
||||
|
||||
// Partially overrides standard implementation of help plugin
|
||||
// https://github.com/oclif/plugin-help/blob/master/src/index.ts
|
||||
|
||||
function getHelpSubject(args: string[]): string | undefined {
|
||||
for (const arg of args) {
|
||||
if (arg === '--') {
|
||||
return;
|
||||
}
|
||||
if (arg === 'help' || arg === '--help' || arg === '-h') {
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('-')) {
|
||||
return;
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
export default class BalenaHelp extends Help {
|
||||
public static usage: 'help [command]';
|
||||
|
||||
public showHelp(argv: string[]) {
|
||||
const chalk = getChalk();
|
||||
const subject = getHelpSubject(argv);
|
||||
if (!subject) {
|
||||
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
||||
this.showCustomRootHelp(verbose);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = this.config.findCommand(subject);
|
||||
if (command) {
|
||||
this.showCommandHelp(command);
|
||||
return;
|
||||
}
|
||||
|
||||
// If they've typed a topic (e.g. `balena os`) that isn't also a command (e.g. `balena device`)
|
||||
// then list the associated commands.
|
||||
const topicCommands = this.config.commands.filter((c) => {
|
||||
return c.id.startsWith(`${subject}:`);
|
||||
});
|
||||
if (topicCommands.length > 0) {
|
||||
console.log(`${chalk.yellow(subject)} commands include:`);
|
||||
console.log(this.formatCommands(topicCommands));
|
||||
console.log(
|
||||
`\nRun ${chalk.cyan.bold(
|
||||
'balena help -v',
|
||||
)} for a list of all available commands,`,
|
||||
);
|
||||
console.log(
|
||||
` or ${chalk.cyan.bold(
|
||||
'balena help <command>',
|
||||
)} for detailed help on a specific command.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
||||
}
|
||||
|
||||
showCustomRootHelp(showAllCommands: boolean): void {
|
||||
const chalk = getChalk();
|
||||
const bold = chalk.bold;
|
||||
const cmd = chalk.cyan.bold;
|
||||
|
||||
let commands = this.config.commands;
|
||||
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
||||
|
||||
// Get Primary Commands, sorted as in manual list
|
||||
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
});
|
||||
|
||||
// Get the rest as Additional Commands
|
||||
const additionalCommands = commands.filter(
|
||||
(c) =>
|
||||
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
||||
);
|
||||
|
||||
// Find longest usage, and pad usage of first command in each category
|
||||
// This is to ensure that both categories align visually
|
||||
const usageLength = commands
|
||||
.map((c) => c.usage?.length || 0)
|
||||
.reduce((longest, l) => {
|
||||
return l > longest ? l : longest;
|
||||
});
|
||||
|
||||
if (
|
||||
typeof primaryCommands[0]?.usage === 'string' &&
|
||||
typeof additionalCommands[0]?.usage === 'string'
|
||||
) {
|
||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||
additionalCommands[0].usage = additionalCommands[0].usage.padEnd(
|
||||
usageLength,
|
||||
);
|
||||
}
|
||||
|
||||
// Output help
|
||||
console.log(bold('USAGE'));
|
||||
console.log('$ balena [COMMAND] [OPTIONS]');
|
||||
|
||||
console.log(bold('\nPRIMARY COMMANDS'));
|
||||
console.log(this.formatCommands(primaryCommands));
|
||||
|
||||
if (showAllCommands) {
|
||||
console.log(bold('\nADDITIONAL COMMANDS'));
|
||||
console.log(this.formatCommands(additionalCommands));
|
||||
} else {
|
||||
console.log(
|
||||
`\n${bold('...MORE')} run ${cmd(
|
||||
'balena help --verbose',
|
||||
)} to list additional commands.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(bold('\nGLOBAL OPTIONS'));
|
||||
console.log(' --help, -h');
|
||||
console.log(' --debug\n');
|
||||
|
||||
const {
|
||||
reachingOut,
|
||||
} = require('./utils/messages') as typeof import('./utils/messages');
|
||||
console.log(reachingOut);
|
||||
}
|
||||
|
||||
protected formatCommands(commands: any[]): string {
|
||||
if (commands.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const body = renderList(
|
||||
commands
|
||||
.filter((c) => c.usage != null && c.usage !== '')
|
||||
.map((c) => [c.usage, this.formatDescription(c.description)]),
|
||||
{
|
||||
spacer: '\n',
|
||||
stripAnsi: this.opts.stripAnsi,
|
||||
maxWidth: this.opts.maxWidth - 2,
|
||||
},
|
||||
);
|
||||
|
||||
return indent(body, 2);
|
||||
}
|
||||
|
||||
protected formatDescription(desc: string = '') {
|
||||
const chalk = getChalk();
|
||||
|
||||
desc = desc.split('\n')[0];
|
||||
// Remove any ending .
|
||||
if (desc[desc.length - 1] === '.') {
|
||||
desc = desc.substring(0, desc.length - 1);
|
||||
}
|
||||
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')
|
||||
if (desc[1] === desc[1]?.toLowerCase()) {
|
||||
desc = `${desc[0].toLowerCase()}${desc.substring(1)}`;
|
||||
}
|
||||
return chalk.grey(desc);
|
||||
}
|
||||
|
||||
readonly manuallySortedPrimaryCommands = [
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'scan',
|
||||
];
|
||||
}
|
@ -1,698 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getChalk } from './lazy';
|
||||
|
||||
/**
|
||||
* @returns Promise<{import('./compose-types').ComposeOpts}>
|
||||
*/
|
||||
export function generateOpts(options) {
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
if (options.gitignore && options['multi-dockerignore']) {
|
||||
throw new ExpectedError(
|
||||
'The --gitignore and --multi-dockerignore options cannot be used together',
|
||||
);
|
||||
}
|
||||
return fs.realpath(options.source || '.').then((projectPath) => ({
|
||||
projectName: options.projectName,
|
||||
projectPath,
|
||||
inlineLogs: !options.nologs,
|
||||
convertEol: !options['noconvert-eol'],
|
||||
dockerfilePath: options.dockerfile,
|
||||
multiDockerignore: !!options['multi-dockerignore'],
|
||||
nogitignore: !options.gitignore,
|
||||
noParentCheck: options['noparent-check'],
|
||||
}));
|
||||
}
|
||||
|
||||
// Parse the given composition and return a structure with info. Input is:
|
||||
// - composePath: the *absolute* path to the directory containing the compose file
|
||||
// - composeStr: the contents of the compose file, as a string
|
||||
/**
|
||||
* @param {string} composePath
|
||||
* @param {string} composeStr
|
||||
* @param {string | null} projectName
|
||||
* @returns {import('./compose-types').ComposeProject}
|
||||
*/
|
||||
export function createProject(composePath, composeStr, projectName = null) {
|
||||
const yml = require('js-yaml');
|
||||
const compose = require('resin-compose-parse');
|
||||
|
||||
// both methods below may throw.
|
||||
const rawComposition = yml.safeLoad(composeStr, {
|
||||
schema: yml.FAILSAFE_SCHEMA,
|
||||
});
|
||||
const composition = compose.normalize(rawComposition);
|
||||
|
||||
projectName ??= path.basename(composePath);
|
||||
|
||||
const descriptors = compose.parse(composition).map(function (descr) {
|
||||
// generate an image name based on the project and service names
|
||||
// if one is not given and the service requires a build
|
||||
if (
|
||||
typeof descr.image !== 'string' &&
|
||||
descr.image.context != null &&
|
||||
descr.image.tag == null
|
||||
) {
|
||||
descr.image.tag = [projectName, descr.serviceName]
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
}
|
||||
return descr;
|
||||
});
|
||||
return {
|
||||
path: composePath,
|
||||
name: projectName,
|
||||
composition,
|
||||
descriptors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the CLI v10 / v11 "original" tarDirectory function. It is still
|
||||
* around for the benefit of the `--gitignore` option, but is expected to be
|
||||
* deleted in CLI v13.
|
||||
* @param {string} dir Source directory
|
||||
* @param {import('./compose-types').TarDirectoryOptions} param
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
*/
|
||||
export async function originalTarDirectory(dir, param) {
|
||||
let {
|
||||
preFinalizeCallback = null,
|
||||
convertEol = false,
|
||||
nogitignore = false,
|
||||
} = param;
|
||||
if (convertEol == null) {
|
||||
convertEol = false;
|
||||
}
|
||||
|
||||
const Bluebird = require('bluebird');
|
||||
const tar = require('tar-stream');
|
||||
const klaw = require('klaw');
|
||||
const { promises: fs } = require('fs');
|
||||
const streamToPromise = require('stream-to-promise');
|
||||
const { printGitignoreWarn } = require('./compose_ts');
|
||||
const { FileIgnorer, IgnoreFileType } = require('./ignore');
|
||||
const { toPosixPath } = require('resin-multibuild').PathUtils;
|
||||
let readFile;
|
||||
if (process.platform === 'win32') {
|
||||
const { readFileWithEolConversion } = require('./eol-conversion');
|
||||
readFile = (file) => readFileWithEolConversion(file, convertEol);
|
||||
} else {
|
||||
({ readFile } = fs);
|
||||
}
|
||||
|
||||
const getFiles = () =>
|
||||
// @ts-ignore `klaw` returns a `Walker` which is close enough to a stream to work but ts complains
|
||||
Bluebird.resolve(streamToPromise(klaw(dir)))
|
||||
.filter((item) => !item.stats.isDirectory())
|
||||
.map((item) => item.path);
|
||||
|
||||
const ignore = new FileIgnorer(dir);
|
||||
const pack = tar.pack();
|
||||
const ignoreFiles = {};
|
||||
return getFiles()
|
||||
.each(function (file) {
|
||||
const type = ignore.getIgnoreFileType(path.relative(dir, file));
|
||||
if (type != null) {
|
||||
ignoreFiles[type] = ignoreFiles[type] || [];
|
||||
ignoreFiles[type].push(path.resolve(dir, file));
|
||||
return ignore.addIgnoreFile(file, type);
|
||||
}
|
||||
})
|
||||
.tap(() => {
|
||||
if (!nogitignore) {
|
||||
printGitignoreWarn(
|
||||
(ignoreFiles[IgnoreFileType.DockerIgnore] || [])[0] || '',
|
||||
ignoreFiles[IgnoreFileType.GitIgnore] || [],
|
||||
);
|
||||
}
|
||||
})
|
||||
.filter(ignore.filter)
|
||||
.map(function (file) {
|
||||
const relPath = path.relative(path.resolve(dir), file);
|
||||
return Promise.all([relPath, fs.stat(file), readFile(file)]).then(
|
||||
([filename, stats, data]) =>
|
||||
pack.entry(
|
||||
{
|
||||
name: toPosixPath(filename),
|
||||
mtime: stats.mtime,
|
||||
size: stats.size,
|
||||
mode: stats.mode,
|
||||
},
|
||||
data,
|
||||
),
|
||||
);
|
||||
})
|
||||
.then(() => preFinalizeCallback?.(pack))
|
||||
.then(function () {
|
||||
pack.finalize();
|
||||
return pack;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiEndpoint
|
||||
* @param {string} auth
|
||||
* @param {number} userId
|
||||
* @param {number} appId
|
||||
* @param {import('resin-compose-parse').Composition} composition
|
||||
* @returns {Promise<import('./compose-types').Release>}
|
||||
*/
|
||||
export const createRelease = async function (
|
||||
apiEndpoint,
|
||||
auth,
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
) {
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const releaseMod = require('balena-release');
|
||||
|
||||
const client = releaseMod.createClient({ apiEndpoint, auth });
|
||||
|
||||
const { release, serviceImages } = await releaseMod.create({
|
||||
client,
|
||||
user: userId,
|
||||
application: appId,
|
||||
composition,
|
||||
source: 'local',
|
||||
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
|
||||
});
|
||||
|
||||
return {
|
||||
client,
|
||||
release: _.omit(release, [
|
||||
'created_at',
|
||||
'belongs_to__application',
|
||||
'is_created_by__user',
|
||||
'__metadata',
|
||||
]),
|
||||
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
|
||||
_.omit(serviceImage, [
|
||||
'created_at',
|
||||
'is_a_build_of__service',
|
||||
'__metadata',
|
||||
]),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {Array<import('./compose-types').BuiltImage>} images
|
||||
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages
|
||||
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
|
||||
*/
|
||||
export const tagServiceImages = (docker, images, serviceImages) =>
|
||||
Promise.all(
|
||||
images.map(function (d) {
|
||||
const serviceImage = serviceImages[d.serviceName];
|
||||
const imageName = serviceImage.is_stored_at__image_location;
|
||||
const match = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName);
|
||||
if (match == null) {
|
||||
throw new Error(`Could not parse imageName: '${imageName}'`);
|
||||
}
|
||||
const [, registry, repo, tag = 'latest'] = match;
|
||||
const name = `${registry}/${repo}`;
|
||||
return docker
|
||||
.getImage(d.name)
|
||||
.tag({ repo: name, tag, force: true })
|
||||
.then(() => docker.getImage(`${name}:${tag}`))
|
||||
.then((localImage) => ({
|
||||
serviceName: d.serviceName,
|
||||
serviceImage,
|
||||
localImage,
|
||||
registry,
|
||||
repo,
|
||||
logs: d.logs,
|
||||
props: d.props,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {import('./logger')} logger
|
||||
* @param {number} appID
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export const getPreviousRepos = (sdk, docker, logger, appID) =>
|
||||
sdk.pine
|
||||
.get({
|
||||
resource: 'release',
|
||||
options: {
|
||||
$filter: {
|
||||
belongs_to__application: appID,
|
||||
status: 'success',
|
||||
},
|
||||
$select: ['id'],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$expand: 'image',
|
||||
},
|
||||
},
|
||||
$orderby: 'id desc',
|
||||
$top: 1,
|
||||
},
|
||||
})
|
||||
.then(function (release) {
|
||||
// grab all images from the latest release, return all image locations in the registry
|
||||
if (release.length > 0) {
|
||||
const images = release[0].contains__image;
|
||||
return Promise.all(
|
||||
images.map(function (d) {
|
||||
const imageName = d.image[0].is_stored_at__image_location;
|
||||
return docker
|
||||
.getRegistryAndName(imageName)
|
||||
.then(function (registry) {
|
||||
logger.logDebug(
|
||||
`Requesting access to previously pushed image repo (${registry.imageName})`,
|
||||
);
|
||||
return registry.imageName;
|
||||
});
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.logDebug(`Failed to access previously pushed image repo: ${e}`);
|
||||
return [];
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {string} tokenAuthEndpoint
|
||||
* @param {string} registry
|
||||
* @param {string[]} images
|
||||
* @param {string[]} previousRepos
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const authorizePush = function (
|
||||
sdk,
|
||||
tokenAuthEndpoint,
|
||||
registry,
|
||||
images,
|
||||
previousRepos,
|
||||
) {
|
||||
if (!Array.isArray(images)) {
|
||||
images = [images];
|
||||
}
|
||||
|
||||
images.push(...previousRepos);
|
||||
return sdk.request
|
||||
.send({
|
||||
baseUrl: tokenAuthEndpoint,
|
||||
url: '/auth/v1/token',
|
||||
qs: {
|
||||
service: registry,
|
||||
scope: images.map((repo) => `repository:${repo}:pull,push`),
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body.token)
|
||||
.catch(() => '');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {string} token
|
||||
* @param {Array<import('./compose-types').TaggedImage>} images
|
||||
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
|
||||
*/
|
||||
export const pushAndUpdateServiceImages = function (
|
||||
docker,
|
||||
token,
|
||||
images,
|
||||
afterEach,
|
||||
) {
|
||||
const { DockerProgress } = require('docker-progress');
|
||||
const { retry } = require('./helpers');
|
||||
const tty = require('./tty')(process.stdout);
|
||||
const Bluebird = require('bluebird');
|
||||
|
||||
const opts = { authconfig: { registrytoken: token } };
|
||||
|
||||
const progress = new DockerProgress({ dockerToolbelt: docker });
|
||||
const renderer = pushProgressRenderer(
|
||||
tty,
|
||||
getChalk().blue('[Push]') + ' ',
|
||||
);
|
||||
const reporters = progress.aggregateProgress(images.length, renderer);
|
||||
|
||||
return Bluebird.using(tty.cursorHidden(), () =>
|
||||
Promise.all(
|
||||
images.map(({ serviceImage, localImage, props, logs }, index) =>
|
||||
Promise.all([
|
||||
localImage.inspect().then((img) => img.Size),
|
||||
retry({
|
||||
// @ts-ignore
|
||||
func: () => progress.push(localImage.name, reporters[index], opts),
|
||||
maxAttempts: 3, // try calling func 3 times (max)
|
||||
// @ts-ignore
|
||||
label: localImage.name, // label for retry log messages
|
||||
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
|
||||
backoffScaler: 1.4, // wait multiplier for each retry
|
||||
}).finally(renderer.end),
|
||||
])
|
||||
.then(
|
||||
/** @type {([number, string]) => void} */
|
||||
function ([size, digest]) {
|
||||
serviceImage.image_size = size;
|
||||
serviceImage.content_hash = digest;
|
||||
serviceImage.build_log = logs;
|
||||
serviceImage.dockerfile = props.dockerfile;
|
||||
serviceImage.project_type = props.projectType;
|
||||
if (props.startTime) {
|
||||
serviceImage.start_timestamp = props.startTime;
|
||||
}
|
||||
if (props.endTime) {
|
||||
serviceImage.end_timestamp = props.endTime;
|
||||
}
|
||||
serviceImage.push_timestamp = new Date();
|
||||
serviceImage.status = 'success';
|
||||
},
|
||||
)
|
||||
.catch(function (e) {
|
||||
serviceImage.error_message = '' + e;
|
||||
serviceImage.status = 'failed';
|
||||
throw e;
|
||||
})
|
||||
.finally(() => afterEach?.(serviceImage, props)),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// utilities
|
||||
|
||||
const renderProgressBar = function (percentage, stepCount) {
|
||||
const _ = require('lodash');
|
||||
percentage = _.clamp(percentage, 0, 100);
|
||||
const barCount = Math.floor((stepCount * percentage) / 100);
|
||||
const spaceCount = stepCount - barCount;
|
||||
const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`;
|
||||
return `${bar} ${_.padStart(percentage, 3)}%`;
|
||||
};
|
||||
|
||||
var pushProgressRenderer = function (tty, prefix) {
|
||||
const fn = function (e) {
|
||||
const { error, percentage } = e;
|
||||
if (error != null) {
|
||||
throw new Error(error);
|
||||
}
|
||||
const bar = renderProgressBar(percentage, 40);
|
||||
return tty.replaceLine(`${prefix}${bar}\r`);
|
||||
};
|
||||
fn.end = () => {
|
||||
tty.clearLine();
|
||||
};
|
||||
return fn;
|
||||
};
|
||||
|
||||
export class BuildProgressUI {
|
||||
constructor(tty, descriptors) {
|
||||
this._handleEvent = this._handleEvent.bind(this);
|
||||
this._handleInterrupt = this._handleInterrupt.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this._display = this._display.bind(this);
|
||||
const _ = require('lodash');
|
||||
const through = require('through2');
|
||||
|
||||
const eventHandler = this._handleEvent;
|
||||
const services = _.map(descriptors, 'serviceName');
|
||||
|
||||
const streams = _(services)
|
||||
.map(function (service) {
|
||||
const stream = through.obj(function (event, _enc, cb) {
|
||||
eventHandler(service, event);
|
||||
return cb();
|
||||
});
|
||||
stream.pipe(tty.stream, { end: false });
|
||||
return [service, stream];
|
||||
})
|
||||
.fromPairs()
|
||||
.value();
|
||||
|
||||
this._tty = tty;
|
||||
this._serviceToDataMap = {};
|
||||
this._services = services;
|
||||
|
||||
// Logger magically prefixes the log line with [Build] etc., but it doesn't
|
||||
// work well with the spinner we're also showing. Manually build the prefix
|
||||
// here and bypass the logger.
|
||||
const prefix = getChalk().blue('[Build]') + ' ';
|
||||
|
||||
const offset = 10; // account for escape sequences inserted for colouring
|
||||
this._prefixWidth =
|
||||
offset + prefix.length + _.max(_.map(services, 'length'));
|
||||
this._prefix = prefix;
|
||||
|
||||
// these are to handle window wrapping
|
||||
this._maxLineWidth = null;
|
||||
this._lineWidths = [];
|
||||
|
||||
this._startTime = null;
|
||||
this._ended = false;
|
||||
this._cancelled = false;
|
||||
this._spinner = require('./compose_ts').createSpinner();
|
||||
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
_handleEvent(service, event) {
|
||||
this._serviceToDataMap[service] = event;
|
||||
}
|
||||
|
||||
_handleInterrupt() {
|
||||
this._cancelled = true;
|
||||
this.end();
|
||||
return process.exit(130); // 128 + SIGINT
|
||||
}
|
||||
|
||||
start() {
|
||||
process.on('SIGINT', this._handleInterrupt);
|
||||
this._tty.hideCursor();
|
||||
this._services.forEach((service) => {
|
||||
this.streams[service].write({ status: 'Preparing...' });
|
||||
});
|
||||
this._runloop = require('./compose_ts').createRunLoop(this._display);
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
end(summary = null) {
|
||||
if (this._ended) {
|
||||
return;
|
||||
}
|
||||
this._ended = true;
|
||||
process.removeListener('SIGINT', this._handleInterrupt);
|
||||
this._runloop?.end();
|
||||
this._runloop = null;
|
||||
|
||||
this._clear();
|
||||
this._renderStatus(true);
|
||||
this._renderSummary(summary ?? this._getServiceSummary());
|
||||
this._tty.showCursor();
|
||||
}
|
||||
|
||||
_display() {
|
||||
this._clear();
|
||||
this._renderStatus();
|
||||
this._renderSummary(this._getServiceSummary());
|
||||
this._tty.cursorUp(this._services.length + 1); // for status line
|
||||
}
|
||||
|
||||
_clear() {
|
||||
this._tty.deleteToEnd();
|
||||
this._maxLineWidth = this._tty.currentWindowSize().width;
|
||||
}
|
||||
|
||||
_getServiceSummary() {
|
||||
const _ = require('lodash');
|
||||
|
||||
const services = this._services;
|
||||
const serviceToDataMap = this._serviceToDataMap;
|
||||
|
||||
return _(services)
|
||||
.map(function (service) {
|
||||
const { status, progress, error } = serviceToDataMap[service] ?? {};
|
||||
if (error) {
|
||||
return `${error}`;
|
||||
} else if (progress) {
|
||||
const bar = renderProgressBar(progress, 20);
|
||||
if (status) {
|
||||
return `${bar} ${status}`;
|
||||
}
|
||||
return `${bar}`;
|
||||
} else if (status) {
|
||||
return `${status}`;
|
||||
} else {
|
||||
return 'Waiting...';
|
||||
}
|
||||
})
|
||||
.map((data, index) => [services[index], data])
|
||||
.fromPairs()
|
||||
.value();
|
||||
}
|
||||
|
||||
_renderStatus(end) {
|
||||
end ??= false;
|
||||
|
||||
const moment = require('moment');
|
||||
require('moment-duration-format')(moment);
|
||||
|
||||
this._tty.clearLine();
|
||||
this._tty.write(this._prefix);
|
||||
if (end && this._cancelled) {
|
||||
this._tty.writeLine('Build cancelled');
|
||||
} else if (end) {
|
||||
const serviceCount = this._services.length;
|
||||
const serviceStr =
|
||||
serviceCount === 1 ? '1 service' : `${serviceCount} services`;
|
||||
const durationStr =
|
||||
this._startTime == null
|
||||
? 'unknown time'
|
||||
: moment
|
||||
.duration(
|
||||
Math.floor((Date.now() - this._startTime) / 1000),
|
||||
'seconds',
|
||||
)
|
||||
.format();
|
||||
this._tty.writeLine(`Built ${serviceStr} in ${durationStr}`);
|
||||
} else {
|
||||
this._tty.writeLine(`Building services... ${this._spinner()}`);
|
||||
}
|
||||
}
|
||||
|
||||
_renderSummary(serviceToStrMap) {
|
||||
const _ = require('lodash');
|
||||
const chalk = getChalk();
|
||||
const truncate = require('cli-truncate');
|
||||
const strlen = require('string-width');
|
||||
|
||||
this._services.forEach((service, index) => {
|
||||
let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth);
|
||||
str += serviceToStrMap[service];
|
||||
if (this._maxLineWidth != null) {
|
||||
str = truncate(str, this._maxLineWidth);
|
||||
}
|
||||
this._lineWidths[index] = strlen(str);
|
||||
|
||||
this._tty.clearLine();
|
||||
this._tty.writeLine(str);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class BuildProgressInline {
|
||||
constructor(outStream, descriptors) {
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this._renderEvent = this._renderEvent.bind(this);
|
||||
const _ = require('lodash');
|
||||
const through = require('through2');
|
||||
|
||||
const services = _.map(descriptors, 'serviceName');
|
||||
const eventHandler = this._renderEvent;
|
||||
const streams = _(services)
|
||||
.map(function (service) {
|
||||
const stream = through.obj(function (event, _enc, cb) {
|
||||
eventHandler(service, event);
|
||||
return cb();
|
||||
});
|
||||
stream.pipe(outStream, { end: false });
|
||||
return [service, stream];
|
||||
})
|
||||
.fromPairs()
|
||||
.value();
|
||||
|
||||
const offset = 10; // account for escape sequences inserted for colouring
|
||||
this._prefixWidth = offset + _.max(_.map(services, 'length'));
|
||||
this._outStream = outStream;
|
||||
this._services = services;
|
||||
this._startTime = null;
|
||||
this._ended = false;
|
||||
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
start() {
|
||||
this._outStream.write('Building services...\n');
|
||||
this._services.forEach((service) => {
|
||||
this.streams[service].write({ status: 'Preparing...' });
|
||||
});
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
end(summary = null) {
|
||||
const moment = require('moment');
|
||||
require('moment-duration-format')(moment);
|
||||
|
||||
if (this._ended) {
|
||||
return;
|
||||
}
|
||||
this._ended = true;
|
||||
|
||||
if (summary != null) {
|
||||
this._services.forEach((service) => {
|
||||
this._renderEvent(service, { status: summary[service] });
|
||||
});
|
||||
}
|
||||
|
||||
const serviceCount = this._services.length;
|
||||
const serviceStr =
|
||||
serviceCount === 1 ? '1 service' : `${serviceCount} services`;
|
||||
const durationStr =
|
||||
this._startTime == null
|
||||
? 'unknown time'
|
||||
: moment
|
||||
.duration(
|
||||
Math.floor((Date.now() - this._startTime) / 1000),
|
||||
'seconds',
|
||||
)
|
||||
.format();
|
||||
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
|
||||
}
|
||||
|
||||
_renderEvent(service, event) {
|
||||
const _ = require('lodash');
|
||||
|
||||
const str = (function () {
|
||||
const { status, error } = event;
|
||||
if (error) {
|
||||
return `${error}`;
|
||||
} else if (status) {
|
||||
return `${status}`;
|
||||
} else {
|
||||
return 'Waiting...';
|
||||
}
|
||||
})();
|
||||
|
||||
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);
|
||||
this._outStream.write(prefix);
|
||||
this._outStream.write(str);
|
||||
this._outStream.write('\n');
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as semver from 'balena-semver';
|
||||
import { getBalenaSdk } from './lazy';
|
||||
|
||||
export interface ImgConfig {
|
||||
applicationName: string;
|
||||
applicationId: number;
|
||||
deviceType: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
appUpdatePollInterval: number;
|
||||
listenPort: number;
|
||||
vpnPort: number;
|
||||
apiEndpoint: string;
|
||||
vpnEndpoint: string;
|
||||
registryEndpoint: string;
|
||||
deltaEndpoint: string;
|
||||
mixpanelToken: string;
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
initialDeviceName?: string;
|
||||
|
||||
// props for older OS versions
|
||||
connectivity?: string;
|
||||
files?: {
|
||||
[filepath: string]: string;
|
||||
};
|
||||
|
||||
// device specific config props
|
||||
deviceId?: number;
|
||||
uuid?: string;
|
||||
registered_at?: number;
|
||||
|
||||
os?: {
|
||||
sshKeys?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateBaseConfig(
|
||||
application: BalenaSdk.Application,
|
||||
options: {
|
||||
version: string;
|
||||
appUpdatePollInterval?: number;
|
||||
deviceType?: string;
|
||||
os?: {
|
||||
sshKeys?: string[];
|
||||
};
|
||||
},
|
||||
): Promise<ImgConfig> {
|
||||
options = {
|
||||
...options,
|
||||
appUpdatePollInterval: options.appUpdatePollInterval || 10,
|
||||
};
|
||||
|
||||
const config = (await getBalenaSdk().models.os.getConfig(
|
||||
application.slug,
|
||||
options,
|
||||
)) as ImgConfig & { apiKey?: string };
|
||||
// os.getConfig always returns a config for an app
|
||||
delete config.apiKey;
|
||||
|
||||
// merge sshKeys to config, when they have been specified
|
||||
if (options.os && options.os.sshKeys) {
|
||||
// Create config.os object if it does not exist
|
||||
config.os = config.os ? config.os : {};
|
||||
config.os.sshKeys = config.os.sshKeys
|
||||
? [...config.os.sshKeys, ...options.os.sshKeys]
|
||||
: options.os.sshKeys;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function generateApplicationConfig(
|
||||
application: BalenaSdk.Application,
|
||||
options: {
|
||||
version: string;
|
||||
deviceType?: string;
|
||||
appUpdatePollInterval?: number;
|
||||
},
|
||||
) {
|
||||
const config = await generateBaseConfig(application, options);
|
||||
|
||||
if (semver.satisfies(options.version, '<2.7.8')) {
|
||||
await addApplicationKey(config, application.id);
|
||||
} else {
|
||||
await addProvisioningKey(config, application.id);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function generateDeviceConfig(
|
||||
device: DeviceWithDeviceType & {
|
||||
belongs_to__application: BalenaSdk.PineDeferred;
|
||||
},
|
||||
deviceApiKey: string | true | undefined,
|
||||
options: { version: string },
|
||||
) {
|
||||
return getBalenaSdk()
|
||||
.models.application.get(device.belongs_to__application.__id)
|
||||
.then(async (application) => {
|
||||
const baseConfigOpts = {
|
||||
...options,
|
||||
deviceType: device.is_of__device_type[0].slug,
|
||||
};
|
||||
const config = await generateBaseConfig(application, baseConfigOpts);
|
||||
|
||||
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
|
||||
await addApplicationKey(config, application.id);
|
||||
} else {
|
||||
await addDeviceKey(config, device.uuid, deviceApiKey || true);
|
||||
}
|
||||
|
||||
return config;
|
||||
})
|
||||
.then((config) => {
|
||||
// Associate a device, to prevent the supervisor
|
||||
// from creating another one on its own.
|
||||
config.registered_at = Math.floor(Date.now() / 1000);
|
||||
config.deviceId = device.id;
|
||||
config.uuid = device.uuid;
|
||||
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
function addApplicationKey(config: any, applicationNameOrId: string | number) {
|
||||
return getBalenaSdk()
|
||||
.models.application.generateApiKey(applicationNameOrId)
|
||||
.then((apiKey) => {
|
||||
config.apiKey = apiKey;
|
||||
return apiKey;
|
||||
});
|
||||
}
|
||||
|
||||
function addProvisioningKey(config: any, applicationNameOrId: string | number) {
|
||||
return getBalenaSdk()
|
||||
.models.application.generateProvisioningKey(applicationNameOrId)
|
||||
.then((apiKey) => {
|
||||
config.apiKey = apiKey;
|
||||
return apiKey;
|
||||
});
|
||||
}
|
||||
|
||||
async function addDeviceKey(
|
||||
config: any,
|
||||
uuid: string,
|
||||
customDeviceApiKey: string | true,
|
||||
) {
|
||||
if (customDeviceApiKey === true) {
|
||||
config.deviceApiKey = await getBalenaSdk().models.device.generateDeviceKey(
|
||||
uuid,
|
||||
);
|
||||
} else {
|
||||
config.deviceApiKey = customDeviceApiKey;
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type { ContainerInfo } from 'dockerode';
|
||||
import { stripIndent } from '../lazy';
|
||||
|
||||
export interface DeviceSSHOpts {
|
||||
address: string;
|
||||
port?: number;
|
||||
forceTTY?: boolean;
|
||||
verbose: boolean;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
|
||||
export async function performLocalDeviceSSH(
|
||||
opts: DeviceSSHOpts,
|
||||
): Promise<void> {
|
||||
const { escapeRegExp, reduce } = await import('lodash');
|
||||
const { spawnSshAndThrowOnError } = await import('../ssh');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
let command = '';
|
||||
|
||||
if (opts.service != null) {
|
||||
// Get the containers which are on-device. Currently we
|
||||
// are single application, which means we can assume any
|
||||
// container which fulfills the form of
|
||||
// $serviceName_$appId_$releaseId is what we want. Once
|
||||
// we have multi-app, we should show a dialog which
|
||||
// allows the user to choose the correct container
|
||||
|
||||
const Docker = await import('dockerode');
|
||||
const docker = new Docker({
|
||||
host: opts.address,
|
||||
port: 2375,
|
||||
});
|
||||
|
||||
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
|
||||
const nameRegex = /\/?([a-zA-Z0-9_]+)_\d+_\d+/;
|
||||
let allContainers: ContainerInfo[];
|
||||
try {
|
||||
allContainers = await docker.listContainers();
|
||||
} catch (_e) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not access docker daemon on device ${opts.address}.
|
||||
Please ensure the device is in local mode.`);
|
||||
}
|
||||
|
||||
const serviceNames: string[] = [];
|
||||
const containers: Array<{ id: string; name: string }> = [];
|
||||
for (const container of allContainers) {
|
||||
for (const name of container.Names) {
|
||||
if (regex.test(name)) {
|
||||
containers.push({ id: container.Id, name });
|
||||
break;
|
||||
}
|
||||
const match = name.match(nameRegex);
|
||||
if (match) {
|
||||
serviceNames.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containers.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Could not find a service on device with name ${opts.service}. ${
|
||||
serviceNames.length > 0
|
||||
? `Available services:\n${reduce(
|
||||
serviceNames,
|
||||
(str, name) => `${str}\t${name}\n`,
|
||||
'',
|
||||
)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
if (containers.length > 1) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Found more than one container matching service name "${opts.service}":
|
||||
${containers.map((container) => container.name).join(', ')}
|
||||
Use different service names to avoid ambiguity.
|
||||
`);
|
||||
}
|
||||
|
||||
const containerId = containers[0].id;
|
||||
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
|
||||
// stdin (fd=0) is not a tty when data is piped in, for example
|
||||
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
|
||||
// See https://www.balena.io/blog/balena-monthly-roundup-january-2020/#charliestipsntricks
|
||||
// https://assets.balena.io/newsletter/2020-01/pipe.png
|
||||
const isTTY = !!opts.forceTTY || (await import('tty')).isatty(0);
|
||||
const ttyFlag = isTTY ? '-t' : '';
|
||||
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
}
|
||||
|
||||
return spawnSshAndThrowOnError([
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-p', opts.port ? opts.port.toString() : '22222'],
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
`root@${opts.address}`,
|
||||
...(command ? [command] : []),
|
||||
]);
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Functions to help actions which rely on using docker
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
const generateConnectOpts = async function (opts) {
|
||||
const { promises: fs } = await import('fs');
|
||||
|
||||
const connectOpts = {};
|
||||
|
||||
// Start with docker-modem defaults which take several env vars into account,
|
||||
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
// https://github.com/apocas/docker-modem/blob/v2.1.3/lib/modem.js#L15-L65
|
||||
const Modem = await import('docker-modem');
|
||||
const defaultOpts = new Modem();
|
||||
const optsOfInterest = [
|
||||
'ca',
|
||||
'cert',
|
||||
'key',
|
||||
'host',
|
||||
'port',
|
||||
'socketPath',
|
||||
'protocol',
|
||||
'username',
|
||||
'sshAuthAgent',
|
||||
'timeout',
|
||||
];
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
|
||||
// Now override the default options with any explicit command line options
|
||||
if (opts.docker != null && opts.dockerHost == null) {
|
||||
// good, local docker socket
|
||||
connectOpts.socketPath = opts.docker;
|
||||
delete connectOpts.host;
|
||||
delete connectOpts.port;
|
||||
} else if (opts.dockerHost != null && opts.docker == null) {
|
||||
// Good a host is provided, and local socket isn't
|
||||
connectOpts.host = opts.dockerHost;
|
||||
connectOpts.port = opts.dockerPort || 2376;
|
||||
delete connectOpts.socketPath;
|
||||
} else if (opts.docker != null && opts.dockerHost != null) {
|
||||
// Both provided, no obvious way to continue
|
||||
throw new ExpectedError(
|
||||
"Both a local docker socket and docker host have been provided. Don't know how to continue.",
|
||||
);
|
||||
}
|
||||
|
||||
// Now need to check if the user wants to connect over TLS
|
||||
// to the host
|
||||
|
||||
// If any are set...
|
||||
if (opts.ca != null || opts.cert != null || opts.key != null) {
|
||||
// but not all
|
||||
if (!(opts.ca != null && opts.cert != null && opts.key != null)) {
|
||||
throw new ExpectedError(
|
||||
'You must provide a CA, certificate and key in order to use TLS',
|
||||
);
|
||||
}
|
||||
|
||||
const [ca, cert, key] = await Promise.all([
|
||||
fs.readFile(opts.ca, 'utf-8'),
|
||||
fs.readFile(opts.cert, 'utf-8'),
|
||||
fs.readFile(opts.key, 'utf-8'),
|
||||
]);
|
||||
return _.merge(connectOpts, {
|
||||
ca,
|
||||
cert,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
return connectOpts;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* ca?: string; // path to ca (Certificate Authority) file (TLS)
|
||||
* cert?: string; // path to cert (Certificate) file (TLS)
|
||||
* key?: string; // path to key file (TLS)
|
||||
* docker?: string; // dockerode DockerOptions.socketPath
|
||||
* dockerHost?: string; // dockerode DockerOptions.host
|
||||
* dockerPort?: number; // dockerode DockerOptions.port
|
||||
* host?: string;
|
||||
* port?: number;
|
||||
* timeout?: number;
|
||||
* }} options
|
||||
* @returns {Promise<import('docker-toolbelt')>}
|
||||
*/
|
||||
export async function getDocker(options) {
|
||||
const connectOpts = await generateConnectOpts(options);
|
||||
const client = createClient(connectOpts);
|
||||
await ensureDockerSeemsAccessible(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
const getDockerToolbelt = _.once(function () {
|
||||
const Docker = require('docker-toolbelt');
|
||||
const Bluebird = require('bluebird');
|
||||
Bluebird.promisifyAll(Docker.prototype, {
|
||||
filter(name) {
|
||||
return name === 'run';
|
||||
},
|
||||
multiArgs: true,
|
||||
});
|
||||
Bluebird.promisifyAll(Docker.prototype);
|
||||
// @ts-ignore `getImage()` should have a param but this whole thing is a hack that should be removed
|
||||
Bluebird.promisifyAll(new Docker({}).getImage().constructor.prototype);
|
||||
// @ts-ignore `getContainer()` should have a param but this whole thing is a hack that should be removed
|
||||
Bluebird.promisifyAll(new Docker({}).getContainer().constructor.prototype);
|
||||
return Docker;
|
||||
});
|
||||
|
||||
// docker-toolbelt v3 is not backwards compatible as it removes all *Async
|
||||
// methods that are in wide use in the CLI. The workaround for now is to
|
||||
// manually promisify the client and replace all `new Docker()` calls with
|
||||
// this shared function that returns a promisified client.
|
||||
//
|
||||
// **New code must not use the *Async methods.**
|
||||
//
|
||||
/**
|
||||
* @param {{
|
||||
* host: string;
|
||||
* port: number;
|
||||
* timeout?: number;
|
||||
* socketPath?: string
|
||||
* }} opts
|
||||
* @returns {import('docker-toolbelt')}
|
||||
*/
|
||||
export const createClient = function (opts) {
|
||||
const Docker = getDockerToolbelt();
|
||||
const docker = new Docker(opts);
|
||||
const { modem } = docker;
|
||||
// Workaround for a docker-modem 2.0.x bug where it sets a default
|
||||
// socketPath on Windows even if the input options specify a host/port.
|
||||
if (modem.socketPath && modem.host) {
|
||||
if (opts.socketPath) {
|
||||
modem.host = undefined;
|
||||
modem.port = undefined;
|
||||
} else if (opts.host) {
|
||||
modem.socketPath = undefined;
|
||||
}
|
||||
}
|
||||
return docker;
|
||||
};
|
||||
|
||||
var ensureDockerSeemsAccessible = function (docker) {
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
return docker
|
||||
.ping()
|
||||
.catch((e) =>
|
||||
exitWithExpectedError(
|
||||
`Docker seems to be unavailable. Is it installed and running?\n${e}`,
|
||||
),
|
||||
);
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2018-2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as dockerode from 'dockerode';
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import { parseAsInteger } from './validation';
|
||||
|
||||
export * from './docker-js';
|
||||
|
||||
interface BalenaEngineVersion extends dockerode.DockerVersion {
|
||||
Engine?: string;
|
||||
}
|
||||
|
||||
export interface DockerConnectionCliFlags {
|
||||
docker?: string;
|
||||
dockerHost?: string;
|
||||
dockerPort?: number;
|
||||
ca?: string;
|
||||
cert?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface DockerCliFlags extends DockerConnectionCliFlags {
|
||||
tag?: string;
|
||||
buildArg?: string[];
|
||||
'cache-from'?: string;
|
||||
nocache: boolean;
|
||||
pull?: boolean;
|
||||
squash: boolean;
|
||||
}
|
||||
|
||||
export const dockerConnectionCliFlags: flags.Input<DockerConnectionCliFlags> = {
|
||||
docker: flags.string({
|
||||
description: 'Path to a local docker socket (e.g. /var/run/docker.sock)',
|
||||
char: 'P',
|
||||
}),
|
||||
dockerHost: flags.string({
|
||||
description:
|
||||
'Docker daemon hostname or IP address (dev machine or balena device) ',
|
||||
char: 'h',
|
||||
}),
|
||||
dockerPort: flags.integer({
|
||||
description:
|
||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||
char: 'p',
|
||||
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||
}),
|
||||
ca: flags.string({
|
||||
description: 'Docker host TLS certificate authority file',
|
||||
}),
|
||||
cert: flags.string({
|
||||
description: 'Docker host TLS certificate file',
|
||||
}),
|
||||
key: flags.string({
|
||||
description: 'Docker host TLS key file',
|
||||
}),
|
||||
};
|
||||
|
||||
export const dockerCliFlags: flags.Input<DockerCliFlags> = {
|
||||
tag: flags.string({
|
||||
description: 'The alias to the generated image',
|
||||
char: 't',
|
||||
}),
|
||||
buildArg: flags.string({
|
||||
description:
|
||||
'Set a build-time variable (eg. "-B \'ARG=value\'"). Can be specified multiple times.',
|
||||
char: 'B',
|
||||
multiple: true,
|
||||
}),
|
||||
'cache-from': flags.string({
|
||||
description: `\
|
||||
Comma-separated list (no spaces) of image names for build cache resolution. \
|
||||
Implements the same feature as the "docker build --cache-from" option.`,
|
||||
}),
|
||||
nocache: flags.boolean({
|
||||
description: "Don't use docker layer caching when building",
|
||||
}),
|
||||
pull: flags.boolean({
|
||||
description: 'Pull the base images again even if they exist locally',
|
||||
}),
|
||||
squash: flags.boolean({
|
||||
description: 'Squash newly built layers into a single new layer',
|
||||
}),
|
||||
...dockerConnectionCliFlags,
|
||||
};
|
||||
|
||||
export interface BuildOpts {
|
||||
buildargs?: Dictionary<string>;
|
||||
cachefrom?: string[];
|
||||
nocache?: boolean;
|
||||
pull?: boolean;
|
||||
registryconfig?: import('resin-multibuild').RegistrySecrets;
|
||||
squash?: boolean;
|
||||
t?: string;
|
||||
}
|
||||
|
||||
function parseBuildArgs(args: string[]): Dictionary<string> {
|
||||
if (!Array.isArray(args)) {
|
||||
args = [args];
|
||||
}
|
||||
const buildArgs: Dictionary<string> = {};
|
||||
args.forEach(function (arg) {
|
||||
// note: [^] matches any character, including line breaks
|
||||
const pair = /^([^\s]+?)=([^]*)$/.exec(arg);
|
||||
if (pair != null) {
|
||||
buildArgs[pair[1]] = pair[2] ?? '';
|
||||
} else {
|
||||
throw new ExpectedError(`Could not parse build argument: '${arg}'`);
|
||||
}
|
||||
});
|
||||
return buildArgs;
|
||||
}
|
||||
|
||||
export function generateBuildOpts(options: {
|
||||
buildArg?: string[];
|
||||
'cache-from'?: string;
|
||||
nocache: boolean;
|
||||
pull?: boolean;
|
||||
'registry-secrets'?: import('resin-multibuild').RegistrySecrets;
|
||||
squash: boolean;
|
||||
tag?: string;
|
||||
}): BuildOpts {
|
||||
const opts: BuildOpts = {};
|
||||
if (options.buildArg != null) {
|
||||
opts.buildargs = parseBuildArgs(options.buildArg);
|
||||
}
|
||||
if (options['cache-from']?.trim()) {
|
||||
opts.cachefrom = options['cache-from'].split(',').filter((i) => !!i.trim());
|
||||
}
|
||||
if (options.nocache != null) {
|
||||
opts.nocache = true;
|
||||
}
|
||||
if (options.pull != null) {
|
||||
opts.pull = true;
|
||||
}
|
||||
if (
|
||||
options['registry-secrets'] &&
|
||||
Object.keys(options['registry-secrets']).length
|
||||
) {
|
||||
opts.registryconfig = options['registry-secrets'];
|
||||
}
|
||||
if (options.squash != null) {
|
||||
opts.squash = true;
|
||||
}
|
||||
if (options.tag != null) {
|
||||
opts.t = options.tag;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||
// dockerVersion.Engine should equal 'balena-engine' for the current/latest
|
||||
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
|
||||
// https://github.com/balena-os/balena-engine/pull/32/files
|
||||
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
|
||||
return !!(
|
||||
dockerVersion.Engine && dockerVersion.Engine.match(/balena|balaena/)
|
||||
);
|
||||
}
|
@ -1,435 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import _ = require('lodash');
|
||||
|
||||
import {
|
||||
exitWithExpectedError,
|
||||
instanceOf,
|
||||
NotLoggedInError,
|
||||
ExpectedError,
|
||||
} from '../errors';
|
||||
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import validation = require('./validation');
|
||||
import { delay } from './helpers';
|
||||
import { isV13 } from './version';
|
||||
import type { Application, Device, Organization } from 'balena-sdk';
|
||||
import { getApplication } from './sdk';
|
||||
|
||||
export function authenticate(options: {}): Promise<void> {
|
||||
const balena = getBalenaSdk();
|
||||
return getCliForm()
|
||||
.run(
|
||||
[
|
||||
{
|
||||
message: 'Email:',
|
||||
name: 'email',
|
||||
type: 'input',
|
||||
validate: validation.validateEmail,
|
||||
},
|
||||
{
|
||||
message: 'Password:',
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
},
|
||||
],
|
||||
{ override: options },
|
||||
)
|
||||
.then(balena.auth.login)
|
||||
.then(balena.auth.twoFactor.isPassed)
|
||||
.then((isTwoFactorAuthPassed: boolean) => {
|
||||
if (isTwoFactorAuthPassed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getCliForm()
|
||||
.ask({
|
||||
message: 'Two factor auth challenge:',
|
||||
name: 'code',
|
||||
type: 'input',
|
||||
})
|
||||
.then(balena.auth.twoFactor.challenge)
|
||||
.catch((error: any) => {
|
||||
return balena.auth.logout().then(() => {
|
||||
if (
|
||||
error.name === 'BalenaRequestError' &&
|
||||
error.statusCode === 401
|
||||
) {
|
||||
throw new ExpectedError('Invalid two factor authentication code');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logged in, and throw `NotLoggedInError` if not.
|
||||
* Note: `NotLoggedInError` is an `ExpectedError`.
|
||||
*/
|
||||
export async function checkLoggedIn(): Promise<void> {
|
||||
const balena = getBalenaSdk();
|
||||
if (!(await balena.auth.isLoggedIn())) {
|
||||
throw new NotLoggedInError(stripIndent`
|
||||
Login required: use the “balena login” command to log in.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
export function askLoginType() {
|
||||
return getCliForm().ask<'web' | 'credentials' | 'token' | 'register'>({
|
||||
message: 'How would you like to login?',
|
||||
name: 'loginType',
|
||||
type: 'list',
|
||||
choices: [
|
||||
{
|
||||
name: 'Web authorization (recommended)',
|
||||
value: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Credentials',
|
||||
value: 'credentials',
|
||||
},
|
||||
{
|
||||
name: 'Authentication token',
|
||||
value: 'token',
|
||||
},
|
||||
{
|
||||
name: "I don't have a balena account!",
|
||||
value: 'register',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function selectDeviceType() {
|
||||
return getBalenaSdk()
|
||||
.models.config.getDeviceTypes()
|
||||
.then((deviceTypes) => {
|
||||
deviceTypes = _.sortBy(deviceTypes, 'name').filter(
|
||||
(dt) => dt.state !== 'DISCONTINUED',
|
||||
);
|
||||
return getCliForm().ask({
|
||||
message: 'Device Type',
|
||||
type: 'list',
|
||||
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display interactive confirmation prompt.
|
||||
* If the user declines, then either an error will be thrown,
|
||||
* or `exitWithExpectedError` will be called (if exitIfDeclined true).
|
||||
* @param yesOption - automatically confirm if true
|
||||
* @param message - message to display with prompt
|
||||
* @param yesMessage - message to display if automatically confirming
|
||||
* @param exitIfDeclined - exitWithExpectedError when decline if true
|
||||
*/
|
||||
export async function confirm(
|
||||
yesOption: boolean,
|
||||
message: string,
|
||||
yesMessage?: string,
|
||||
exitIfDeclined = false,
|
||||
) {
|
||||
if (yesOption) {
|
||||
if (yesMessage) {
|
||||
if (isV13()) {
|
||||
console.error(yesMessage);
|
||||
} else {
|
||||
console.log(yesMessage);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await getCliForm().ask<boolean>({
|
||||
message,
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
const err = new ExpectedError('Aborted');
|
||||
// TODO remove this deprecated function (exitWithExpectedError)
|
||||
if (exitIfDeclined) {
|
||||
exitWithExpectedError(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectApplication(
|
||||
filter?: (app: ApplicationWithDeviceType) => boolean,
|
||||
errorOnEmptySelection = false,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.application
|
||||
.hasAny()
|
||||
.then(async (hasAnyApplications) => {
|
||||
if (!hasAnyApplications) {
|
||||
throw new ExpectedError("You don't have any applications");
|
||||
}
|
||||
|
||||
const apps = (await balena.models.application.getAll({
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
})) as ApplicationWithDeviceType[];
|
||||
return apps.filter(filter || _.constant(true));
|
||||
})
|
||||
.then((applications) => {
|
||||
if (errorOnEmptySelection && applications.length === 0) {
|
||||
throw new ExpectedError('No suitable applications found for selection');
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: _.map(applications, (application) => ({
|
||||
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
|
||||
value: application,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function selectOrganization(organizations?: Organization[]) {
|
||||
// Use either provided orgs (if e.g. already loaded) or load from cloud
|
||||
organizations =
|
||||
organizations || (await getBalenaSdk().models.organization.getAll());
|
||||
return getCliForm().ask({
|
||||
message: 'Select an organization',
|
||||
type: 'list',
|
||||
choices: organizations.map((org) => ({
|
||||
name: `${org.name} (${org.handle})`,
|
||||
value: org.handle,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function awaitDevice(uuid: string) {
|
||||
const balena = getBalenaSdk();
|
||||
const deviceName = await balena.models.device.getName(uuid);
|
||||
const visuals = getVisuals();
|
||||
const spinner = new visuals.Spinner(
|
||||
`Waiting for ${deviceName} to come online`,
|
||||
);
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
const isOnline = await balena.models.device.isOnline(uuid);
|
||||
if (isOnline) {
|
||||
spinner.stop();
|
||||
console.info(`The device **${deviceName}** is online!`);
|
||||
return;
|
||||
} else {
|
||||
// Spinner implementation is smart enough to
|
||||
// not start again if it was already started
|
||||
spinner.start();
|
||||
|
||||
await delay(3000);
|
||||
await poll();
|
||||
}
|
||||
};
|
||||
|
||||
console.info(`Waiting for ${deviceName} to connect to balena...`);
|
||||
await poll();
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export async function awaitDeviceOsUpdate(
|
||||
uuid: string,
|
||||
targetOsVersion: string,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const deviceName = await balena.models.device.getName(uuid);
|
||||
const visuals = getVisuals();
|
||||
const progressBar = new visuals.Progress(
|
||||
`Updating the OS of ${deviceName} to v${targetOsVersion}`,
|
||||
);
|
||||
progressBar.update({ percentage: 0 });
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
const [
|
||||
osUpdateStatus,
|
||||
{ overall_progress: osUpdateProgress },
|
||||
] = await Promise.all([
|
||||
balena.models.device.getOsUpdateStatus(uuid),
|
||||
balena.models.device.get(uuid, { $select: 'overall_progress' }),
|
||||
]);
|
||||
if (osUpdateStatus.status === 'done') {
|
||||
console.info(
|
||||
`The device ${deviceName} has been updated to v${targetOsVersion} and will restart shortly!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (osUpdateStatus.error) {
|
||||
console.error(
|
||||
`Failed to complete Host OS update on device ${deviceName}!`,
|
||||
);
|
||||
exitWithExpectedError(osUpdateStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (osUpdateProgress !== null) {
|
||||
// Avoid resetting to 0% at end of process when device goes offline.
|
||||
progressBar.update({ percentage: osUpdateProgress });
|
||||
}
|
||||
|
||||
await delay(3000);
|
||||
await poll();
|
||||
};
|
||||
|
||||
await poll();
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export function inferOrSelectDevice(preferredUuid: string) {
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.device.getAll().then((devices) => {
|
||||
const onlineDevices = devices.filter((device) => device.is_online);
|
||||
if (_.isEmpty(onlineDevices)) {
|
||||
throw new ExpectedError("You don't have any devices online");
|
||||
}
|
||||
|
||||
const defaultUuid = _(onlineDevices).map('uuid').includes(preferredUuid)
|
||||
? preferredUuid
|
||||
: onlineDevices[0].uuid;
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select a device',
|
||||
type: 'list',
|
||||
default: defaultUuid,
|
||||
choices: _.map(onlineDevices, (device) => ({
|
||||
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
|
||||
0,
|
||||
7,
|
||||
)})`,
|
||||
value: device.uuid,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Given applicationOrDevice, which may be
|
||||
* - an application name
|
||||
* - an application slug
|
||||
* - an application id (integer)
|
||||
* - a device uuid
|
||||
* Either:
|
||||
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
|
||||
* - in case of application, return uuid of device user selects from list of online devices.
|
||||
*
|
||||
* TODO: Modify this when app IDs dropped.
|
||||
*/
|
||||
export async function getOnlineTargetDeviceUuid(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
applicationOrDevice: string,
|
||||
) {
|
||||
const logger = (await import('../utils/logger')).getLogger();
|
||||
|
||||
// If looks like UUID, probably device
|
||||
if (validation.validateUuid(applicationOrDevice)) {
|
||||
let device: Device;
|
||||
try {
|
||||
logger.logDebug(
|
||||
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
||||
);
|
||||
device = await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid', 'is_online'],
|
||||
});
|
||||
|
||||
if (!device.is_online) {
|
||||
throw new ExpectedError(
|
||||
`Device with UUID ${applicationOrDevice} is offline`,
|
||||
);
|
||||
}
|
||||
|
||||
return device.uuid;
|
||||
} catch (err) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(err, BalenaDeviceNotFound)) {
|
||||
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
|
||||
// Now try app
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not a device UUID, try app
|
||||
let app: Application;
|
||||
try {
|
||||
logger.logDebug(
|
||||
`Trying to fetch application by name/slug/ID: ${applicationOrDevice}`,
|
||||
);
|
||||
app = await getApplication(sdk, applicationOrDevice);
|
||||
} catch (err) {
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (instanceOf(err, BalenaApplicationNotFound)) {
|
||||
throw new ExpectedError(
|
||||
`Application or Device not found: ${applicationOrDevice}`,
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// App found, load its devices
|
||||
const devices = await sdk.models.device.getAllByApplication(app.id, {
|
||||
$select: ['device_name', 'uuid'],
|
||||
$filter: { is_online: true },
|
||||
});
|
||||
|
||||
// Throw if no devices online
|
||||
if (_.isEmpty(devices)) {
|
||||
throw new ExpectedError(
|
||||
`Application ${app.slug} found, but has no devices online.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ask user to select from online devices for application
|
||||
return getCliForm().ask({
|
||||
message: `Select a device on application ${app.slug}`,
|
||||
type: 'list',
|
||||
default: devices[0].uuid,
|
||||
choices: _.map(devices, (device) => ({
|
||||
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
|
||||
value: device.uuid,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectFromList<T>(
|
||||
message: string,
|
||||
choices: Array<T & { name: string }>,
|
||||
): Promise<T> {
|
||||
return getCliForm().ask<T>({
|
||||
message,
|
||||
type: 'list',
|
||||
choices: _.map(choices, (s) => ({
|
||||
name: s.name,
|
||||
value: s,
|
||||
})),
|
||||
});
|
||||
}
|
114
lib/utils/sdk.ts
114
lib/utils/sdk.ts
@ -1,114 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Application,
|
||||
BalenaSDK,
|
||||
Organization,
|
||||
PineOptions,
|
||||
} from 'balena-sdk';
|
||||
|
||||
/**
|
||||
* Wraps the sdk application.get method,
|
||||
* adding disambiguation in cases where the provided
|
||||
* identifier could be interpreted in multiple valid ways.
|
||||
*/
|
||||
export async function getApplication(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlugOrId: string | number,
|
||||
options?: PineOptions<Application>,
|
||||
): Promise<Application> {
|
||||
const { looksLikeInteger } = await import('./validation');
|
||||
if (looksLikeInteger(nameOrSlugOrId as string)) {
|
||||
try {
|
||||
// Test for existence of app with this numerical ID
|
||||
return await sdk.models.application.get(Number(nameOrSlugOrId), options);
|
||||
} catch (e) {
|
||||
const { instanceOf } = await import('../errors');
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
||||
throw e;
|
||||
}
|
||||
// App with this numerical ID not found, but there may be an app with this numerical name.
|
||||
}
|
||||
}
|
||||
return sdk.models.application.get(nameOrSlugOrId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an string representation of an application identifier,
|
||||
* which could be one of:
|
||||
* - name (including numeric names)
|
||||
* - slug
|
||||
* - numerical id
|
||||
* disambiguate and return a properly typed identifier.
|
||||
*
|
||||
* Attempts to minimise the number of API calls required.
|
||||
* TODO: Remove this once support for numeric App IDs is removed.
|
||||
*/
|
||||
export async function getTypedApplicationIdentifier(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlugOrId: string,
|
||||
) {
|
||||
const { looksLikeInteger } = await import('./validation');
|
||||
// If there's no possible ambiguity,
|
||||
// return the passed identifier unchanged
|
||||
if (!looksLikeInteger(nameOrSlugOrId)) {
|
||||
return nameOrSlugOrId;
|
||||
}
|
||||
|
||||
// Resolve ambiguity
|
||||
try {
|
||||
// Test for existence of app with this numerical ID,
|
||||
// and return typed id if found
|
||||
return (await sdk.models.application.get(Number(nameOrSlugOrId))).id;
|
||||
} catch (e) {
|
||||
const { instanceOf } = await import('../errors');
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// App with this numerical id not found
|
||||
// return the passed identifier unchanged
|
||||
return nameOrSlugOrId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the sdk organization.getAll method,
|
||||
* restricting to those orgs user is a member of
|
||||
*/
|
||||
export async function getOwnOrganizations(
|
||||
sdk: BalenaSDK,
|
||||
): Promise<Organization[]> {
|
||||
return await sdk.models.organization.getAll({
|
||||
$filter: {
|
||||
organization_membership: {
|
||||
$any: {
|
||||
$alias: 'orm',
|
||||
$expr: {
|
||||
orm: {
|
||||
user: await sdk.auth.getUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$orderby: 'name asc',
|
||||
});
|
||||
}
|
169
lib/utils/ssh.ts
169
lib/utils/ssh.ts
@ -1,169 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { spawn, StdioOptions } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
export class ExecError extends TypedError {
|
||||
public cmd: string;
|
||||
public exitCode: number;
|
||||
|
||||
constructor(cmd: string, exitCode: number) {
|
||||
super(`Command '${cmd}' failed with error: ${exitCode}`);
|
||||
this.cmd = cmd;
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
export async function exec(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const { which } = await import('./helpers');
|
||||
const program = await which('ssh');
|
||||
const args = [
|
||||
'-n',
|
||||
'-t',
|
||||
'-p',
|
||||
'22222',
|
||||
'-o',
|
||||
'LogLevel=ERROR',
|
||||
'-o',
|
||||
'StrictHostKeyChecking=no',
|
||||
'-o',
|
||||
'UserKnownHostsFile=/dev/null',
|
||||
`root@${deviceIp}`,
|
||||
cmd,
|
||||
];
|
||||
if (process.env.DEBUG) {
|
||||
const logger = (await import('./logger')).getLogger();
|
||||
logger.logDebug(`Executing [${program},${args}]`);
|
||||
}
|
||||
|
||||
// Note: stdin must be 'inherit' to workaround a bug in older versions of
|
||||
// the built-in Windows 10 ssh client that otherwise prints the following
|
||||
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
|
||||
// They fixed the bug in newer versions of the ssh client:
|
||||
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
|
||||
// but users whould have to manually download and install a new client.
|
||||
// Note that "ssh -n" does not solve the problem, but should in theory
|
||||
// prevent the ssh client from using the CLI process stdin, even if it
|
||||
// is connected with 'inherit'.
|
||||
const stdio: StdioOptions = [
|
||||
'inherit',
|
||||
stdout ? 'pipe' : 'inherit',
|
||||
'inherit',
|
||||
];
|
||||
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
|
||||
if (stdout) {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
});
|
||||
if (exitCode !== 0) {
|
||||
throw new ExecError(cmd, exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
export async function execBuffered(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
enc?: string,
|
||||
): Promise<string> {
|
||||
const through = await import('through2');
|
||||
const buffer: string[] = [];
|
||||
await exec(
|
||||
deviceIp,
|
||||
cmd,
|
||||
through(function (data, _enc, cb) {
|
||||
buffer.push(data.toString(enc));
|
||||
cb();
|
||||
}),
|
||||
);
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a device's balenaOS release by executing 'cat /etc/os-release'
|
||||
* over ssh to the given deviceIp address. The result is cached with
|
||||
* lodash's memoize.
|
||||
*/
|
||||
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
||||
execBuffered(deviceIp, 'cat /etc/os-release'),
|
||||
);
|
||||
|
||||
// TODO: consolidate the various forms of executing ssh child processes
|
||||
// in the CLI, like exec and spawn, starting with the files:
|
||||
// lib/actions/ssh.ts
|
||||
// lib/utils/ssh.ts
|
||||
// lib/utils/device/ssh.ts
|
||||
|
||||
/**
|
||||
* Obtain the full path for ssh using which, then spawn a child process.
|
||||
* - If the child process returns error code 0, return the function normally
|
||||
* (do not throw an error).
|
||||
* - If the child process returns a non-zero error code, set process.exitCode
|
||||
* to that error code, and throw ExpectedError with a warning message.
|
||||
* - If the child process is terminated by a process signal, set
|
||||
* process.exitCode = 1, and throw ExpectedError with a warning message.
|
||||
*/
|
||||
export async function spawnSshAndThrowOnError(
|
||||
args: string[],
|
||||
options?: import('child_process').SpawnOptions,
|
||||
) {
|
||||
const { whichSpawn } = await import('./helpers');
|
||||
const [exitCode, exitSignal] = await whichSpawn(
|
||||
'ssh',
|
||||
args,
|
||||
options,
|
||||
true, // returnExitCodeOrSignal
|
||||
);
|
||||
if (exitCode || exitSignal) {
|
||||
// ssh returns a wide range of exit codes, including return codes of
|
||||
// interactive shells. For example, if the user types CTRL-C on an
|
||||
// interactive shell and then `exit`, ssh returns error code 130.
|
||||
// Another example, typing "exit 1" on an interactive shell causes ssh
|
||||
// to return exit code 1. In these cases, print a short one-line warning
|
||||
// message, and exits the CLI process with the same error code.
|
||||
process.exitCode = exitCode;
|
||||
throw new ExpectedError(sshErrorMessage(exitSignal, exitCode));
|
||||
}
|
||||
}
|
||||
|
||||
function sshErrorMessage(exitSignal?: string, exitCode?: number) {
|
||||
const msg: string[] = [];
|
||||
if (exitSignal) {
|
||||
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
|
||||
} else {
|
||||
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
|
||||
switch (exitCode) {
|
||||
case 255:
|
||||
msg.push(`
|
||||
Are the SSH keys correctly configured in balenaCloud? See:
|
||||
https://www.balena.io/docs/learn/manage/ssh-access/#add-an-ssh-key-to-balenacloud`);
|
||||
msg.push('Are you accidentally using `sudo`?');
|
||||
}
|
||||
}
|
||||
return msg.join('\n');
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import isRoot = require('is-root');
|
||||
import * as UpdateNotifier from 'update-notifier';
|
||||
|
||||
import packageJSON = require('../../package.json');
|
||||
|
||||
// Check for an update once a day. 1 day granularity should be
|
||||
// enough, rather than every run.
|
||||
const balenaUpdateInterval = 1000 * 60 * 60 * 24 * 1;
|
||||
|
||||
let notifier: UpdateNotifier.UpdateNotifier;
|
||||
|
||||
export function notify() {
|
||||
if (!notifier) {
|
||||
// `update-notifier` creates files to make the next
|
||||
// running time ask for updated, however this can lead
|
||||
// to ugly EPERM issues if those files are created as root.
|
||||
if (isRoot()) {
|
||||
return;
|
||||
} else {
|
||||
notifier = UpdateNotifier({
|
||||
pkg: packageJSON,
|
||||
updateCheckInterval: balenaUpdateInterval,
|
||||
});
|
||||
}
|
||||
}
|
||||
const up = notifier.update;
|
||||
if (
|
||||
up &&
|
||||
(require('semver') as typeof import('semver')).lt(up.current, up.latest)
|
||||
) {
|
||||
notifier.notify({
|
||||
defer: false,
|
||||
message: `Update available ${up.current} → ${up.latest}\n
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`,
|
||||
});
|
||||
}
|
||||
}
|
30200
npm-shrinkwrap.json
generated
30200
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
298
package.json
298
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.36.1",
|
||||
"version": "19.0.3",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -14,62 +14,69 @@
|
||||
"bin/",
|
||||
"build/",
|
||||
"doc/",
|
||||
"lib/",
|
||||
"src/",
|
||||
"patches/",
|
||||
"!patches/**/**.dev.patch",
|
||||
"*.md",
|
||||
"npm-shrinkwrap.json",
|
||||
"oclif.manifest.json"
|
||||
],
|
||||
"bin": {
|
||||
"balena": "./bin/balena"
|
||||
"balena": "./bin/run.js"
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": [
|
||||
"build/**/*.js",
|
||||
"node_modules/balena-sdk/es2018/index.js",
|
||||
"node_modules/balena-sync/build/**/*.js",
|
||||
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
|
||||
"node_modules/resin-compose-parse/build/schemas/*.json"
|
||||
"node_modules/@balena/compose/dist/parse/schemas/*.json"
|
||||
],
|
||||
"assets": [
|
||||
"build/auth/pages/*.ejs",
|
||||
"node_modules/resin-discoverable-services/services/**/*",
|
||||
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
|
||||
"node_modules/balena-pine/**/*",
|
||||
"node_modules/pinejs-client-core/**/*",
|
||||
"node_modules/opn/xdg-open",
|
||||
"node_modules/open/xdg-open",
|
||||
"node_modules/windosu/*.bat",
|
||||
"node_modules/windosu/*.cmd",
|
||||
"node_modules/axios/**/*",
|
||||
"npm-shrinkwrap.json",
|
||||
"oclif.manifest.json"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node patches/apply-patches.js",
|
||||
"prebuild": "rimraf build/ build-bin/",
|
||||
"pretarball": "ts-node --transpile-only ../../automation/run.ts sign:binaries",
|
||||
"build": "npm run build:src && npm run catch-uncommitted",
|
||||
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc",
|
||||
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
|
||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
|
||||
"build:doc": "mkdirp doc/ && ts-node --transpile-only automation/capitanodoc/index.ts > doc/cli.markdown",
|
||||
"build:t": "npm run lint && npm run build:fast && npm run build:test",
|
||||
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
|
||||
"build:pages": "mkdirp ./build/auth/pages/&& inline-source --compress ./src/auth/pages/error.ejs ./build/auth/pages/error.ejs && inline-source --compress ./src/auth/pages/success.ejs ./build/auth/pages/success.ejs",
|
||||
"build:fast": "npm run build:pages && tsc && npx oclif manifest",
|
||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit",
|
||||
"build:doc": "ts-node --transpile-only automation/capitanodoc/index.ts > docs/balena-cli.md",
|
||||
"build:completion": "node completion/generate-completion.js",
|
||||
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
||||
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
|
||||
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
||||
"release": "ts-node --transpile-only automation/run.ts release",
|
||||
"pretest": "npm run build",
|
||||
"test": "npm run test:shrinkwrap && npm run test:source && npm run test:standalone",
|
||||
"test": "npm run test:shrinkwrap && npm run test:core",
|
||||
"test:core": "npm run test:source && npm run test:standalone",
|
||||
"test:shrinkwrap": "ts-node --transpile-only automation/run.ts test-shrinkwrap",
|
||||
"test:source": "cross-env BALENA_CLI_TEST_TYPE=source mocha",
|
||||
"test:standalone": "npm run build:standalone && npm run test:standalone:fast",
|
||||
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone mocha --config .mocharc-standalone.js",
|
||||
"test:fast": "npm run build:fast && npm run test:source",
|
||||
"test:fast-profile": "npm run test:fast -- -- --inspect-brk=0.0.0.0",
|
||||
"test:debug": "cross-env BALENA_CLI_TEST_TYPE=source mocha --inspect-brk=0.0.0.0",
|
||||
"test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"",
|
||||
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||
"ci": "npm run test && npm run catch-uncommitted",
|
||||
"watch": "gulp watch",
|
||||
"lint": "balena-lint -e ts -e js --typescript --fix automation/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js",
|
||||
"lint": "npm run lint-tsconfig && npm run lint-other",
|
||||
"lint-tsconfig": "balena-lint -e ts -e js -t tsconfig.dev.json --fix automation/ src/ tests/ typings/",
|
||||
"lint-other": "balena-lint -e ts -e js --fix bin/run.js bin/dev.js completion/ .mocharc.js .mocharc-standalone.js",
|
||||
"update": "ts-node --transpile-only ./automation/update-module.ts",
|
||||
"prepare": "echo {} > bin/.fast-boot.json",
|
||||
"prepare": "echo {} > bin/.fast-boot.json && husky",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
@ -81,202 +88,199 @@
|
||||
"resin.io",
|
||||
"git"
|
||||
],
|
||||
"author": "Juan Cruz Viotti <juan@balena.io>",
|
||||
"author": "Balena Inc. (https://balena.io/)",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.20.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "node automation/check-npm-version.js && node automation/check-doc.js"
|
||||
}
|
||||
"node": "^20.6.0"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "balena",
|
||||
"commands": "./build/commands",
|
||||
"helpClass": "./build/help",
|
||||
"topicSeparator": " ",
|
||||
"hooks": {
|
||||
"prerun": "./build/hooks/prerun/track",
|
||||
"command_not_found": "./build/hooks/command-not-found/suggest"
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"help"
|
||||
],
|
||||
"macos": {
|
||||
"identifier": "io.balena.cli",
|
||||
"sign": "Developer ID Installer: Rulemotion Ltd (66H43P8FRG)"
|
||||
},
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
]
|
||||
"sign": "\"Developer ID Installer: Balena Ltd (66H43P8FRG)\""
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^5.2.0",
|
||||
"@oclif/config": "^1.17.0",
|
||||
"@oclif/dev-cli": "^1.22.2",
|
||||
"@oclif/parser": "^3.8.5",
|
||||
"@octokit/plugin-throttling": "^3.3.0",
|
||||
"@octokit/rest": "^16.43.2",
|
||||
"@types/archiver": "^2.1.3",
|
||||
"@types/bluebird": "^3.5.32",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/chai": "^4.2.12",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/cli-truncate": "^1.1.0",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/dockerode": "^2.5.34",
|
||||
"@types/ejs": "^3.0.4",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/fs-extra": "^8.1.1",
|
||||
"@types/global-agent": "^2.1.0",
|
||||
"@types/global-tunnel-ng": "^2.1.0",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@balena/lint": "^8.0.0",
|
||||
"@electron/notarize": "^2.0.0",
|
||||
"@octokit/plugin-throttling": "^3.5.1",
|
||||
"@octokit/rest": "^18.6.7",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/bluebird": "^3.5.36",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/cli-truncate": "^2.0.0",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fast-levenshtein": "^0.0.4",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/global-agent": "^2.1.1",
|
||||
"@types/global-tunnel-ng": "^2.1.1",
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"@types/inquirer": "^7.3.3",
|
||||
"@types/intercept-stdout": "^0.1.0",
|
||||
"@types/is-root": "^2.1.2",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/klaw": "^3.0.1",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/mixpanel": "^2.14.2",
|
||||
"@types/mocha": "^8.0.4",
|
||||
"@types/mock-require": "^2.0.0",
|
||||
"@types/moment-duration-format": "^2.2.2",
|
||||
"@types/net-keepalive": "^0.4.1",
|
||||
"@types/nock": "^11.0.7",
|
||||
"@types/node": "^10.17.28",
|
||||
"@types/node-cleanup": "^2.1.1",
|
||||
"@types/parse-link-header": "^1.0.0",
|
||||
"@types/prettyjson": "0.0.29",
|
||||
"@types/progress-stream": "^2.0.0",
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/klaw": "^3.0.6",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mixpanel": "^2.14.3",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/mock-require": "^2.0.1",
|
||||
"@types/moment-duration-format": "^2.2.3",
|
||||
"@types/ndjson": "^2.0.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/prettyjson": "^0.0.33",
|
||||
"@types/progress-stream": "^2.0.2",
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/rewire": "^2.5.30",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/sinon": "^9.0.8",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/split": "^1.0.0",
|
||||
"@types/stream-to-promise": "2.2.0",
|
||||
"@types/tar-stream": "^2.1.0",
|
||||
"@types/stream-to-promise": "^2.2.1",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.2.0",
|
||||
"@types/which": "^1.3.2",
|
||||
"archiver": "^3.1.1",
|
||||
"catch-uncommitted": "^1.6.2",
|
||||
"chai": "^4.2.0",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"@types/which": "^2.0.1",
|
||||
"@types/window-size": "^1.1.1",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"archiver": "^7.0.1",
|
||||
"catch-uncommitted": "^2.0.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"diff": "^4.0.2",
|
||||
"diff": "^5.0.0",
|
||||
"ent": "^2.2.0",
|
||||
"filehound": "^1.17.4",
|
||||
"fs-extra": "^8.0.1",
|
||||
"gulp": "^4.0.1",
|
||||
"gulp-inline-source": "^2.1.0",
|
||||
"filehound": "^1.17.5",
|
||||
"fs-extra": "^11.2.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"husky": "^4.2.5",
|
||||
"husky": "^9.1.5",
|
||||
"inline-source-cli": "^2.0.0",
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^0.5.5",
|
||||
"mocha": "^8.2.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"klaw": "^4.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"mocha": "^10.6.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^12.0.3",
|
||||
"parse-link-header": "~1.0.1",
|
||||
"pkg": "^4.4.9",
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^5.0.0",
|
||||
"simple-git": "^1.132.0",
|
||||
"sinon": "^9.2.1",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "^4.0.2"
|
||||
"nock": "^13.2.1",
|
||||
"oclif": "^4.14.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"simple-git": "^3.14.1",
|
||||
"sinon": "^18.0.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^4.0.1",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@oclif/command": "^1.8.0",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@balena/release-bundle": "^0.5.2",
|
||||
"@oclif/core": "^4.0.8",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^5.25.0",
|
||||
"@types/fast-levenshtein": "0.0.1",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"@zeit/dockerignore": "0.0.3",
|
||||
"JSONStream": "^1.0.3",
|
||||
"balena-config-json": "^4.1.0",
|
||||
"balena-device-init": "^5.0.2",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-manager": "^7.0.3",
|
||||
"balena-preload": "^10.3.1",
|
||||
"balena-release": "^3.0.0",
|
||||
"balena-sdk": "^15.20.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^7.0.1",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^10.0.1",
|
||||
"balena-preload": "^15.0.6",
|
||||
"balena-sdk": "^19.7.3",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.5",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-sync": "^11.0.2",
|
||||
"balena-settings-client": "^5.0.2",
|
||||
"balena-settings-storage": "^8.1.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"body-parser": "^1.19.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"chokidar": "^3.5.2",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"color-hash": "^1.0.3",
|
||||
"columnify": "^1.5.2",
|
||||
"color-hash": "^1.1.1",
|
||||
"common-tags": "^1.7.2",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "^2.1.4",
|
||||
"docker-progress": "^4.0.1",
|
||||
"docker-qemu-transpose": "^1.1.1",
|
||||
"docker-toolbelt": "^3.3.8",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^3.1.3",
|
||||
"etcher-sdk": "^2.0.20",
|
||||
"event-stream": "3.3.4",
|
||||
"express": "^4.13.3",
|
||||
"docker-modem": "^5.0.3",
|
||||
"docker-progress": "^5.1.3",
|
||||
"dockerode": "^4.0.2",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "9.1.0",
|
||||
"express": "^4.17.2",
|
||||
"fast-boot2": "^1.1.0",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
"filenamify": "^4.3.0",
|
||||
"get-stdin": "^8.0.0",
|
||||
"global-agent": "^2.1.12",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^2.2.0",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
"got": "^11.8.3",
|
||||
"humanize": "0.0.9",
|
||||
"ignore": "^5.1.8",
|
||||
"inquirer": "^7.3.3",
|
||||
"is-elevated": "^3.0.0",
|
||||
"is-root": "^2.1.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"klaw": "^3.0.0",
|
||||
"livepush": "^3.5.0",
|
||||
"lodash": "^4.17.20",
|
||||
"minimatch": "^3.0.4",
|
||||
"mixpanel": "^0.10.3",
|
||||
"moment": "^2.27.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"JSONStream": "^1.0.3",
|
||||
"livepush": "^3.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"ndjson": "^2.0.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-unzip-2": "^0.2.8",
|
||||
"oclif": "^1.16.1",
|
||||
"open": "^7.1.0",
|
||||
"patch-package": "6.2.2",
|
||||
"prettyjson": "^1.1.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettyjson": "^1.2.5",
|
||||
"progress-stream": "^2.0.0",
|
||||
"reconfix": "^0.1.0",
|
||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^2.0.2",
|
||||
"resin-cli-visuals": "^1.7.0",
|
||||
"resin-compose-parse": "^2.1.2",
|
||||
"resin-doodles": "^0.1.1",
|
||||
"resin-image-fs": "^5.0.9",
|
||||
"resin-multibuild": "^4.7.2",
|
||||
"resin-cli-form": "^3.0.0",
|
||||
"resin-cli-visuals": "^2.0.0",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.2",
|
||||
"semver": "^7.3.5",
|
||||
"shell-escape": "^0.2.0",
|
||||
"split": "^1.0.1",
|
||||
"stream-to-promise": "^2.2.0",
|
||||
"string-width": "^4.2.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi-stream": "^1.0.0",
|
||||
"tar-stream": "^2.1.3",
|
||||
"tar-utils": "^2.1.1",
|
||||
"through2": "^2.0.3",
|
||||
"tmp": "^0.2.1",
|
||||
"typed-error": "^3.2.1",
|
||||
"umount": "^1.1.6",
|
||||
"update-notifier": "^4.1.0",
|
||||
"update-notifier": "^5.1.0",
|
||||
"which": "^2.0.2",
|
||||
"window-size": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"net-keepalive": "^1.3.6",
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"inline-source-cli": {
|
||||
"inline-source": "^8.0.3"
|
||||
}
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2024-09-05T12:34:09.871Z"
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
diff --git a/node_modules/@oclif/config/lib/config.js b/node_modules/@oclif/config/lib/config.js
|
||||
index aba1da9..830f800 100644
|
||||
--- a/node_modules/@oclif/config/lib/config.js
|
||||
+++ b/node_modules/@oclif/config/lib/config.js
|
||||
@@ -165,7 +165,9 @@ class Config {
|
||||
debug('runCommand %s %o', id, argv);
|
||||
const c = this.findCommand(id);
|
||||
if (!c) {
|
||||
- await this.runHook('command_not_found', { id });
|
||||
+ // argv added to command_not_found hook
|
||||
+ // We should try to upstream this change
|
||||
+ await this.runHook('command_not_found', { id, argv });
|
||||
throw new errors_1.CLIError(`command ${id} not found`);
|
||||
}
|
||||
const command = c.load();
|
79
patches/all/@oclif+core+4.0.18.patch
Normal file
79
patches/all/@oclif+core+4.0.18.patch
Normal file
@ -0,0 +1,79 @@
|
||||
diff --git a/node_modules/@oclif/core/lib/help/command.js b/node_modules/@oclif/core/lib/help/command.js
|
||||
index 90922c8..6b7f417 100644
|
||||
--- a/node_modules/@oclif/core/lib/help/command.js
|
||||
+++ b/node_modules/@oclif/core/lib/help/command.js
|
||||
@@ -58,7 +58,8 @@ class CommandHelp extends formatter_1.HelpFormatter {
|
||||
return;
|
||||
return args.map((a) => {
|
||||
// Add ellipsis to indicate that the argument takes multiple values if strict is false
|
||||
- const name = this.command.strict === false ? `${a.name.toUpperCase()}...` : a.name.toUpperCase();
|
||||
+ let name = this.command.strict === false ? `${a.name.toUpperCase()}...` : a.name.toUpperCase();
|
||||
+ name = a.required ? `<${name}>` : `[${name}]`;
|
||||
let description = a.description || '';
|
||||
if (a.default)
|
||||
description = `${(0, theme_1.colorize)(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}`;
|
||||
diff --git a/node_modules/@oclif/core/lib/help/index.js b/node_modules/@oclif/core/lib/help/index.js
|
||||
index 4a34b89..d7eb6ac 100644
|
||||
--- a/node_modules/@oclif/core/lib/help/index.js
|
||||
+++ b/node_modules/@oclif/core/lib/help/index.js
|
||||
@@ -172,11 +172,12 @@ class Help extends HelpBase {
|
||||
}
|
||||
this.log(this.formatCommand(command));
|
||||
this.log('');
|
||||
- if (subTopics.length > 0) {
|
||||
+ const SUPPRESS_SUBTOPICS = true;
|
||||
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||
this.log(this.formatTopics(subTopics));
|
||||
this.log('');
|
||||
}
|
||||
- if (subCommands.length > 0) {
|
||||
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||
const aliases = [];
|
||||
const uniqueSubCommands = subCommands.filter((p) => {
|
||||
aliases.push(...p.aliases);
|
||||
diff --git a/node_modules/@oclif/core/lib/parser/errors.js b/node_modules/@oclif/core/lib/parser/errors.js
|
||||
index 168da99..538a880 100644
|
||||
--- a/node_modules/@oclif/core/lib/parser/errors.js
|
||||
+++ b/node_modules/@oclif/core/lib/parser/errors.js
|
||||
@@ -15,7 +15,8 @@ class CLIParseError extends errors_1.CLIError {
|
||||
parse;
|
||||
showHelp = false;
|
||||
constructor(options) {
|
||||
- options.message += '\nSee more help with --help';
|
||||
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
|
||||
+ options.message += `\nSee more help with ${help}`;
|
||||
super(options.message, { exit: options.exit });
|
||||
this.parse = options.parse;
|
||||
}
|
||||
@@ -38,7 +39,8 @@ exports.InvalidArgsSpecError = InvalidArgsSpecError;
|
||||
class RequiredArgsError extends CLIParseError {
|
||||
args;
|
||||
constructor({ args, exit, flagsWithMultiple, parse, }) {
|
||||
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
|
||||
const namedArgs = args.filter((a) => a.name);
|
||||
if (namedArgs.length > 0) {
|
||||
const list = (0, list_1.default)(namedArgs.map((a) => {
|
||||
@@ -52,7 +54,7 @@ class RequiredArgsError extends CLIParseError {
|
||||
message += `\n\nNote: ${flags} allow${flagsWithMultiple.length === 1 ? 's' : ''} multiple values. Because of this you need to provide all arguments before providing ${flagsWithMultiple.length === 1 ? 'that flag' : 'those flags'}.`;
|
||||
message += '\nAlternatively, you can use "--" to signify the end of the flags and the beginning of arguments.';
|
||||
}
|
||||
- super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse });
|
||||
+ super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse, command });
|
||||
this.args = args;
|
||||
this.showHelp = true;
|
||||
}
|
||||
diff --git a/node_modules/@oclif/core/lib/ux/list.js b/node_modules/@oclif/core/lib/ux/list.js
|
||||
index 954954c..0e507c7 100644
|
||||
--- a/node_modules/@oclif/core/lib/ux/list.js
|
||||
+++ b/node_modules/@oclif/core/lib/ux/list.js
|
||||
@@ -22,7 +22,7 @@ function renderList(items) {
|
||||
}
|
||||
left = left.padEnd(maxLength);
|
||||
right = linewrap(maxLength + 2, right);
|
||||
- return `${left} ${right}`;
|
||||
+ return `${left} : ${right}`;
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
index cd771cd..4a66939 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
@@ -37,6 +37,7 @@ class PackMacos extends command_1.Command {
|
||||
if (process.env.OSX_KEYCHAIN)
|
||||
args.push('--keychain', process.env.OSX_KEYCHAIN);
|
||||
args.push(dist);
|
||||
+ console.log(`pkgbuild "${args.join('" "')}"`);
|
||||
await qq.x('pkgbuild', args);
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
index a9d4276..4ac508f 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const command_1 = require("@oclif/command");
|
||||
const qq = require("qqjs");
|
||||
const Tarballs = require("../../tarballs");
|
||||
+const { fixPath } = require("../../util");
|
||||
+
|
||||
class PackWin extends command_1.Command {
|
||||
async run() {
|
||||
await this.checkForNSIS();
|
||||
const { flags } = this.parse(PackWin);
|
||||
- const buildConfig = await Tarballs.buildConfig(flags.root);
|
||||
+ const targets = flags.targets !== undefined ? flags.targets.split(',') : undefined;
|
||||
+ const buildConfig = await Tarballs.buildConfig(flags.root, {targets});
|
||||
const { config } = buildConfig;
|
||||
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
|
||||
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
|
||||
@@ -17,7 +20,7 @@ class PackWin extends command_1.Command {
|
||||
await qq.write([installerBase, `bin/${config.bin}`], scripts.sh(config));
|
||||
await qq.write([installerBase, `${config.bin}.nsi`], scripts.nsis(config, arch));
|
||||
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
|
||||
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
|
||||
+ await qq.x(`makensis ${fixPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`)
|
||||
const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`);
|
||||
await qq.mv([installerBase, 'installer.exe'], o);
|
||||
this.log(`built ${o}`);
|
||||
@@ -40,6 +43,7 @@ class PackWin extends command_1.Command {
|
||||
PackWin.description = 'create windows installer from oclif CLI';
|
||||
PackWin.flags = {
|
||||
root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }),
|
||||
+ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}),
|
||||
};
|
||||
exports.default = PackWin;
|
||||
const scripts = {
|
||||
@@ -89,6 +93,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
|
||||
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||
|
||||
Section "${config.name} CLI \${VERSION}"
|
||||
+ ; First remove any old client files.
|
||||
+ ; (Remnants of old versions were causing CLI errors)
|
||||
+ ; Initially tried running the Uninstall.exe, but was
|
||||
+ ; unable to make script wait for completion (despite using _?)
|
||||
+ DetailPrint "Removing files from previous version."
|
||||
+ RMDir /r "$INSTDIR\\client"
|
||||
+
|
||||
SetOutPath $INSTDIR
|
||||
File /r bin
|
||||
File /r client
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
index 3e613e0..4059ff4 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
@@ -17,8 +17,11 @@ const pack = async (from, to) => {
|
||||
qq.cd(prevCwd);
|
||||
};
|
||||
async function build(c, options = {}) {
|
||||
- const { xz, config } = c;
|
||||
+ const { xz, config, tmp } = c;
|
||||
const prevCwd = qq.cwd();
|
||||
+
|
||||
+ console.log(`[patched @oclif/dev-cli] cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
|
||||
+
|
||||
const packCLI = async () => {
|
||||
const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root });
|
||||
return path.join(c.root, stdout.split('\n').pop());
|
||||
@@ -34,6 +37,44 @@ async function build(c, options = {}) {
|
||||
await qq.mv(f, '.');
|
||||
await qq.rm('package', tarball, 'bin/run.cmd');
|
||||
};
|
||||
+ const copyCLI = async() => {
|
||||
+ const ws = c.workspace();
|
||||
+ await qq.emptyDir(ws);
|
||||
+ qq.cd(ws);
|
||||
+ const sources = [
|
||||
+ 'CHANGELOG.md',
|
||||
+ 'INSTALL.md',
|
||||
+ 'LICENSE',
|
||||
+ 'README.md',
|
||||
+ 'TROUBLESHOOTING.md',
|
||||
+ 'automation/check-npm-version.js',
|
||||
+ 'bin',
|
||||
+ 'build',
|
||||
+ 'npm-shrinkwrap.json',
|
||||
+ 'package.json',
|
||||
+ 'patches',
|
||||
+ 'typings',
|
||||
+ 'oclif.manifest.json',
|
||||
+ ];
|
||||
+ for (const source of sources) {
|
||||
+ let destDir = ws;
|
||||
+ const dirname = path.dirname(source);
|
||||
+ if (dirname && dirname !== '.') {
|
||||
+ destDir = path.join(ws, dirname);
|
||||
+ qq.mkdirp(destDir);
|
||||
+ }
|
||||
+ console.log(`cp "${source}" -> "${ws}"`);
|
||||
+ await qq.cp(path.join(c.root, source), destDir);
|
||||
+ }
|
||||
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
|
||||
+ await qq.mv('bin/balena', 'bin/run');
|
||||
+ await qq.rm('bin/run.cmd');
|
||||
+ // The oclif installers are produced with `npm i --production`, while the
|
||||
+ // source `bin` folder may contain a `.fast-boot.json` produced with `npm i`.
|
||||
+ // This has previously led to issues preventing the CLI from starting, so
|
||||
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
||||
+ await qq.rm('bin/.fast-boot.json');
|
||||
+ }
|
||||
const updatePJSON = async () => {
|
||||
qq.cd(c.workspace());
|
||||
const pjson = await qq.readJSON('package.json');
|
||||
@@ -56,7 +97,13 @@ async function build(c, options = {}) {
|
||||
lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
|
||||
}
|
||||
await qq.cp(lockpath, '.');
|
||||
- await qq.x('npm install --production');
|
||||
+
|
||||
+ const npmVersion = await qq.x.stdout('npm', ['--version']);
|
||||
+ if (require('semver').lt(npmVersion, '6.9.0')) {
|
||||
+ await qq.x('npx npm@6.9.0 install --production');
|
||||
+ } else {
|
||||
+ await qq.x('npm install --production');
|
||||
+ }
|
||||
}
|
||||
};
|
||||
const buildTarget = async (target) => {
|
||||
@@ -71,7 +118,8 @@ async function build(c, options = {}) {
|
||||
output: path.join(workspace, 'bin', 'node'),
|
||||
platform: target.platform,
|
||||
arch: target.arch,
|
||||
- tmp: qq.join(config.root, 'tmp'),
|
||||
+ tmp,
|
||||
+ projectRootPath: c.root,
|
||||
});
|
||||
if (options.pack === false)
|
||||
return;
|
||||
@@ -124,7 +172,8 @@ async function build(c, options = {}) {
|
||||
await qq.writeJSON(c.dist(config.s3Key('manifest')), manifest);
|
||||
};
|
||||
log_1.log(`gathering workspace for ${config.bin} to ${c.workspace()}`);
|
||||
- await extractCLI(await packCLI());
|
||||
+ // await extractCLI(await packCLI());
|
||||
+ await copyCLI();
|
||||
await updatePJSON();
|
||||
await addDependencies();
|
||||
await bin_1.writeBinScripts({ config, baseWorkspace: c.workspace(), nodeVersion: c.nodeVersion });
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/config.js b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
index 320fc52..efe3f2f 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
@@ -10,7 +10,13 @@ function gitSha(cwd, options = {}) {
|
||||
}
|
||||
exports.gitSha = gitSha;
|
||||
async function Tmp(config) {
|
||||
- const tmp = path.join(config.root, 'tmp');
|
||||
+ let tmp;
|
||||
+ if (process.env.BUILD_TMP) {
|
||||
+ tmp = path.join(process.env.BUILD_TMP, 'oclif');
|
||||
+ } else {
|
||||
+ tmp = path.join(config.root, 'tmp');
|
||||
+ }
|
||||
+ console.log(`@oclif/dev-cli tmp="${tmp}"`);
|
||||
await qq.mkdirp(tmp);
|
||||
return tmp;
|
||||
}
|
||||
@@ -36,7 +42,7 @@ async function buildConfig(root, options = {}) {
|
||||
s3Config: updateConfig.s3,
|
||||
nodeVersion: updateConfig.node.version || process.versions.node,
|
||||
workspace(target) {
|
||||
- const base = qq.join(config.root, 'tmp');
|
||||
+ const base = tmp;
|
||||
if (target && target.platform)
|
||||
return qq.join(base, [target.platform, target.arch].join('-'), config.s3Key('baseDir', target));
|
||||
return qq.join(base, config.s3Key('baseDir', target));
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
index b3918c5..073515a 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
@@ -1,28 +1,58 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const errors_1 = require("@oclif/errors");
|
||||
+const { isMSYS2 } = require('qqjs');
|
||||
const path = require("path");
|
||||
const qq = require("qqjs");
|
||||
const log_1 = require("../log");
|
||||
-async function checkFor7Zip() {
|
||||
- try {
|
||||
- await qq.x('7z', { stdio: [0, null, 2] });
|
||||
+const { fixPath } = require("../util");
|
||||
+let try_install_7zip = true;
|
||||
+async function checkFor7Zip(projectRootPath) {
|
||||
+ let zPaths = [
|
||||
+ fixPath(path.join(projectRootPath, 'node_modules', '7zip', '7zip-lite', '7z.exe')),
|
||||
+ '7z',
|
||||
+ ];
|
||||
+ let foundPath = '';
|
||||
+ for (const zPath of zPaths) {
|
||||
+ try {
|
||||
+ console.log(`probing 7zip at "${zPath}"...`);
|
||||
+ await qq.x(zPath, { stdio: [0, null, 2] });
|
||||
+ foundPath = zPath;
|
||||
+ break;
|
||||
+ }
|
||||
+ catch (err) {}
|
||||
}
|
||||
- catch (err) {
|
||||
- if (err.code === 127)
|
||||
- errors_1.error('install 7-zip to package windows tarball');
|
||||
- else
|
||||
- throw err;
|
||||
+ if (foundPath) {
|
||||
+ console.log(`found 7zip at "${foundPath}"`);
|
||||
+ } else if (try_install_7zip) {
|
||||
+ try_install_7zip = false;
|
||||
+ console.log(`attempting "npm install 7zip"...`);
|
||||
+ qq.pushd(projectRootPath);
|
||||
+ try {
|
||||
+ await qq.x('npm', ['install', '--no-save', '7zip']);
|
||||
+ } catch (err) {
|
||||
+ errors_1.error('install 7-zip to package windows tarball', true);
|
||||
+ } finally {
|
||||
+ qq.popd();
|
||||
+ }
|
||||
+ return checkFor7Zip(projectRootPath);
|
||||
+ } else {
|
||||
+ errors_1.error('install 7-zip to package windows tarball', true);
|
||||
}
|
||||
+ return foundPath;
|
||||
}
|
||||
-async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
|
||||
+async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp, projectRootPath }) {
|
||||
+
|
||||
+ console.log(`fetchNodeBinary using tmp="${tmp}`);
|
||||
+
|
||||
if (arch === 'arm')
|
||||
arch = 'armv7l';
|
||||
let nodeBase = `node-v${nodeVersion}-${platform}-${arch}`;
|
||||
let tarball = path.join(tmp, 'node', `${nodeBase}.tar.xz`);
|
||||
let url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.tar.xz`;
|
||||
+ let zPath = '';
|
||||
if (platform === 'win32') {
|
||||
- await checkFor7Zip();
|
||||
+ zPath = await checkFor7Zip(projectRootPath);
|
||||
nodeBase = `node-v${nodeVersion}-win-${arch}`;
|
||||
tarball = path.join(tmp, 'node', `${nodeBase}.7z`);
|
||||
url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.7z`;
|
||||
@@ -40,7 +70,8 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
|
||||
const basedir = path.dirname(tarball);
|
||||
await qq.mkdirp(basedir);
|
||||
await qq.download(url, tarball);
|
||||
- await qq.x(`grep ${path.basename(tarball)} ${shasums} | shasum -a 256 -c -`, { cwd: basedir });
|
||||
+ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -';
|
||||
+ await qq.x(`grep ${path.basename(tarball)} ${fixPath(shasums)} | ${shaCmd}`, { cwd: basedir });
|
||||
};
|
||||
const extract = async () => {
|
||||
log_1.log(`extracting ${nodeBase}`);
|
||||
@@ -50,7 +81,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
|
||||
await qq.mkdirp(path.dirname(cache));
|
||||
if (platform === 'win32') {
|
||||
qq.pushd(nodeTmp);
|
||||
- await qq.x(`7z x -bd -y ${tarball} > /dev/null`);
|
||||
+ await qq.x(`"${zPath}" x -bd -y ${fixPath(tarball)} > /dev/null`);
|
||||
await qq.mv([nodeBase, 'node.exe'], cache);
|
||||
qq.popd();
|
||||
}
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js
|
||||
index 17368b4..9d3fcf9 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/util.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/util.js
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const _ = require("lodash");
|
||||
+const { isCygwin, isMinGW, isMSYS2 } = require('qqjs');
|
||||
function castArray(input) {
|
||||
if (input === undefined)
|
||||
return [];
|
||||
@@ -40,3 +41,17 @@ function sortBy(arr, fn) {
|
||||
}
|
||||
exports.sortBy = sortBy;
|
||||
exports.template = (context) => (t) => _.template(t || '')(context);
|
||||
+
|
||||
+function fixPath(badPath) {
|
||||
+ console.log(`fixPath MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
|
||||
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
|
||||
+ let fixed = badPath.replace(/\\/g, '/');
|
||||
+ if (isMSYS2 || isMinGW) {
|
||||
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/$1');
|
||||
+ } else if (isCygwin) {
|
||||
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
|
||||
+ }
|
||||
+ console.log(`[patched @oclif/dev-cli] fixPath before="${badPath}" after="${fixed}"`);
|
||||
+ return fixed;
|
||||
+}
|
||||
+exports.fixPath = fixPath;
|
@ -1,55 +0,0 @@
|
||||
diff --git a/node_modules/@oclif/parser/lib/errors.js b/node_modules/@oclif/parser/lib/errors.js
|
||||
index 0c93a81..95d06be 100644
|
||||
--- a/node_modules/@oclif/parser/lib/errors.js
|
||||
+++ b/node_modules/@oclif/parser/lib/errors.js
|
||||
@@ -13,7 +13,8 @@ const m = deps_1.default()
|
||||
.add('list', () => require('./list'));
|
||||
class CLIParseError extends errors_1.CLIError {
|
||||
constructor(options) {
|
||||
- options.message += '\nSee more help with --help';
|
||||
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
|
||||
+ options.message += `\nSee more help with ${help}`;
|
||||
super(options.message);
|
||||
this.parse = options.parse;
|
||||
}
|
||||
@@ -34,22 +35,24 @@ class InvalidArgsSpecError extends CLIParseError {
|
||||
exports.InvalidArgsSpecError = InvalidArgsSpecError;
|
||||
class RequiredArgsError extends CLIParseError {
|
||||
constructor({ args, parse }) {
|
||||
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
|
||||
const namedArgs = args.filter(a => a.name);
|
||||
if (namedArgs.length > 0) {
|
||||
const list = m.list.renderList(namedArgs.map(a => [a.name, a.description]));
|
||||
message += `:\n${list}`;
|
||||
}
|
||||
- super({ parse, message });
|
||||
+ super({ parse, message, command });
|
||||
this.args = args;
|
||||
}
|
||||
}
|
||||
exports.RequiredArgsError = RequiredArgsError;
|
||||
class RequiredFlagError extends CLIParseError {
|
||||
constructor({ flag, parse }) {
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
const usage = m.list.renderList(m.help.flagUsages([flag], { displayRequired: false }));
|
||||
const message = `Missing required flag:\n${usage}`;
|
||||
- super({ parse, message });
|
||||
+ super({ parse, message, command });
|
||||
this.flag = flag;
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/@oclif/parser/lib/list.js b/node_modules/@oclif/parser/lib/list.js
|
||||
index 3907cc0..b689ca1 100644
|
||||
--- a/node_modules/@oclif/parser/lib/list.js
|
||||
+++ b/node_modules/@oclif/parser/lib/list.js
|
||||
@@ -21,7 +21,7 @@ function renderList(items) {
|
||||
}
|
||||
left = left.padEnd(maxLength);
|
||||
right = linewrap(maxLength + 2, right);
|
||||
- return `${left} ${right}`;
|
||||
+ return `${left} : ${right}`;
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
diff --git a/node_modules/@oclif/plugin-help/lib/command.js b/node_modules/@oclif/plugin-help/lib/command.js
|
||||
index 1caa65b..e205faf 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/command.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/command.js
|
||||
@@ -87,7 +87,7 @@ class CommandHelp {
|
||||
if (args.filter(a => a.description).length === 0)
|
||||
return;
|
||||
const body = list_1.renderList(args.map(a => {
|
||||
- const name = a.name.toUpperCase();
|
||||
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
|
||||
let description = a.description || '';
|
||||
if (a.default)
|
||||
description = `[default: ${a.default}] ${description}`;
|
||||
@@ -130,9 +130,7 @@ class CommandHelp {
|
||||
if (!flag.helpValue && flag.options) {
|
||||
value = flag.options.join('|');
|
||||
}
|
||||
- if (!value.includes('|'))
|
||||
- value = underline(value);
|
||||
- left += `=${value}`;
|
||||
+ left += ` <${value}>`;
|
||||
}
|
||||
let right = flag.description || '';
|
||||
if (flag.type === 'option' && flag.default) {
|
||||
diff --git a/node_modules/@oclif/plugin-help/lib/index.js b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
index 02646b6..525e19f 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/index.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
@@ -95,11 +95,12 @@ class Help extends HelpBase {
|
||||
console.log(title + '\n');
|
||||
console.log(this.formatCommand(command));
|
||||
console.log('');
|
||||
- if (subTopics.length > 0) {
|
||||
+ const SUPPRESS_SUBTOPICS = true;
|
||||
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||
console.log(this.formatTopics(subTopics));
|
||||
console.log('');
|
||||
}
|
||||
- if (subCommands.length > 0) {
|
||||
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||
console.log(this.formatCommands(subCommands));
|
||||
console.log('');
|
||||
}
|
35
patches/all/oclif+4.14.0.dev.patch
Normal file
35
patches/all/oclif+4.14.0.dev.patch
Normal file
@ -0,0 +1,35 @@
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||
index ef7f90e..8264b7c 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||
${customization}
|
||||
|
||||
Section "${config.name} CLI \${VERSION}"
|
||||
+ ; First remove any old client files.
|
||||
+ ; (Remnants of old versions were causing CLI errors)
|
||||
+ ; Initially tried running the Uninstall.exe, but was
|
||||
+ ; unable to make script wait for completion (despite using _?)
|
||||
+ DetailPrint "Removing files from previous version."
|
||||
+ RMDir /r "$INSTDIR\\client"
|
||||
SetOutPath $INSTDIR
|
||||
File /r bin
|
||||
File /r client
|
||||
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||
index 14d5a6e..7b42a6f 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
|
||||
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
|
||||
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
|
||||
]);
|
||||
+
|
||||
+ // The oclif installers are a production installation, while the source
|
||||
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
|
||||
+ // This has previously led to issues preventing the CLI from starting, so
|
||||
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
||||
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
|
||||
+
|
||||
};
|
||||
const buildTarget = async (target, c, options) => {
|
||||
const workspace = c.workspace(target);
|
@ -1,15 +1,16 @@
|
||||
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
|
||||
index 3bf5373..e042b64 100644
|
||||
index 13147d0..ff161dd 100644
|
||||
--- a/node_modules/open/index.js
|
||||
+++ b/node_modules/open/index.js
|
||||
@@ -11,7 +11,9 @@ const pAccess = promisify(fs.access);
|
||||
const pExecFile = promisify(childProcess.execFile);
|
||||
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
|
||||
const pReadFile = promisify(fs.readFile);
|
||||
|
||||
// Path to included `xdg-open`.
|
||||
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
|
||||
+const localXdgOpenPath = process.pkg
|
||||
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
|
||||
+ : path.join(__dirname, 'xdg-open');
|
||||
+
|
||||
|
||||
// Convert a path from WSL format to Windows format:
|
||||
// `/mnt/c/Program Files/Example/MyApp.exe` → `C:\Program Files\Example\MyApp.exe`
|
||||
/**
|
||||
Get the mount point for fixed drives in WSL.
|
@ -1,15 +0,0 @@
|
||||
diff --git a/node_modules/opn/index.js b/node_modules/opn/index.js
|
||||
index 13dcb66..0f0c1df 100644
|
||||
--- a/node_modules/opn/index.js
|
||||
+++ b/node_modules/opn/index.js
|
||||
@@ -51,7 +51,9 @@ module.exports = function (target, opts) {
|
||||
if (opts.app) {
|
||||
cmd = opts.app;
|
||||
} else {
|
||||
- cmd = path.join(__dirname, 'xdg-open');
|
||||
+ cmd = process.pkg
|
||||
+ ? path.join(path.dirname(process.execPath), 'xdg-open-402')
|
||||
+ : path.join(__dirname, 'xdg-open');
|
||||
}
|
||||
|
||||
if (appArgs.length > 0) {
|
@ -1,37 +0,0 @@
|
||||
diff --git a/node_modules/pkg/lib-es5/packer.js b/node_modules/pkg/lib-es5/packer.js
|
||||
index 7295bb6..76805a3 100644
|
||||
--- a/node_modules/pkg/lib-es5/packer.js
|
||||
+++ b/node_modules/pkg/lib-es5/packer.js
|
||||
@@ -128,6 +128,7 @@ function _default({
|
||||
const newStat = Object.assign({}, value);
|
||||
newStat.isFileValue = value.isFile();
|
||||
newStat.isDirectoryValue = value.isDirectory();
|
||||
+ newStat.isSocketValue = value.isSocket();
|
||||
const buffer = Buffer.from(JSON.stringify(newStat));
|
||||
stripes.push({
|
||||
snap,
|
||||
diff --git a/node_modules/pkg/prelude/bootstrap.js b/node_modules/pkg/prelude/bootstrap.js
|
||||
index 0d19f1d..db69015 100644
|
||||
--- a/node_modules/pkg/prelude/bootstrap.js
|
||||
+++ b/node_modules/pkg/prelude/bootstrap.js
|
||||
@@ -925,8 +925,10 @@ function payloadFileSync (pointer) {
|
||||
|
||||
var isFileValue = s.isFileValue;
|
||||
var isDirectoryValue = s.isDirectoryValue;
|
||||
+ var isSocketValue = s.isSocketValue;
|
||||
delete s.isFileValue;
|
||||
delete s.isDirectoryValue;
|
||||
+ delete s.isSocketValue;
|
||||
|
||||
s.isFile = function () {
|
||||
return isFileValue;
|
||||
@@ -934,6 +936,9 @@ function payloadFileSync (pointer) {
|
||||
s.isDirectory = function () {
|
||||
return isDirectoryValue;
|
||||
};
|
||||
+ s.isSocket = function () {
|
||||
+ return isSocketValue;
|
||||
+ };
|
||||
s.isSymbolicLink = function () {
|
||||
return false;
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
diff --git a/node_modules/qqjs/node_modules/execa/index.js b/node_modules/qqjs/node_modules/execa/index.js
|
||||
index 06f3969..8bca191 100644
|
||||
--- a/node_modules/qqjs/node_modules/execa/index.js
|
||||
+++ b/node_modules/qqjs/node_modules/execa/index.js
|
||||
@@ -14,6 +14,17 @@ const stdio = require('./lib/stdio');
|
||||
|
||||
const TEN_MEGABYTES = 1000 * 1000 * 10;
|
||||
|
||||
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
|
||||
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
|
||||
+// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin
|
||||
+const isCygwin = process.env.OSTYPE === 'cygwin';
|
||||
+const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW');
|
||||
+const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS');
|
||||
+
|
||||
+console.log(`[patched execa] detected "${
|
||||
+ isCygwin ? 'Cygwin' : isMinGW ? 'MinGW' : isMSYS2 ? 'MSYS2' : 'standard'
|
||||
+}" environment (MSYSTEM="${process.env.MSYSTEM}")`);
|
||||
+
|
||||
function handleArgs(cmd, args, opts) {
|
||||
let parsed;
|
||||
|
||||
@@ -104,13 +115,22 @@ function handleShell(fn, cmd, opts) {
|
||||
|
||||
opts = Object.assign({}, opts);
|
||||
|
||||
- if (process.platform === 'win32') {
|
||||
+ if (isMSYS2 || isMinGW || isCygwin) {
|
||||
+ file = process.env.MSYSSHELLPATH ||
|
||||
+ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' :
|
||||
+ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' :
|
||||
+ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : file)));
|
||||
+ }
|
||||
+ else if (process.platform === 'win32') {
|
||||
opts.__winShell = true;
|
||||
file = process.env.comspec || 'cmd.exe';
|
||||
args = ['/s', '/c', `"${cmd}"`];
|
||||
opts.windowsVerbatimArguments = true;
|
||||
}
|
||||
|
||||
+ const argStr = (args && args.length) ? `["${args.join('", "')}"]` : args;
|
||||
+ console.log(`[patched execa] handleShell file="${file}" args=${argStr}`);
|
||||
+
|
||||
if (opts.shell) {
|
||||
file = opts.shell;
|
||||
delete opts.shell;
|
||||
@@ -199,6 +219,9 @@ module.exports = (cmd, args, opts) => {
|
||||
const maxBuffer = parsed.opts.maxBuffer;
|
||||
const joinedCmd = joinCmd(cmd, args);
|
||||
|
||||
+ const argStr = (args && args.length) ? `["${args.join('", "')}"]` : args;
|
||||
+ console.log(`[patched execa] parsed.cmd="${parsed.cmd}" parsed.args=${argStr}`);
|
||||
+
|
||||
let spawned;
|
||||
try {
|
||||
spawned = childProcess.spawn(parsed.cmd, parsed.args, parsed.opts);
|
||||
@@ -364,3 +387,7 @@ module.exports.sync = (cmd, args, opts) => {
|
||||
module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);
|
||||
|
||||
module.exports.spawn = util.deprecate(module.exports, 'execa.spawn() is deprecated. Use execa() instead.');
|
||||
+
|
||||
+module.exports.isCygwin = isCygwin;
|
||||
+module.exports.isMinGW = isMinGW;
|
||||
+module.exports.isMSYS2 = isMSYS2;
|
@ -1,16 +0,0 @@
|
||||
diff --git a/node_modules/qqjs/lib/exec.js b/node_modules/qqjs/lib/exec.js
|
||||
index d9293a9..442fea8 100644
|
||||
--- a/node_modules/qqjs/lib/exec.js
|
||||
+++ b/node_modules/qqjs/lib/exec.js
|
||||
@@ -5,6 +5,11 @@ const m = {
|
||||
m: {},
|
||||
get execa() { return this.m.execa = this.m.execa || require('execa'); },
|
||||
};
|
||||
+const { isCygwin, isMinGW, isMSYS2 } = require('execa');
|
||||
+exports.isCygwin = isCygwin;
|
||||
+exports.isMinGW = isMinGW;
|
||||
+exports.isMSYS2 = isMSYS2;
|
||||
+console.log(`qqjs exec.js MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
|
||||
/**
|
||||
* easy access to process.env
|
||||
*/
|
@ -24,7 +24,10 @@ const { promisify } = require('util');
|
||||
const execFileAsync = promisify(execFile);
|
||||
const patchesDir = 'patches';
|
||||
|
||||
/** Run the patch-package tool in a child process and wait for it to finish */
|
||||
/**
|
||||
* Run the patch-package tool in a child process and wait for it to finish
|
||||
* @param {string} patchDir
|
||||
*/
|
||||
async function patchPackage(patchDir) {
|
||||
// Equivalent to: `npx patch-package --patch-dir $patchDir`
|
||||
const result = await execFileAsync('node', [
|
||||
|
@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/patch-package/dist/applyPatches.js b/node_modules/patch-package/dist/applyPatches.js
|
||||
index 35a22f7..c43a3b8 100644
|
||||
--- a/node_modules/patch-package/dist/applyPatches.js
|
||||
+++ b/node_modules/patch-package/dist/applyPatches.js
|
||||
@@ -85,7 +85,7 @@ function applyPatchesForApp(_a) {
|
||||
})) {
|
||||
// yay patch was applied successfully
|
||||
// print warning if version mismatch
|
||||
- if (installedPackageVersion !== version) {
|
||||
+ if (installedPackageVersion !== version.replace(/\.dev$/, "")) {
|
||||
printVersionMismatchWarning({
|
||||
packageName: name,
|
||||
actualVersion: installedPackageVersion,
|
@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/node-gyp-build/index.js b/node_modules/node-gyp-build/index.js
|
||||
index b5096ed..7cd451a 100644
|
||||
--- a/node_modules/node-gyp-build/index.js
|
||||
+++ b/node_modules/node-gyp-build/index.js
|
||||
@@ -29,6 +29,9 @@ load.path = function (dir) {
|
||||
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
|
||||
index 61b398e..3cc3be8 100644
|
||||
--- a/node_modules/node-gyp-build/node-gyp-build.js
|
||||
+++ b/node_modules/node-gyp-build/node-gyp-build.js
|
||||
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
|
||||
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
|
||||
} catch (err) {}
|
||||
|
8
repo.yml
8
repo.yml
@ -10,5 +10,9 @@ upstream:
|
||||
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
||||
- repo: 'balena-preload'
|
||||
url: 'https://github.com/balena-io-modules/balena-preload'
|
||||
- repo: 'balena-sync'
|
||||
url: 'https://github.com/balena-io-modules/balena-sync'
|
||||
- repo: 'etcher-sdk'
|
||||
url: 'https://github.com/balena-io-modules/etcher-sdk/'
|
||||
- repo: '@balena/compose'
|
||||
url: 'https://github.com/balena-io-modules/balena-compose'
|
||||
- repo: 'docker-progress'
|
||||
url: 'https://github.com/balena-io-modules/docker-progress'
|
||||
|
@ -16,8 +16,15 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import type { AppOptions } from './preparser';
|
||||
import {
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
import { run as mainRun, settings } from '@oclif/core';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
@ -27,6 +34,7 @@ export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
@ -43,13 +51,8 @@ export const setupSentry = onceAsync(async () => {
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,11 +78,13 @@ export function setMaxListeners(maxListeners: number) {
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
}
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
await checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
@ -89,43 +94,77 @@ async function init() {
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
if (!process.env.BALENARC_OFFLINE_MODE) {
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(
|
||||
command: string[],
|
||||
options: import('./preparser').AppOptions,
|
||||
) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||
// checkForNewReleasesIfNeeded may query the npm registry
|
||||
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
let isEEXIT = false;
|
||||
try {
|
||||
if (options.development) {
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
settings.debug = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
// For posteriority: We can't use default oclif 'execute' as
|
||||
// We customize error handling and flushing
|
||||
await mainRun(command, options.loadOptions ?? options.dir);
|
||||
} catch (error) {
|
||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||
// for example the `balena help` command.
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
isEEXIT = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (shouldFlush) {
|
||||
const { flush } = await import('@oclif/core');
|
||||
await flush();
|
||||
}
|
||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
||||
// addition to calling it in the main `run()` function in this file.
|
||||
// If it is not called here as well, there is a process exit delay of
|
||||
// 1 second when the fast-boot2 cache is modified (1 second is the
|
||||
// default cache saving timeout). Try for example `balena help`.
|
||||
// I have found that, when oclif's `Error: EEXIT: 0` is caught in
|
||||
// the try/catch block above, execution does not get past the
|
||||
// Promise.all() call below, but I don't understand why.
|
||||
if (isEEXIT) {
|
||||
(await import('./fast-boot')).stop();
|
||||
}
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
|
||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
|
||||
export async function run(cliArgs = process.argv, options: AppOptions) {
|
||||
try {
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||
'./utils/bootstrap'
|
||||
);
|
||||
setOfflineModeEnvVars();
|
||||
normalizeEnvVars();
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
@ -136,8 +175,6 @@ export async function run(
|
||||
|
||||
await init();
|
||||
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
@ -146,6 +183,13 @@ export async function run(
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
try {
|
||||
(await import('./fast-boot')).stop();
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Stopping fast-boot: ${e}`);
|
||||
}
|
||||
}
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
@ -56,7 +56,7 @@ export async function login({ host = '127.0.0.1', port = 0 }) {
|
||||
|
||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||
const open = await import('open');
|
||||
open(loginUrl, { wait: false });
|
||||
await open(loginUrl, { wait: false });
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const token = await loginServer.awaitForToken();
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user