mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-03 17:29:07 +00:00
Compare commits
875 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
42d9cbb48d | ||
|
408efa91c1 | ||
|
a2209ffe56 | ||
|
3f27db811b | ||
|
839a3050fb | ||
|
c8ea9cfcdb | ||
|
776115ef5d | ||
|
f031ec1dea | ||
|
fe42438090 | ||
|
b616fbdd79 | ||
|
81edfbbae1 | ||
|
663e83c3b8 | ||
|
b650f8ff6d | ||
|
58234f17e1 | ||
|
77905f4a74 | ||
|
30076fabe6 | ||
|
28703bb5ae | ||
|
37b3c6abe9 | ||
|
b4e473e4d4 | ||
|
0d4e411777 | ||
|
7e6f2189e8 | ||
|
3903daf8a8 | ||
|
18bc0d61e7 | ||
|
7f2daeebb0 | ||
|
813e9cb82e | ||
|
3bcb3c1b2e | ||
|
20d76556c2 | ||
|
e829068725 | ||
|
650e896f70 | ||
|
a9042124ea | ||
|
d24d78dac7 | ||
|
42c50ef8ae | ||
|
ba4b9bd447 | ||
|
02c0ea5b59 | ||
|
bc3558dd8e | ||
|
aad62d1ccd | ||
|
ecc6f80164 | ||
|
c0fd1e3886 | ||
|
9d3120b144 | ||
|
ed0e03ddb2 | ||
|
8fe6d6c026 | ||
|
727033ae14 | ||
|
c19ce6a905 | ||
|
1a33029738 | ||
|
043bc48a1c | ||
|
a10156a441 | ||
|
4f665f43d2 | ||
|
9f097a96f5 | ||
|
64d1943804 | ||
|
666ce876e6 | ||
|
e01184080f | ||
|
93039b010d | ||
|
795259bf30 | ||
|
fa134d2d39 | ||
|
bef5221ed8 | ||
|
72d6db796c | ||
|
e848eb63ee | ||
|
6f0f7350cf | ||
|
07a88c700e | ||
|
9cae66bd92 | ||
|
cddea24cef | ||
|
b1c246c0b4 | ||
|
00b4d57a03 | ||
|
2cba82e914 | ||
|
1352c5c823 | ||
|
c86eb97010 | ||
|
53be743b9d | ||
|
d9f21b4c3f | ||
|
261ab398dd | ||
|
f28a9992e4 | ||
|
29e7827eb1 | ||
|
1d77cf3665 | ||
|
017c767f61 | ||
|
7d79c4e24b | ||
|
60bc5092e0 | ||
|
a33a794931 | ||
|
f0ede6fca2 | ||
|
dbe177e766 | ||
|
09f80730a8 | ||
|
327d28c103 | ||
|
56ab785a82 | ||
|
305d65d5ed | ||
|
c4d3686a34 | ||
|
ce06854b55 | ||
|
8db05cc8a7 | ||
|
7a22c987d2 | ||
|
45efbcdfe3 | ||
|
d6a9b78b3e | ||
|
e8ac3ea960 | ||
|
0ffa0f85a2 | ||
|
5e7479f60e | ||
|
07365c45f2 | ||
|
e5076434c6 | ||
|
5d687f5a55 | ||
|
e192767156 | ||
|
5a8d2fad5f | ||
|
45f482fad1 | ||
|
c0e7ae9c91 | ||
|
36077cacda | ||
|
82b9983450 | ||
|
703dbd01c9 | ||
|
602e63c8a9 | ||
|
2ab635f49a | ||
|
322736a145 | ||
|
c347b67b25 | ||
|
4022beeb56 | ||
|
ccf97cfc9f | ||
|
9c5fe14f2e | ||
|
38e29251e7 | ||
|
bfc7a14646 | ||
|
610db81fcb | ||
|
d1f7d6d07f | ||
|
694eb78aaa | ||
|
1caccafbcd | ||
|
61d4d1f1e7 | ||
|
a01c85bc15 | ||
|
5d7b7cfc6f | ||
|
92fd9e0883 | ||
|
24273b5ac0 | ||
|
6155509f4c | ||
|
735af9f6a9 | ||
|
d7c60e6dea | ||
|
bcb42c8a21 | ||
|
04f5e0fa2b | ||
|
8cb5804848 | ||
|
91c3fced49 | ||
|
99a94eafbb | ||
|
b4cff78588 | ||
|
8577bb6281 | ||
|
e0f081623b | ||
|
2887ab8200 | ||
|
aaf4625abb | ||
|
6f30dc0550 | ||
|
6565ef4392 | ||
|
60b46192a2 | ||
|
bb101de96e | ||
|
ca6344bf4c | ||
|
48596fa318 | ||
|
7fb28ddb21 | ||
|
6dd6f43d64 | ||
|
07a7bd76fe | ||
|
4b3fdcf99c | ||
|
d023d0af91 | ||
|
de1821d7ac | ||
|
12923c9b84 | ||
|
8be069dbdb | ||
|
9d3f9128a8 | ||
|
61ebf9e4fd | ||
|
84985022e5 | ||
|
c5d8f73263 | ||
|
5db0c71bb3 | ||
|
c7a06f7259 | ||
|
bc66febc50 | ||
|
bb80311700 | ||
|
3dee7bd6f6 | ||
|
3251f04287 | ||
|
35dce4579a | ||
|
71ef00534d | ||
|
b6f8be27ec | ||
|
13110cca45 | ||
|
d4b554da1b | ||
|
1275c11573 | ||
|
593233a99f | ||
|
ec92f21b70 | ||
|
5adc43bcbd | ||
|
1ee9a68288 | ||
|
56e5dafb20 | ||
|
f0e0c0d728 | ||
|
afd14794f5 | ||
|
37e08e4667 | ||
|
61af57acc9 | ||
|
19be0fec1a | ||
|
2b656c23b3 | ||
|
2bf7b81645 | ||
|
746b6fa439 | ||
|
f52e218290 | ||
|
8655b89313 | ||
|
09d52c504d | ||
|
e5cee648f2 | ||
|
98a6b431d9 | ||
|
f305d5d9a0 | ||
|
5cb5ac80a6 | ||
|
3eb3b3b584 | ||
|
8ee5ede34d | ||
|
d6d08cc7c9 | ||
|
76c63d4b20 | ||
|
bff5897047 | ||
|
22c9fd399e | ||
|
65222b5fc9 | ||
|
d554658eee | ||
|
8ded517dd9 | ||
|
b0d8b021d5 | ||
|
2eb5d7f6b3 | ||
|
f1924bba6b | ||
|
44082e22e4 | ||
|
2d15530f61 | ||
|
a7c612f7de | ||
|
147ce8067a | ||
|
5e7ac09dd0 | ||
|
4c9fc89a6b | ||
|
9b1eb57973 | ||
|
76c08b6c13 | ||
|
3f82f42652 | ||
|
69d820878a | ||
|
aaaee47e0c | ||
|
e7761a616b | ||
|
9559d5cba3 | ||
|
c1649dd828 | ||
|
8e9b992a59 | ||
|
b77f266bd7 | ||
|
3c9ac76982 | ||
|
e483d06d2b | ||
|
24d2d19d33 | ||
|
ba5bb7b12c | ||
|
9082e7b3f7 | ||
|
0716544042 | ||
|
e1858aa69d | ||
|
082cce332a | ||
|
218e0a1b6b | ||
|
4065c5775c | ||
|
fd966df1f0 | ||
|
5eba175bf1 | ||
|
bd1b71bf2f | ||
|
bd7ea3d21a | ||
|
31662d9175 | ||
|
417c75484b | ||
|
fcd77e97d9 | ||
|
1dd819ae61 | ||
|
388e02ce85 | ||
|
30446605e1 | ||
|
e853b15f12 | ||
|
96cf380f66 | ||
|
5d0d02a24d | ||
|
f5ca07a422 | ||
|
6b5c6e072b | ||
|
3615f8e525 | ||
|
ec6cbd120e | ||
|
65b278e40b | ||
|
8d1394a77d | ||
|
ca0a3ee147 | ||
|
019af9e703 | ||
|
f213940c84 | ||
|
5243803342 | ||
|
811e009ba9 | ||
|
45e0f21685 | ||
|
60f5f47930 | ||
|
cad5543863 | ||
|
a1e936bb3f | ||
|
49984c2366 | ||
|
2a0ab6abbb | ||
|
349bab7702 | ||
|
af2c04540f | ||
|
cdfabb8f92 | ||
|
5e058d5158 | ||
|
6a81ed2d70 | ||
|
d323c0742c | ||
|
9cdde4f6c2 | ||
|
ebe10360b3 | ||
|
440c5ad15b | ||
|
0699278220 | ||
|
0eb5c78e33 | ||
|
067232b5c4 | ||
|
5716ba29ad | ||
|
fc2234b0dd | ||
|
12cdb14638 | ||
|
b936c51941 | ||
|
6c23b06b4c | ||
|
87c52c55ed | ||
|
f792343180 | ||
|
680d592af2 | ||
|
f52e6bd8b4 | ||
|
0847daba1b | ||
|
057b37ae38 | ||
|
deb7de8951 | ||
|
55dbe42e84 | ||
|
3e8bc57fdb | ||
|
d206e7cd66 | ||
|
7092db8ee8 | ||
|
276d61cf6c | ||
|
77ccd9c39c | ||
|
9e140eff13 | ||
|
da95baa70c | ||
|
a3ec75c2c7 | ||
|
f6f6be8ee8 | ||
|
09e653692b | ||
|
3ac89b236a | ||
|
bd472f2380 | ||
|
b5dcf45c40 | ||
|
7e2b5abe60 | ||
|
7b66e0d216 | ||
|
877c5031a4 | ||
|
1245b1c99b | ||
|
8dbe1af551 | ||
|
aae303202b | ||
|
284784505d | ||
|
77b9514442 | ||
|
ff4afe3ab2 | ||
|
5ea246f016 | ||
|
127bd7ec72 | ||
|
fa35877137 | ||
|
a402dffbc5 | ||
|
c7441b06ac | ||
|
251d64eb88 | ||
|
ff9bb52a20 | ||
|
c799c3f10d | ||
|
89efe2a2c8 | ||
|
f6ff397969 | ||
|
aaf709a1d4 | ||
|
ca6eea4371 | ||
|
d39dc5a39a | ||
|
1699419788 | ||
|
c25591cb4a | ||
|
a2b4f76c94 | ||
|
6a1239bd52 | ||
|
ddf34326a4 | ||
|
58f480ad7c | ||
|
7e6589a7d7 | ||
|
c699bb1dbc | ||
|
e101e0f466 | ||
|
e29273142e | ||
|
519395cfcd | ||
|
314e8800d0 | ||
|
0bb1c892e8 | ||
|
5eb79f5cf0 | ||
|
707b249e97 | ||
|
2a725cd1f0 | ||
|
83f274cc62 | ||
|
9242a3493a | ||
|
aa46d314b4 | ||
|
58f7dfc894 | ||
|
39e1c02648 | ||
|
5f92bbc846 | ||
|
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 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -4,6 +4,10 @@
|
||||
*.* -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
|
||||
docs/balena-cli.md text eol=lf
|
||||
|
38
.github/actions/always/action.yml
vendored
38
.github/actions/always/action.yml
vendored
@ -1,38 +0,0 @@
|
||||
---
|
||||
name: cleanup
|
||||
# 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
|
||||
|
||||
# --- custom environment
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# delete draft releases if the pull request is closed without merging
|
||||
- name: Delete draft release
|
||||
if: |
|
||||
runner.os == 'Linux' &&
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == false &&
|
||||
github.event.action == 'closed'
|
||||
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
58
.github/actions/finalize/action.yml
vendored
58
.github/actions/finalize/action.yml
vendored
@ -1,58 +0,0 @@
|
||||
---
|
||||
name: publish 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
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get release version
|
||||
if: runner.os == 'Linux'
|
||||
id: get_release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
# https://docs.github.com/en/rest/releases
|
||||
- name: Finalize GitHub release
|
||||
if: runner.os == 'Linux'
|
||||
# FIXME: Process completed with exit code 141.
|
||||
# https://stackoverflow.com/questions/54999974
|
||||
# https://stackoverflow.com/questions/22464786
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -a
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
previous_tag="$(git tag --sort=-version:refname | head -n 2 | tail -n 1)"
|
||||
release_notes="$(git log ${previous_tag}..HEAD --pretty=reference)"
|
||||
|
||||
gh release edit '${{ github.event.pull_request.head.ref }}' \
|
||||
--notes "${release_notes}" \
|
||||
--title 'v${{ steps.get_release.outputs.version }}' \
|
||||
--tag 'v${{ steps.get_release.outputs.version }}' \
|
||||
--prerelease=false \
|
||||
--draft=false
|
||||
|
||||
release_id="$(gh api "/repos/${{ github.repository }}/releases/tags/v${{ steps.get_release.outputs.version }}" \
|
||||
-H 'Accept: application/vnd.github+json' | jq -r .id)"
|
||||
|
||||
gh api --method PATCH "/repos/${{ github.repository }}/releases/${release_id}" \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-F make_latest="true"
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
100
.github/actions/publish/action.yml
vendored
100
.github/actions/publish/action.yml
vendored
@ -3,32 +3,34 @@ 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"
|
||||
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"
|
||||
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"
|
||||
default: 'accounts+apple@balena.io'
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
# FIXME: (please) https://github.com/balena-io/balena-cli/issues/2165
|
||||
default: "12.x"
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
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
|
||||
@ -37,11 +39,17 @@ runs:
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # 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
|
||||
@ -58,7 +66,7 @@ runs:
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
@ -67,23 +75,15 @@ runs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
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
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
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
|
||||
id: package_release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
@ -94,17 +94,26 @@ runs:
|
||||
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_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
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK='${{ runner.temp }}\certificate.pfx'
|
||||
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 }}
|
||||
|
||||
# patches/all/oclif.patch
|
||||
MSYSSHELLPATH="$(which bash)"
|
||||
MSYSTEM=MSYS
|
||||
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}"
|
||||
@ -114,40 +123,21 @@ runs:
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
|
||||
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
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://sectigo.com/resource-library/time-stamping-server
|
||||
TIMESTAMP_SERVER: http://timestamp.sectigo.com
|
||||
# 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 }}
|
||||
|
||||
# https://github.com/softprops/action-gh-release#-customizing
|
||||
- name: Create draft GitHub (pre)release
|
||||
uses: softprops/action-gh-release@v1
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
# use PR branch name for draft releases
|
||||
name: ${{ github.event.pull_request.head.ref }}
|
||||
tag_name: ${{ github.event.pull_request.head.ref }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
files: |
|
||||
dist/*.pkg
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
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
|
||||
|
37
.github/actions/test/action.yml
vendored
37
.github/actions/test/action.yml
vendored
@ -8,12 +8,14 @@ inputs:
|
||||
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
|
||||
# FIXME: (please) https://github.com/balena-io/balena-cli/issues/2165
|
||||
default: "12.x"
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
@ -22,26 +24,19 @@ runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Delete previous draft release
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # 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: |
|
||||
@ -49,22 +44,22 @@ runs:
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
if [[ -e package-lock.json ]]; then
|
||||
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
|
||||
npm run build
|
||||
npm run test
|
||||
npm run test:core
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
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"]
|
||||
}
|
41
.github/workflows/flowzone.yml
vendored
41
.github/workflows/flowzone.yml
vendored
@ -1,16 +1,45 @@
|
||||
name: Flowzone
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches:
|
||||
- "main"
|
||||
- "master"
|
||||
|
||||
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:
|
||||
tests_run_on: '["ubuntu-latest","macos-latest","windows-2019"]'
|
||||
custom_test_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-13"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
custom_publish_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-13"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
github_prerelease: false
|
||||
restrict_custom_actions: false
|
||||
|
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
|
File diff suppressed because it is too large
Load Diff
3348
CHANGELOG.md
3348
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -115,14 +115,27 @@ 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
|
||||
`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
|
||||
|
||||
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
|
||||
@ -210,7 +223,7 @@ 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 `lib/fast-boot.ts` to automatically invalidate the cache if
|
||||
`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:
|
||||
|
@ -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
|
||||
|
||||
@ -78,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
||||
> **Versions 13 and later are not yet fully supported.**
|
||||
> **The balena CLI currently requires Node.js version ^20.6.0**
|
||||
> **Versions 21 and later are not yet fully supported.**
|
||||
|
||||
### Install development tools
|
||||
|
||||
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
|
||||
$ 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 12
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
@ -106,15 +106,15 @@ recommended.
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 12
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
* 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:
|
||||
@ -145,7 +145,7 @@ container) in order to allow npm scripts like `postinstall` to be executed.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||
The `balena device ssh`, `device detect`, `build`, `deploy` and `preload` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
|
@ -33,7 +33,7 @@ as described above.
|
||||
|
||||
## sudo configuration
|
||||
|
||||
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
|
||||
A few CLI commands require execution through sudo, e.g. `sudo balena device detect`.
|
||||
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:
|
||||
@ -61,19 +61,19 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
### balena device ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
||||
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
|
||||
should do the trick on Debian or Ubuntu.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
### balena device detect
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like
|
||||
The `balena device detect` command requires a multicast DNS (mDNS) service like
|
||||
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
|
||||
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
|
||||
`sudo apt-get install avahi-daemon`.
|
||||
|
@ -19,7 +19,7 @@ Selected operating system: **macOS**
|
||||
- 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`
|
||||
No further steps are required to run most CLI commands. The `balena device ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
in the next section.
|
||||
|
||||
@ -27,7 +27,7 @@ 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/lib/balena-cli/bin/uninstall
|
||||
sudo /usr/local/src/balena-cli/bin/uninstall
|
||||
```
|
||||
|
||||
## Additional Dependencies
|
||||
@ -41,9 +41,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
### balena device ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. To check whether
|
||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. To check whether
|
||||
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
|
||||
include:
|
||||
|
||||
@ -52,7 +52,7 @@ include:
|
||||
Components → Command Line Tools → Install.
|
||||
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
|
@ -19,7 +19,7 @@ Selected operating system: **Windows**
|
||||
- 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`,
|
||||
No further steps are required to run most CLI commands. The `balena device ssh`, `device detect`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
described below.
|
||||
|
||||
@ -34,9 +34,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
### balena device ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
||||
distributing an SSH client with Windows 10, which is automatically installed through Windows
|
||||
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
|
||||
can also be [manually
|
||||
@ -44,13 +44,13 @@ installed](https://docs.microsoft.com/en-us/windows-server/administration/openss
|
||||
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
|
||||
parties.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
### balena device detect
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
||||
The `balena device detect` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
||||
Many Windows machines will already have this service installed, as it is bundled in popular
|
||||
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
||||
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999
|
||||
|
@ -88,9 +88,9 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
|
||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||
`BALENARC_PROXY`.
|
||||
|
||||
### Proxy setup for balena ssh
|
||||
### Proxy setup for balena device ssh
|
||||
|
||||
In order to work behind a proxy server, the `balena ssh` command requires the
|
||||
In order to work behind a proxy server, the `balena device ssh` command requires the
|
||||
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
|
||||
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
|
||||
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
|
||||
@ -110,7 +110,7 @@ The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations f
|
||||
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||
> Node.js version 10.16.0 or later.
|
||||
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||
> * To exclude a `balena device ssh` target from proxying (IP address or `.local` hostname), the
|
||||
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
||||
|
||||
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
||||
|
@ -31,7 +31,7 @@ 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`
|
||||
## I get `connect ETIMEDOUT` with `balena device 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
|
||||
@ -79,10 +79,10 @@ Try resetting the ownership by running:
|
||||
$ sudo chown -R <user> $HOME/.balena
|
||||
```
|
||||
|
||||
## Broken line wrapping / cursor behavior with `balena ssh`
|
||||
## Broken line wrapping / cursor behavior with `balena device ssh`
|
||||
|
||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example
|
||||
when long command lines are typed in a `balena ssh` session, or when using text editors like `vim`
|
||||
when long command lines are typed in a `balena device ssh` session, or when using text editors like `vim`
|
||||
or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue
|
||||
with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell
|
||||
configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`
|
||||
|
@ -15,21 +15,20 @@
|
||||
* 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';
|
||||
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 { Stats } from 'fs';
|
||||
import type { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as klaw from 'klaw';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import { promisify } from 'util';
|
||||
import { notarize } from '@electron/notarize';
|
||||
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
@ -41,6 +40,8 @@ import {
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const execAsync = promisify(exec);
|
||||
const rimrafAsync = promisify(rimraf);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
@ -60,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('win32', `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 = {
|
||||
@ -87,7 +92,7 @@ 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 = [
|
||||
@ -155,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();
|
||||
@ -180,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',
|
||||
@ -197,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(
|
||||
@ -292,7 +305,7 @@ async function zipPkg() {
|
||||
);
|
||||
}
|
||||
await fs.mkdirp(path.dirname(outputFile));
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||
|
||||
const archive = archiver('zip', {
|
||||
@ -313,7 +326,11 @@ async function zipPkg() {
|
||||
});
|
||||
}
|
||||
|
||||
async function signFilesForNotarization() {
|
||||
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/')
|
||||
@ -413,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],
|
||||
@ -427,18 +445,20 @@ async function renameInstallerFiles() {
|
||||
* 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];
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
// trust ...
|
||||
await execFileAsync('signtool.exe', [
|
||||
'sign',
|
||||
'-t',
|
||||
'-sha1',
|
||||
process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
|
||||
'-tr',
|
||||
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||
'-f',
|
||||
process.env.CSC_LINK,
|
||||
'-p',
|
||||
process.env.CSC_KEY_PASSWORD,
|
||||
'-td',
|
||||
'SHA256',
|
||||
'-fd',
|
||||
'SHA256',
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
exeName,
|
||||
@ -456,16 +476,15 @@ async function signWindowsInstaller() {
|
||||
* 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 appBundleId = packageJSON.oclif.macos.identifier || 'io.balena.cli';
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||
|
||||
if (appleIdPassword) {
|
||||
const { notarize } = await import('electron-notarize');
|
||||
// https://github.com/electron/notarize/blob/main/README.md
|
||||
if (appleIdPassword && teamId) {
|
||||
await notarize({
|
||||
appBundleId,
|
||||
tool: 'notarytool',
|
||||
teamId,
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
@ -484,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}`);
|
||||
@ -497,17 +517,14 @@ export async function buildOclifInstaller() {
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
console.log('Signing files for notarization...');
|
||||
await signFilesForNotarization();
|
||||
await rimrafAsync(dir);
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif "${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
|
||||
|
@ -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,173 +25,111 @@ 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.
|
||||
*
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Fleet',
|
||||
files: [
|
||||
'build/commands/fleets.js',
|
||||
'build/commands/fleet/index.js',
|
||||
'build/commands/fleet/create.js',
|
||||
'build/commands/fleet/purge.js',
|
||||
'build/commands/fleet/rename.js',
|
||||
'build/commands/fleet/restart.js',
|
||||
'build/commands/fleet/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/deactivate.js',
|
||||
'build/commands/device/identify.js',
|
||||
'build/commands/device/init.js',
|
||||
'build/commands/device/local-mode.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: 'Releases',
|
||||
files: [
|
||||
'build/commands/releases.js',
|
||||
'build/commands/release/index.js',
|
||||
'build/commands/release/finalize.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',
|
||||
login: 'Authentication',
|
||||
whoami: 'Authentication',
|
||||
logout: 'Authentication',
|
||||
env: 'Environment Variables',
|
||||
help: 'Help and Version',
|
||||
'ssh-key': 'SSH Keys',
|
||||
organization: 'Organizations',
|
||||
os: 'OS',
|
||||
util: 'Utilities',
|
||||
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.
|
||||
@ -206,7 +145,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
throw new Error(`Error parsing section title`);
|
||||
}
|
||||
// match[1] has the title, match[2] has the rest
|
||||
return match && match[2];
|
||||
return match?.[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||
|
4
automation/capitanodoc/doc-types.d.ts
vendored
4
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;
|
||||
|
||||
@ -26,7 +26,7 @@ export interface Document {
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: OclifCommand[];
|
||||
commands: Array<OclifCommand & { name: string }>;
|
||||
}
|
||||
|
||||
export { OclifCommand };
|
||||
|
@ -16,9 +16,8 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
@ -39,7 +38,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);
|
||||
}
|
||||
@ -47,49 +46,23 @@ export async function renderMarkdown(): Promise<string> {
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
// Help is now managed via a plugin
|
||||
// This fake command allows capitanodoc to include help in docs
|
||||
class FakeHelpCommand {
|
||||
description = stripIndent`
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
async function importOclifCommands(jsFilename: string) {
|
||||
const command = (await import(path.join(process.cwd(), jsFilename)))
|
||||
.default as OclifCommand;
|
||||
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
`;
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help login',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
args = [
|
||||
{
|
||||
name: 'command',
|
||||
description: 'command to show help for',
|
||||
},
|
||||
];
|
||||
|
||||
usage = 'help [command]';
|
||||
|
||||
flags = {
|
||||
verbose: {
|
||||
description: 'show additional commands',
|
||||
char: '-v',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): 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);
|
||||
|
||||
return [command];
|
||||
return {
|
||||
...command,
|
||||
// build/commands/device/index.js -> device
|
||||
// build/commands/device/list.js -> device list
|
||||
name: jsFilename
|
||||
.split('/')
|
||||
.slice(2)
|
||||
.join(' ')
|
||||
.split('.')
|
||||
.slice(0, 1)
|
||||
.join(' ')
|
||||
.split(' index')[0],
|
||||
} as Category['commands'][0];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,5 +78,5 @@ async function printMarkdown() {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
printMarkdown();
|
||||
|
@ -14,16 +14,31 @@
|
||||
* 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 { capitanoizeOclifUsage } from '../../src/utils/oclif-utils';
|
||||
import type { Category, Document } from './doc-types';
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage || '')}`];
|
||||
function renderOclifCommand(command: Category['commands'][0]): string[] {
|
||||
const result = [`## ${ent.encode(command.name || '')}`];
|
||||
if (command.aliases?.length) {
|
||||
result.push('### Aliases');
|
||||
result.push(
|
||||
command.aliases
|
||||
.map(
|
||||
(alias) =>
|
||||
`- \`${alias}\`${command.deprecateAliases ? ' *(deprecated)*' : ''}`,
|
||||
)
|
||||
.join('\n'),
|
||||
);
|
||||
result.push(
|
||||
`\nTo use one of the aliases, replace \`${command.name}\` with the alias.`,
|
||||
);
|
||||
}
|
||||
|
||||
result.push('### Description');
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
@ -37,8 +52,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 +64,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();
|
||||
@ -80,7 +95,7 @@ function renderToc(categories: Category[]): string[] {
|
||||
result.push(
|
||||
category.commands
|
||||
.map((command) => {
|
||||
const signature = capitanoizeOclifUsage(command.usage);
|
||||
const signature = capitanoizeOclifUsage(command.name);
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
@ -89,33 +104,7 @@ function renderToc(categories: Category[]): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
OS: [
|
||||
'os versions',
|
||||
'os download',
|
||||
'os build config',
|
||||
'os configure',
|
||||
'os initialize',
|
||||
],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: OclifCommand, x: string) =>
|
||||
(cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
|
@ -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,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line:import-blacklist
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
@ -43,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)));
|
||||
@ -82,5 +82,5 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
||||
|
@ -23,8 +23,8 @@ function parseSemver(version) {
|
||||
* @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;
|
||||
|
@ -1,257 +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 * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
|
||||
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) {
|
||||
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (_retryAfter: number, options: any) => {
|
||||
// does not retry, only logs a warning
|
||||
console.warn(
|
||||
`Abuse detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract pagination information (current page, total pages, ordinal number)
|
||||
* from the 'link' response header (example below), using the parse-link-header
|
||||
* npm package:
|
||||
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
||||
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
||||
*
|
||||
* @param response Octokit response object (including response.headers.link)
|
||||
* @param perPageDefault Default per_page pagination value if missing in URL
|
||||
* @return Object where 'page' is the current page number (1-based),
|
||||
* '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(
|
||||
response: any,
|
||||
perPageDefault: number,
|
||||
): { 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 parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||
}
|
||||
let perPage = perPageDefault;
|
||||
if (parsed.next) {
|
||||
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);
|
||||
} else {
|
||||
if (parsed.prev.per_page) {
|
||||
perPage = parseInt(parsed.prev.per_page, 10);
|
||||
}
|
||||
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
||||
}
|
||||
res.ordinal = (res.page - 1) * perPage + 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over every GitHub release in the given owner/repo, check whether
|
||||
* its tag_name matches against the affectedVersions semver spec, and if so
|
||||
* replace its release description (body) with the given newDescription value.
|
||||
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
||||
* @param repo GitHub repo, e.g. 'balena-cli'
|
||||
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
||||
* @param newDescription New release description (body)
|
||||
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
||||
* can be searched to determine whether that release has already been updated.
|
||||
*/
|
||||
async function updateGitHubReleaseDescriptions(
|
||||
owner: string,
|
||||
repo: string,
|
||||
affectedVersions: string,
|
||||
newDescription: string,
|
||||
editID: string,
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
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,
|
||||
} = getPageNumbers(response, perPage);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
if (!cliRelease.id) {
|
||||
console.error(
|
||||
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
||||
console.info(`${skipMsg}: already updated`);
|
||||
continue;
|
||||
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
||||
console.info(`${skipMsg}: outside version range`);
|
||||
continue;
|
||||
} else {
|
||||
const updatedRelease = {
|
||||
owner,
|
||||
repo,
|
||||
release_id: cliRelease.id,
|
||||
body: newDescription,
|
||||
};
|
||||
let oldBodyPreview = cliRelease.body;
|
||||
if (oldBodyPreview) {
|
||||
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
||||
if (oldBodyPreview.length > 12) {
|
||||
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning description to CLI releases affected by a mixpanel tracking
|
||||
* security issue (#1359). This function can be executed "manually" with the
|
||||
* following command line:
|
||||
*
|
||||
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
||||
*/
|
||||
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
||||
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
||||
// (It could have been any other platform, as long as it only runs once.)
|
||||
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
||||
return;
|
||||
}
|
||||
const owner = 'balena-io';
|
||||
const repo = 'balena-cli';
|
||||
const affectedVersions =
|
||||
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
||||
const editID = '[AA100]';
|
||||
let newDescription = `
|
||||
Please note: the "login" command in this release is affected by a
|
||||
security issue fixed in versions
|
||||
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
||||
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
||||
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
||||
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
||||
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
||||
and later. If you need to use this version, avoid passing your password,
|
||||
keys or tokens as command-line arguments. ${editID}`;
|
||||
// remove line breaks and collapse white space
|
||||
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
||||
await updateGitHubReleaseDescriptions(
|
||||
owner,
|
||||
repo,
|
||||
affectedVersions,
|
||||
newDescription,
|
||||
editID,
|
||||
);
|
||||
}
|
@ -21,12 +21,9 @@ import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
catchUncommitted,
|
||||
signFilesForNotarization,
|
||||
testShrinkwrap,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
@ -40,7 +37,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
* 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)
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
@ -54,32 +50,16 @@ async function parse(args?: string[]) {
|
||||
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)) {
|
||||
if (!Object.hasOwn(commands, arg)) {
|
||||
throw new Error(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const cmdFunc = commands[arg];
|
||||
@ -103,5 +83,5 @@ export async function run(args?: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
||||
|
@ -3,7 +3,7 @@ import * as semver from 'semver';
|
||||
|
||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||
|
||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
const validateChangeType = (maybeChangeType = 'minor') => {
|
||||
maybeChangeType = maybeChangeType.toLowerCase();
|
||||
switch (maybeChangeType) {
|
||||
case 'patch':
|
||||
@ -107,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');
|
||||
@ -136,5 +136,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
main();
|
||||
void main();
|
||||
|
@ -16,8 +16,10 @@
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { diffTrimmedLines } from 'diff';
|
||||
import * as whichMod from 'which';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
@ -65,7 +67,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
|
||||
@ -77,15 +78,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');
|
||||
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
return JSON.parse(packageJson);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,7 +102,6 @@ export function loadPackageJson() {
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
@ -129,7 +132,7 @@ export async function whichSpawn(
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err as Error);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
23
bin/balena
23
bin/balena
@ -1,23 +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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
run();
|
1
bin/balena
Symbolic link
1
bin/balena
Symbolic link
@ -0,0 +1 @@
|
||||
run.js
|
@ -1,89 +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';
|
||||
|
||||
// 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 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();
|
||||
});
|
||||
|
||||
// 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" %*
|
90
bin/dev.js
Executable file
90
bin/dev.js
Executable file
@ -0,0 +1,90 @@
|
||||
#!/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,
|
||||
});
|
||||
void 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 });
|
||||
}
|
||||
|
||||
void run();
|
@ -8,25 +8,28 @@ _balena() {
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
main_commands=( api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate )
|
||||
api_key_cmds=( generate list 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 track-fleet )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||
device_type_cmds=( list )
|
||||
device_cmds=( deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel )
|
||||
env_cmds=( list rename rm set )
|
||||
fleet_cmds=( create list pin purge rename restart rm track-latest )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
organization_cmds=( list )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize invalidate validate )
|
||||
tag_cmds=( rm set )
|
||||
release_cmds=( finalize invalidate list validate )
|
||||
ssh_key_cmds=( add list rm )
|
||||
tag_cmds=( list rm set )
|
||||
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'(- 1 *)--help[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
@ -43,15 +46,21 @@ _balena_sec_cmds() {
|
||||
"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-type")
|
||||
_describe -t device_type_cmds 'device-type_cmd' device_type_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
|
||||
;;
|
||||
@ -61,18 +70,21 @@ _balena_sec_cmds() {
|
||||
"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
|
||||
;;
|
||||
"organization")
|
||||
_describe -t organization_cmds 'organization_cmd' organization_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
|
||||
;;
|
||||
"ssh-key")
|
||||
_describe -t ssh_key_cmds 'ssh-key_cmd' ssh_key_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||
;;
|
||||
|
@ -7,20 +7,23 @@ _balena_complete()
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
main_commands="api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate"
|
||||
api_key_cmds="generate list 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 track-fleet"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||
device_type_cmds="list"
|
||||
device_cmds="deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel"
|
||||
env_cmds="list rename rm set"
|
||||
fleet_cmds="create list pin purge rename restart rm track-latest"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
organization_cmds="list"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize invalidate validate"
|
||||
tag_cmds="rm set"
|
||||
release_cmds="finalize invalidate list validate"
|
||||
ssh_key_cmds="add list rm"
|
||||
tag_cmds="list rm set"
|
||||
|
||||
|
||||
|
||||
@ -37,15 +40,21 @@ _balena_complete()
|
||||
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-type)
|
||||
COMPREPLY=( $(compgen -W "$device_type_cmds" -- $cur) )
|
||||
;;
|
||||
device)
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
devices)
|
||||
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
@ -55,18 +64,21 @@ _balena_complete()
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
key)
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
local)
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
organization)
|
||||
COMPREPLY=( $(compgen -W "$organization_cmds" -- $cur) )
|
||||
;;
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
ssh-key)
|
||||
COMPREPLY=( $(compgen -W "$ssh_key_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
@ -31,9 +31,9 @@ if (fs.existsSync(commandsFilePath)) {
|
||||
|
||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||
|
||||
var mainCommands = [];
|
||||
var additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands)) {
|
||||
const mainCommands = [];
|
||||
const additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands).sort()) {
|
||||
const cmd = key.split(':');
|
||||
if (cmd.length > 1) {
|
||||
additionalCommands.push(cmd);
|
||||
@ -72,8 +72,8 @@ fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
||||
/\$main_commands\$/g,
|
||||
'main_commands="' + mainCommandsStr + '"',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
@ -134,8 +134,8 @@ fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
||||
/\$main_commands\$/g,
|
||||
'main_commands=( ' + mainCommandsStr + ' )',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
|
@ -14,7 +14,7 @@ $sub_cmds$
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'(- 1 *)--help[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
|
5275
docs/balena-cli.md
5275
docs/balena-cli.md
File diff suppressed because it is too large
Load Diff
32
eslint.config.js
Normal file
32
eslint.config.js
Normal file
@ -0,0 +1,32 @@
|
||||
const { FlatCompat } = require('@eslint/eslintrc');
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
module.exports = [
|
||||
...require('@balena/lint/config/eslint.config'),
|
||||
...compat.config({
|
||||
parserOptions: {
|
||||
project: 'tsconfig.dev.json',
|
||||
},
|
||||
ignorePatterns: ['**/generate-completion.js', '**/bin/**/*'],
|
||||
rules: {
|
||||
ignoreDefinitionFiles: 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||
}],
|
||||
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
];
|
174
lib/command.ts
174
lib/command.ts
@ -1,174 +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 Command from '@oclif/command';
|
||||
import {
|
||||
InsufficientPrivilegesError,
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
* When set to true, command will be listed in `help`,
|
||||
* otherwise listed in `help --verbose` with secondary commands.
|
||||
*/
|
||||
public static primary = false;
|
||||
|
||||
/**
|
||||
* Require elevated privileges to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if executed without root on Mac/Linux
|
||||
* or if executed by non-Administrator on Windows.
|
||||
*/
|
||||
public static root = false;
|
||||
|
||||
/**
|
||||
* Require authentication to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is not already logged in.
|
||||
*/
|
||||
public static authenticated = false;
|
||||
|
||||
/**
|
||||
* Require an internet connection to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
|
||||
*/
|
||||
public static offlineCompatible = false;
|
||||
|
||||
/**
|
||||
* Accept piped input.
|
||||
* When set to true, command will read from stdin during init
|
||||
* and make contents available on member `stdin`.
|
||||
*/
|
||||
public static readStdin = false;
|
||||
|
||||
public stdin: string;
|
||||
|
||||
/**
|
||||
* Throw InsufficientPrivilegesError if not root on Mac/Linux
|
||||
* or non-Administrator on Windows.
|
||||
*
|
||||
* Called automatically if `root=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*/
|
||||
protected static async checkElevatedPrivileges() {
|
||||
const isElevated = await (await import('is-elevated'))();
|
||||
if (!isElevated) {
|
||||
throw new InsufficientPrivilegesError(
|
||||
'You need root/admin privileges to run this command',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotLoggedInError if not logged in.
|
||||
*
|
||||
* Called automatically if `authenticated=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotLoggedInError}
|
||||
*/
|
||||
public static async checkLoggedIn() {
|
||||
await (await import('./utils/patterns')).checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotLoggedInError if not logged in when condition true.
|
||||
*
|
||||
* @param {boolean} doCheck - will check if true.
|
||||
* @throws {NotLoggedInError}
|
||||
*/
|
||||
public static async checkLoggedInIf(doCheck: boolean) {
|
||||
if (doCheck) {
|
||||
await this.checkLoggedIn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotAvailableInOfflineModeError if in offline mode.
|
||||
*
|
||||
* Called automatically if `onlineOnly=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotAvailableInOfflineModeError}
|
||||
*/
|
||||
public static checkNotUsingOfflineMode() {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||
This command requires an internet connection, and cannot be used in offline mode.
|
||||
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read stdin contents and make available to command.
|
||||
*
|
||||
* This approach could be improved in the future to automatically set argument
|
||||
* values from stdin based in configuration, minimising command implementation.
|
||||
*/
|
||||
protected async getStdin() {
|
||||
this.stdin = await (await import('get-stdin'))();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a logger instance.
|
||||
*/
|
||||
protected static async getLogger() {
|
||||
return (await import('./utils/logger')).getLogger();
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const ctr = this.constructor as typeof BalenaCommand;
|
||||
|
||||
if (ctr.root) {
|
||||
await BalenaCommand.checkElevatedPrivileges();
|
||||
}
|
||||
|
||||
if (ctr.authenticated) {
|
||||
await BalenaCommand.checkLoggedIn();
|
||||
}
|
||||
|
||||
if (!ctr.offlineCompatible) {
|
||||
BalenaCommand.checkNotUsingOfflineMode();
|
||||
}
|
||||
|
||||
if (ctr.readStdin) {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
}
|
@ -1,85 +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';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
|
||||
Generate a new balenaCloud API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
`;
|
||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'api-key generate <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command cannot be run when logged in with an API key.
|
||||
Please login again with 'balena login' and select an alternative method.
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create a fleet.
|
||||
|
||||
Create a new balena fleet.
|
||||
|
||||
You can specify the organization the fleet 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 fleet'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 fleet create MyFleet',
|
||||
'$ balena fleet create MyFleet --organization mmyorg',
|
||||
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'fleet name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the fleet should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'fleet 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>(
|
||||
FleetCreateCmd,
|
||||
);
|
||||
|
||||
// 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: fleet "${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 fleets in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Output
|
||||
console.log(
|
||||
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
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,94 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count: number;
|
||||
online_devices: number;
|
||||
device_type?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class FleetsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all fleets.
|
||||
|
||||
List all your balena fleets.
|
||||
|
||||
For detailed information on a particular fleet, use
|
||||
\`balena fleet <fleet>\`
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena fleets'];
|
||||
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.dataSetOutputFlags,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications =
|
||||
(await balena.models.application.getAllDirectlyAccessible({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices =
|
||||
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,87 +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';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class ReleasesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all releases of a fleet.
|
||||
|
||||
List all releases of the given fleet.
|
||||
|
||||
${applicationNameNote.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = ['$ balena releases myorg/myfleet'];
|
||||
|
||||
public static usage = 'releases <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug (preferred)',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
|
||||
const releases = await balena.models.release.getAllByApplication(
|
||||
await getFleetSlug(balena, params.fleet),
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Copyright 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 { DataOutputOptions, DataSetOutputOptions } from './output';
|
||||
|
||||
export { DataOutputOptions, DataSetOutputOptions };
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
Copyright 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 { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
'no-truncate'?: boolean;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output message to STDERR
|
||||
*/
|
||||
export function outputMessage(msg: string) {
|
||||
// Messages go to STDERR
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output result data to STDOUT
|
||||
* Supports:
|
||||
* - arrays of items (displayed in a tabular way),
|
||||
* - single items (displayed in a field per row format).
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
export async function outputData(
|
||||
data: any[] | {},
|
||||
fields: string[],
|
||||
options: DataOutputOptions | DataSetOutputOptions,
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||
} else {
|
||||
await outputDataItem(data, fields, options as DataOutputOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the cli.ux table implementation, to output tabular data
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataSet(
|
||||
data: any[],
|
||||
fields: string[],
|
||||
options: DataSetOutputOptions,
|
||||
) {
|
||||
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||
// (e.g. as seen in json output).
|
||||
options.fields = options.fields?.replace(/_/g, ' ');
|
||||
options.filter = options.filter?.replace(/_/g, ' ');
|
||||
options.sort = options.sort?.replace(/_/g, ' ');
|
||||
|
||||
getCliUx().table(
|
||||
data,
|
||||
// Convert fields array to column object keys
|
||||
// that cli.ux expects. We can later add support
|
||||
// for both formats if beneficial
|
||||
fields.reduce((ac, a) => ({ ...ac, [a]: {} }), {}),
|
||||
{
|
||||
...options,
|
||||
...(options.json
|
||||
? {
|
||||
output: 'json',
|
||||
}
|
||||
: {}),
|
||||
columns: options.fields,
|
||||
printLine,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
|
||||
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions,
|
||||
) {
|
||||
const outData: typeof data = {};
|
||||
|
||||
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||
// or the format displayed in headers (e.g. `Some field`, case insensitive).
|
||||
const userSelectedFields = options.fields?.split(',').map((f) => {
|
||||
return f.toLowerCase().trim().replace(/ /g, '_');
|
||||
});
|
||||
|
||||
// Order and filter the fields based on `fields` parameter and `options.fields`
|
||||
(userSelectedFields || fields).forEach((fieldName) => {
|
||||
if (fields.includes(fieldName)) {
|
||||
outData[fieldName] = data[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
printLine(JSON.stringify(outData, undefined, 2));
|
||||
} else {
|
||||
const chalk = getChalk();
|
||||
const { capitalize } = await import('lodash');
|
||||
|
||||
// Find longest key, so we can align results
|
||||
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||
|
||||
// Output one field per line
|
||||
for (const [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Object.keys(o).length >= 1
|
||||
? Object.keys(o).reduce((a, b) => {
|
||||
return a.length > b.length ? a : b;
|
||||
}).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
// Duplicating oclif cli-ux's default implementation here,
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
@ -1,46 +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 type { Hook } from '@oclif/config';
|
||||
|
||||
let trackResolve: (result: Promise<any>) => void;
|
||||
|
||||
// note: trackPromise is subject to a Bluebird.timeout, defined in events.ts
|
||||
export const trackPromise = new Promise((resolve) => {
|
||||
trackResolve = resolve;
|
||||
});
|
||||
|
||||
/**
|
||||
* This is an oclif 'prerun' hook. This hook runs after the command line is
|
||||
* parsed by oclif, but before the command's run() function is called.
|
||||
* See: https://oclif.io/docs/hooks
|
||||
*
|
||||
* This hook is used to track CLI command signatures (usage analytics).
|
||||
* A command signature is something like "env add NAME [VALUE]". That's
|
||||
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
|
||||
*/
|
||||
const hook: Hook<'prerun'> = async function (options) {
|
||||
const events = await import('../../events');
|
||||
const usage: string | string[] | undefined = options.Command.usage;
|
||||
const cmdSignature =
|
||||
usage == null ? '*' : typeof usage === 'string' ? usage : usage.join(' ');
|
||||
|
||||
// Intentionally do not await for the track promise here, in order to
|
||||
// run the command tracking and the command itself in parallel.
|
||||
trackResolve(events.trackCommand(cmdSignature));
|
||||
};
|
||||
|
||||
export default hook;
|
@ -1,85 +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 { Main } from '@oclif/command';
|
||||
import type * as Config from '@oclif/config';
|
||||
|
||||
/**
|
||||
* This class is a partial copy-and-paste of
|
||||
* @oclif/plugin-help/command/CommandHelp, which is used to generate oclif's
|
||||
* command help output.
|
||||
*/
|
||||
export class CommandHelp {
|
||||
constructor(public command: { args?: any[] }) {}
|
||||
|
||||
protected arg(arg: Config.Command['args'][0]): string {
|
||||
const name = arg.name.toUpperCase();
|
||||
if (arg.required) {
|
||||
return `${name}`;
|
||||
}
|
||||
return `[${name}]`;
|
||||
}
|
||||
|
||||
public defaultUsage(): string {
|
||||
return CommandHelp.compact([
|
||||
// this.command.id,
|
||||
(this.command.args || [])
|
||||
.filter((a) => !a.hidden)
|
||||
.map((a) => this.arg(a))
|
||||
.join(' '),
|
||||
]).join(' ');
|
||||
}
|
||||
|
||||
public static compact<T>(array: Array<T | undefined>): T[] {
|
||||
return array.filter((a): a is T => !!a);
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert e.g. 'env add NAME [VALUE]' to 'env add <name> [value]' */
|
||||
export function capitanoizeOclifUsage(
|
||||
oclifUsage: string | string[] | undefined,
|
||||
): string {
|
||||
return (oclifUsage || '')
|
||||
.toString()
|
||||
.replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, (match) => `<${match}>`)
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export async function getCommandsFromManifest() {
|
||||
const manifest = require('../../oclif.manifest.json');
|
||||
|
||||
if (manifest.commands == null) {
|
||||
throw new Error('Commands section not found in manifest.');
|
||||
}
|
||||
return manifest.commands;
|
||||
}
|
||||
|
||||
export async function getCommandIdsFromManifest() {
|
||||
const commands = await getCommandsFromManifest();
|
||||
return Object.keys(commands);
|
||||
}
|
32258
npm-shrinkwrap.json
generated
32258
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
176
package.json
176
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "14.5.7",
|
||||
"version": "21.1.8",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -14,33 +14,32 @@
|
||||
"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/@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"
|
||||
]
|
||||
@ -48,34 +47,37 @@
|
||||
"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: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 ./lib/auth/pages/error.ejs ./build/auth/pages/error.ejs && inline-source --compress ./lib/auth/pages/success.ejs ./build/auth/pages/success.ejs",
|
||||
"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",
|
||||
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
|
||||
"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",
|
||||
"lint": "npm run lint-tsconfig && npm run lint-other",
|
||||
"lint-tsconfig": "balena-lint -e ts -e js -t tsconfig.dev.json --fix automation/ lib/ tests/ typings/",
|
||||
"lint-other": "balena-lint -e ts -e js --fix bin/balena bin/balena-dev completion/ .mocharc.js .mocharc-standalone.js",
|
||||
"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": [
|
||||
@ -90,150 +92,137 @@
|
||||
"author": "Balena Inc. (https://balena.io/)",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12 <16"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "node automation/check-npm-version.js && ts-node automation/check-doc.ts"
|
||||
}
|
||||
"node": "^20.6.0"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "balena",
|
||||
"commands": "./build/commands",
|
||||
"helpClass": "./build/help",
|
||||
"topicSeparator": " ",
|
||||
"hooks": {
|
||||
"prerun": "./build/hooks/prerun/track",
|
||||
"prerun": "./build/hooks/prerun",
|
||||
"command_not_found": "./build/hooks/command-not-found/suggest"
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"help"
|
||||
],
|
||||
"macos": {
|
||||
"identifier": "io.balena.cli",
|
||||
"sign": "Developer ID Installer: Balena Ltd (66H43P8FRG)"
|
||||
},
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
]
|
||||
"sign": "\"Developer ID Installer: Balena Ltd (66H43P8FRG)\""
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^6.2.0",
|
||||
"@oclif/config": "^1.18.2",
|
||||
"@oclif/parser": "^3.8.6",
|
||||
"@octokit/plugin-throttling": "^3.5.1",
|
||||
"@octokit/rest": "^18.6.7",
|
||||
"@types/archiver": "^5.1.1",
|
||||
"@balena/lint": "^9.1.3",
|
||||
"@electron/notarize": "^2.0.0",
|
||||
"@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/dockerode": "^3.3.9",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.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": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.6",
|
||||
"@types/klaw": "^3.0.3",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/klaw": "^3.0.6",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mixpanel": "^2.14.3",
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/mock-require": "^2.0.1",
|
||||
"@types/moment-duration-format": "^2.2.3",
|
||||
"@types/ndjson": "^2.0.1",
|
||||
"@types/net-keepalive": "^0.4.1",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^12.20.42",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/parse-link-header": "^1.0.1",
|
||||
"@types/prettyjson": "^0.0.30",
|
||||
"@types/prettyjson": "^0.0.33",
|
||||
"@types/progress-stream": "^2.0.2",
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/rewire": "^2.5.30",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/sinon": "^10.0.6",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/split": "^1.0.0",
|
||||
"@types/stream-to-promise": "^2.2.1",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"@types/which": "^2.0.1",
|
||||
"archiver": "^5.3.0",
|
||||
"@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.3",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"diff": "^5.0.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"ent": "^2.2.0",
|
||||
"filehound": "^1.17.5",
|
||||
"fs-extra": "^9.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"husky": "^4.3.8",
|
||||
"husky": "^9.1.5",
|
||||
"inline-source-cli": "^2.0.0",
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mocha": "^8.4.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"klaw": "^4.1.0",
|
||||
"mocha": "^10.6.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^13.2.1",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"pkg": "^5.5.1",
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^5.0.0",
|
||||
"oclif": "^4.17.0",
|
||||
"rewire": "^7.0.0",
|
||||
"simple-git": "^3.14.1",
|
||||
"sinon": "^11.1.2",
|
||||
"sinon": "^19.0.0",
|
||||
"string-to-stream": "^3.0.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^2.1.1",
|
||||
"@balena/compose": "^7.0.1",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/command": "^1.8.16",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@oclif/core": "^4.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@types/fast-levenshtein": "0.0.1",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"JSONStream": "^1.0.3",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^6.0.0",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^8.0.0",
|
||||
"balena-preload": "^12.1.0",
|
||||
"balena-sdk": "^16.28.0",
|
||||
"balena-config-json": "^4.2.2",
|
||||
"balena-device-init": "^8.1.3",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.5.0",
|
||||
"balena-preload": "^18.0.1",
|
||||
"balena-sdk": "^21.3.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.7",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-sync": "^11.0.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"balena-settings-client": "^5.0.2",
|
||||
"balena-settings-storage": "^8.1.0",
|
||||
"body-parser": "^1.19.1",
|
||||
"chalk": "^3.0.0",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.5.2",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"cli-ux": "^5.5.1",
|
||||
"color-hash": "^1.1.1",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "3.0.0",
|
||||
"docker-modem": "^5.0.6",
|
||||
"docker-progress": "^5.1.3",
|
||||
"dockerode": "^3.3.1",
|
||||
"dockerode": "^4.0.5",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "^6.2.1",
|
||||
"event-stream": "3.3.4",
|
||||
"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",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^2.2.0",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
@ -243,25 +232,23 @@
|
||||
"is-elevated": "^3.0.0",
|
||||
"is-root": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"klaw": "^3.0.0",
|
||||
"JSONStream": "^1.0.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"livepush": "^3.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^3.0.4",
|
||||
"moment": "^2.29.1",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"mime": "^2.4.6",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ndjson": "^2.0.0",
|
||||
"net-keepalive": "^3.0.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-unzip-2": "^0.2.8",
|
||||
"oclif": "^1.18.4",
|
||||
"open": "^7.1.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettyjson": "^1.2.5",
|
||||
"progress-stream": "^2.0.0",
|
||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^2.0.2",
|
||||
"resin-cli-visuals": "^1.8.0",
|
||||
"resin-cli-form": "^3.0.0",
|
||||
"resin-cli-visuals": "^2.0.1",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@ -283,7 +270,12 @@
|
||||
"optionalDependencies": {
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"inline-source-cli": {
|
||||
"inline-source": "^8.0.3"
|
||||
}
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2022-11-10T22:19:21.757Z"
|
||||
"publishedAt": "2025-04-03T16:10:03.330Z"
|
||||
}
|
||||
}
|
||||
|
@ -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.1.0.patch
Normal file
79
patches/all/@oclif+core+4.1.0.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 33105a0..0436982 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 0b48c0e..ff4fed4 100644
|
||||
--- a/node_modules/@oclif/core/lib/help/index.js
|
||||
+++ b/node_modules/@oclif/core/lib/help/index.js
|
||||
@@ -173,11 +173,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,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 b3b9010..788e5c6 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/command.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/command.js
|
||||
@@ -88,7 +88,7 @@ class CommandHelp {
|
||||
return;
|
||||
const body = list_1.renderList(args.map(a => {
|
||||
var _a;
|
||||
- const name = a.name.toUpperCase();
|
||||
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
|
||||
let description = a.description || '';
|
||||
// `a.default` is actually not always a string (typing bug), hence `toString()`
|
||||
if (a.default || ((_a = a.default) === null || _a === void 0 ? void 0 : _a.toString()) === '0')
|
||||
@@ -133,9 +133,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 || '';
|
||||
// `flag.default` is not always a string (typing bug), hence `toString()`
|
||||
diff --git a/node_modules/@oclif/plugin-help/lib/index.js b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
index 04d7861..c2fb591 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/index.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
@@ -98,11 +98,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('');
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/macos.js b/node_modules/oclif/lib/commands/pack/macos.js
|
||||
index 924f092..a69e60b 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/macos.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/macos.js
|
||||
@@ -133,6 +133,7 @@ class PackMacos extends command_1.Command {
|
||||
if (process.env.OSX_KEYCHAIN)
|
||||
args.push('--keychain', process.env.OSX_KEYCHAIN);
|
||||
args.push(dist);
|
||||
+ console.error(`[debug] oclif pkgbuild "${args.join('" "')}"`);
|
||||
await qq.x('pkgbuild', args);
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||
index bf4657e..fd58c7d 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||
@@ -52,6 +52,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
|
||||
@@ -61,6 +68,8 @@ Section "${config.name} CLI \${VERSION}"
|
||||
WriteUninstaller "$INSTDIR\\Uninstall.exe"
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
"DisplayName" "${config.name}"
|
||||
+ WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
+ "DisplayVersion" "\${VERSION}"
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
"UninstallString" "$\\"$INSTDIR\\uninstall.exe$\\""
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
@@ -193,7 +202,8 @@ 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 ? flags.targets.split(',') : undefined;
|
||||
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets: $targets });
|
||||
const { config, version, gitSha, targets, tmp } = buildConfig;
|
||||
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
|
||||
const arches = targets.filter(t => t.platform === 'win32').map(t => t.arch);
|
||||
@@ -208,7 +218,8 @@ class PackWin extends command_1.Command {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
|
||||
+ const { msysExec, toMsysPath } = require("../../util");
|
||||
+ await msysExec(`makensis ${toMsysPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
|
||||
const templateKey = upload_util_1.templateShortKey('win32', { bin: config.bin, version: version, sha: gitSha, arch });
|
||||
const o = buildConfig.dist(`win32/${templateKey}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -255,4 +266,5 @@ PackWin.hidden = true;
|
||||
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)'}),
|
||||
};
|
||||
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||
index d3e8e89..a5d29e2 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||
@@ -18,8 +18,9 @@ const pack = async (from, to) => {
|
||||
qq.cd(prevCwd);
|
||||
};
|
||||
async function build(c, options = {}) {
|
||||
- const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig } = c;
|
||||
+ const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig, tmp } = c;
|
||||
const prevCwd = qq.cwd();
|
||||
+ console.error(`[debug] oclif 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());
|
||||
@@ -30,11 +31,19 @@ async function build(c, options = {}) {
|
||||
tarball = path.basename(tarball);
|
||||
tarball = qq.join([c.workspace(), tarball]);
|
||||
qq.cd(c.workspace());
|
||||
- await qq.x(`tar -xzf ${tarball}`);
|
||||
+ const { msysExec, toMsysPath } = require("../util");
|
||||
+ await msysExec(`tar -xzf ${toMsysPath(tarball)}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const f of await qq.ls('package', { fullpath: true }))
|
||||
await qq.mv(f, '.');
|
||||
await qq.rm('package', tarball, 'bin/run.cmd');
|
||||
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
|
||||
+ await qq.mv('bin/balena', 'bin/run');
|
||||
+ // 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 qq.rm('bin/.fast-boot.json');
|
||||
};
|
||||
const updatePJSON = async () => {
|
||||
qq.cd(c.workspace());
|
||||
@@ -46,21 +55,21 @@ async function build(c, options = {}) {
|
||||
await qq.writeJSON('package.json', pjson);
|
||||
};
|
||||
const addDependencies = async () => {
|
||||
- qq.cd(c.workspace());
|
||||
- const yarnRoot = findYarnWorkspaceRoot(c.root) || c.root;
|
||||
- const yarn = await qq.exists([yarnRoot, 'yarn.lock']);
|
||||
- if (yarn) {
|
||||
- await qq.cp([yarnRoot, 'yarn.lock'], '.');
|
||||
- await qq.x('yarn --no-progress --production --non-interactive');
|
||||
- }
|
||||
- else {
|
||||
- let lockpath = qq.join(c.root, 'package-lock.json');
|
||||
- if (!await qq.exists(lockpath)) {
|
||||
- lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
|
||||
- }
|
||||
- await qq.cp(lockpath, '.');
|
||||
- await qq.x('npm install --production');
|
||||
+ const ws = c.workspace();
|
||||
+ qq.cd(ws);
|
||||
+ console.error(`[debug] oclif copying node_modules to "${ws}"`)
|
||||
+ const source = path.join(c.root, 'node_modules');
|
||||
+ if (process.platform === 'win32') {
|
||||
+ // xcopy is much faster than `qq.cp(source, ws)`
|
||||
+ await qq.x(`xcopy "${source}" "${ws}\\node_modules" /S /E /B /I /K /Q /Y`);
|
||||
+ } else {
|
||||
+ // use the shell's `cp` on macOS in order to preserve extended
|
||||
+ // file attributes containing `codesign` digital signatures
|
||||
+ await qq.x(`cp -pR "${source}" "${ws}"`);
|
||||
}
|
||||
+ console.error(`[debug] oclif running "npm prune --production" in "${ws}"`);
|
||||
+ await qq.x('npm prune --production');
|
||||
+ console.error(`[debug] oclif done`);
|
||||
};
|
||||
const pretarball = async () => {
|
||||
qq.cd(c.workspace());
|
||||
@@ -99,7 +108,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;
|
||||
diff --git a/node_modules/oclif/lib/tarballs/config.js b/node_modules/oclif/lib/tarballs/config.js
|
||||
index 0dc3cd7..1336219 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/config.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/config.js
|
||||
@@ -18,7 +18,10 @@ function gitSha(cwd, options = {}) {
|
||||
}
|
||||
exports.gitSha = gitSha;
|
||||
async function Tmp(config) {
|
||||
- const tmp = path.join(config.root, 'tmp');
|
||||
+ const tmp = process.env.BUILD_TMP
|
||||
+ ? path.join(process.env.BUILD_TMP, 'oclif')
|
||||
+ : path.join(config.root, 'tmp');
|
||||
+ console.error(`[debug] oclif tmp="${tmp}"`);
|
||||
await qq.mkdirp(tmp);
|
||||
return tmp;
|
||||
}
|
||||
@@ -43,7 +46,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('-'), upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
|
||||
return qq.join(base, upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
|
||||
diff --git a/node_modules/oclif/lib/tarballs/node.js b/node_modules/oclif/lib/tarballs/node.js
|
||||
index fabe5c4..e32dd76 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/node.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/node.js
|
||||
@@ -4,9 +4,10 @@ const errors_1 = require("@oclif/errors");
|
||||
const path = require("path");
|
||||
const qq = require("qqjs");
|
||||
const log_1 = require("../log");
|
||||
+const { isMSYS2, msysExec, toMsysPath } = require("../util");
|
||||
async function checkFor7Zip() {
|
||||
try {
|
||||
- await qq.x('7z', { stdio: [0, null, 2] });
|
||||
+ await msysExec('7z', { stdio: [0, null, 2] });
|
||||
}
|
||||
catch (error) {
|
||||
if (error.code === 127)
|
||||
@@ -41,7 +42,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 msysExec(`grep ${path.basename(tarball)} ${toMsysPath(shasums)} | ${shaCmd}`, { cwd: basedir });
|
||||
};
|
||||
const extract = async () => {
|
||||
log_1.log(`extracting ${nodeBase}`);
|
||||
@@ -51,7 +53,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 msysExec(`7z x -bd -y ${toMsysPath(tarball)} > /dev/null`);
|
||||
await qq.mv([nodeBase, 'node.exe'], cache);
|
||||
qq.popd();
|
||||
}
|
||||
diff --git a/node_modules/oclif/lib/upload-util.js b/node_modules/oclif/lib/upload-util.js
|
||||
index 45392cb..3c806c7 100644
|
||||
--- a/node_modules/oclif/lib/upload-util.js
|
||||
+++ b/node_modules/oclif/lib/upload-util.js
|
||||
@@ -28,10 +28,10 @@ function templateShortKey(type, ext, options = { root: '.' }) {
|
||||
const templates = {
|
||||
baseDir: '<%- bin %>',
|
||||
unversioned: '<%- bin %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
- versioned: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
- manifest: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %>-buildmanifest',
|
||||
- macos: '<%- bin %>-v<%- version %>-<%- sha %>.pkg',
|
||||
- win32: '<%- bin %>-v<%- version %>-<%- sha %>-<%- arch %>.exe',
|
||||
+ versioned: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
+ manifest: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %>-buildmanifest',
|
||||
+ macos: '<%- bin %>-v<%- version %>.pkg',
|
||||
+ win32: '<%- bin %>-v<%- version %>-<%- arch %>.exe',
|
||||
deb: '<%- bin %>_<%- versionShaRevision %>_<%- arch %>.deb',
|
||||
};
|
||||
return _.template(templates[type])(Object.assign({}, options));
|
||||
diff --git a/node_modules/oclif/lib/util.js b/node_modules/oclif/lib/util.js
|
||||
index 17748ad..4928fc9 100644
|
||||
--- a/node_modules/oclif/lib/util.js
|
||||
+++ b/node_modules/oclif/lib/util.js
|
||||
@@ -67,3 +67,47 @@ exports.sortVersionsObjectByKeysDesc = (input) => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
+
|
||||
+// 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');
|
||||
+const MSYSSHELLPATH = 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' : '/bin/sh')));
|
||||
+
|
||||
+exports.isCygwin = isCygwin;
|
||||
+exports.isMinGW = isMinGW;
|
||||
+exports.isMSYS2 = isMSYS2;
|
||||
+console.error(`[debug] oclif MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
|
||||
+
|
||||
+const qq = require("qqjs");
|
||||
+
|
||||
+/* Convert a Windows path like 'C:\tmp' to a MSYS path like '/c/tmp' */
|
||||
+function toMsysPath(windowsPath) {
|
||||
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
|
||||
+ let msysPath = windowsPath.replace(/\\/g, '/');
|
||||
+ if (isMSYS2 || isMinGW) {
|
||||
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/$1');
|
||||
+ } else if (isCygwin) {
|
||||
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
|
||||
+ }
|
||||
+ console.error(`[debug] oclif toMsysPath before="${windowsPath}" after="${msysPath}"`);
|
||||
+ return msysPath;
|
||||
+}
|
||||
+exports.toMsysPath = toMsysPath;
|
||||
+
|
||||
+/* Like qqjs qq.x(), but using MSYS bash on Windows instead of cmd.exe */
|
||||
+async function msysExec(cmd, options = {}) {
|
||||
+ if (process.platform !== 'win32') {
|
||||
+ return qq.x(cmd, options);
|
||||
+ }
|
||||
+ const sh = MSYSSHELLPATH;
|
||||
+ const args = ['-c', cmd];
|
||||
+ console.error(`[debug] oclif msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
|
||||
+ return qq.x(sh, args, options);
|
||||
+}
|
||||
+exports.msysExec = msysExec;
|
33
patches/all/oclif+4.17.0.dev.patch
Normal file
33
patches/all/oclif+4.17.0.dev.patch
Normal file
@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||
index bfe9205..482519e 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||
@@ -86,6 +86,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 f0c8d95..a72400e 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||
@@ -218,6 +218,11 @@ 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) => {
|
||||
if (target.platform === 'win32' && target.arch === 'arm64' && (0, semver_1.lt)(c.nodeVersion, '20.0.0')) {
|
@ -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,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
@ -6,15 +6,17 @@ upstream:
|
||||
url: 'https://github.com/balena-io/balena-sdk'
|
||||
- repo: 'balena-config-json'
|
||||
url: 'https://github.com/balena-io-modules/balena-config-json'
|
||||
- repo: 'balena-image-fs'
|
||||
url: 'https://github.com/balena-io-modules/balena-image-fs'
|
||||
- repo: 'balena-device-init'
|
||||
url: 'https://github.com/balena-io-modules/balena-device-init'
|
||||
- repo: 'balena-image-manager'
|
||||
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'
|
||||
- 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,14 +16,15 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import type { AppOptions } from './preparser';
|
||||
import {
|
||||
AppOptions,
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
import { run as mainRun, settings } from '@oclif/core';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
@ -100,11 +101,9 @@ async function init() {
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
let deprecationPromise: Promise<void> | undefined;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
@ -114,10 +113,16 @@ async function oclifRun(command: string[], options: AppOptions) {
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
let isEEXIT = false;
|
||||
try {
|
||||
await CustomMain.run(command);
|
||||
if (options.development) {
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
settings.debug = true;
|
||||
}
|
||||
// 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.
|
||||
@ -130,7 +135,8 @@ async function oclifRun(command: string[], options: AppOptions) {
|
||||
}
|
||||
}
|
||||
if (shouldFlush) {
|
||||
await import('@oclif/command/flush');
|
||||
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.
|
||||
@ -145,13 +151,13 @@ async function oclifRun(command: string[], options: AppOptions) {
|
||||
}
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
const { trackPromise } = await import('./hooks/prerun');
|
||||
|
||||
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: 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 { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||
'./utils/bootstrap'
|
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 |
156
src/commands/api-key/generate.ts
Normal file
156
src/commands/api-key/generate.ts
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @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 { Args, Command } from '@oclif/core';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
formatDuration,
|
||||
intervalToDuration,
|
||||
isValid,
|
||||
parseISO,
|
||||
} from 'date-fns';
|
||||
|
||||
// In days
|
||||
const durations = [1, 7, 30, 90];
|
||||
|
||||
async function isLoggedInWithJwt() {
|
||||
const balena = getBalenaSdk();
|
||||
try {
|
||||
const token = await balena.auth.getToken();
|
||||
const { default: jwtDecode } = await import('jwt-decode');
|
||||
jwtDecode(token);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
|
||||
Generate a new balenaCloud API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena api-key generate "Jenkins Key"',
|
||||
'$ balena api-key generate "Jenkins Key" 2025-10-30',
|
||||
'$ balena api-key generate "Jenkins Key" never',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
}),
|
||||
expiryDate: Args.string({
|
||||
description:
|
||||
'the expiry date of the API key as an ISO date string, or "never" for no expiry',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(GenerateCmd);
|
||||
|
||||
let expiryDateResponse: string | number | undefined = params.expiryDate;
|
||||
let key;
|
||||
try {
|
||||
if (!expiryDateResponse) {
|
||||
expiryDateResponse = await getCliForm().ask({
|
||||
message: 'Please pick an expiry date for the API key',
|
||||
type: 'list',
|
||||
choices: [...durations, 'custom', 'never'].map((duration) => ({
|
||||
name:
|
||||
duration === 'never'
|
||||
? 'No expiration'
|
||||
: typeof duration === 'number'
|
||||
? formatDuration(
|
||||
intervalToDuration({
|
||||
start: 0,
|
||||
end: duration * 24 * 60 * 60 * 1000,
|
||||
}),
|
||||
)
|
||||
: 'Custom expiration',
|
||||
value: duration,
|
||||
})),
|
||||
});
|
||||
}
|
||||
let expiryDate: Date | null;
|
||||
if (expiryDateResponse === 'never') {
|
||||
expiryDate = null;
|
||||
} else if (expiryDateResponse === 'custom') {
|
||||
do {
|
||||
expiryDate = parseISO(
|
||||
await getCliForm().ask({
|
||||
message:
|
||||
'Please enter an expiry date for the API key as an ISO date string',
|
||||
type: 'input',
|
||||
}),
|
||||
);
|
||||
if (!isValid(expiryDate)) {
|
||||
console.error('Invalid date format');
|
||||
}
|
||||
} while (!isValid(expiryDate));
|
||||
} else if (typeof expiryDateResponse === 'string') {
|
||||
expiryDate = parseISO(expiryDateResponse);
|
||||
if (!isValid(expiryDate)) {
|
||||
throw new Error(
|
||||
'Invalid date format, please use a valid ISO date string',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
expiryDate = new Date(
|
||||
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
key = await getBalenaSdk().models.apiKey.create({
|
||||
name: params.name,
|
||||
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
if (await isLoggedInWithJwt()) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command requires you to have been recently authenticated.
|
||||
Please login again with 'balena login'.
|
||||
In case you are using the Web authorization method, you need to logout and re-login to the dashboard first.
|
||||
`);
|
||||
}
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command cannot be run when logged in with an API key.
|
||||
Please login again with 'balena login' and select an alternative method.
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
}
|
||||
}
|
82
src/commands/api-key/list.ts
Normal file
82
src/commands/api-key/list.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @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, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class APIKeyListCmd extends Command {
|
||||
public static aliases = ['api-keys'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
public static description = stripIndent`
|
||||
Print a list of balenaCloud API keys.
|
||||
|
||||
Print a list of balenaCloud API keys.
|
||||
|
||||
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
|
||||
`;
|
||||
public static examples = ['$ balena api-key list'];
|
||||
|
||||
public static flags = {
|
||||
user: Flags.boolean({
|
||||
char: 'u',
|
||||
description: 'show API keys for your user',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = await this.parse(APIKeyListCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const actorId = options.fleet
|
||||
? (
|
||||
await getApplication(getBalenaSdk(), options.fleet, {
|
||||
$select: 'actor',
|
||||
})
|
||||
).actor.__id
|
||||
: await getBalenaSdk().auth.getActorId();
|
||||
const keys = await getBalenaSdk().pine.get({
|
||||
resource: 'api_key',
|
||||
options: {
|
||||
$select: ['id', 'created_at', 'name', 'description', 'expiry_date'],
|
||||
$filter: {
|
||||
is_of__actor: actorId,
|
||||
...(options.user
|
||||
? {
|
||||
name: {
|
||||
$ne: null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
$orderby: 'name asc',
|
||||
},
|
||||
});
|
||||
const fields = ['id', 'name', 'created_at', 'description', 'expiry_date'];
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
keys.map((key) => _.mapValues(key, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
59
src/commands/api-key/revoke.ts
Normal file
59
src/commands/api-key/revoke.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @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 { Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class RevokeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Revoke balenaCloud API keys.
|
||||
|
||||
Revoke balenaCloud API keys with the given
|
||||
comma-separated list of ids.
|
||||
|
||||
The given balenaCloud API keys will no longer be usable.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena api-key revoke 123',
|
||||
'$ balena api-key revoke 123,124,456',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
ids: Args.string({
|
||||
description: 'the API key ids',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(RevokeCmd);
|
||||
|
||||
const apiKeyIds = params.ids.split(',');
|
||||
if (apiKeyIds.filter((apiKeyId) => !apiKeyId.match(/^\d+$/)).length > 0) {
|
||||
console.log('API key ids must be positive integers');
|
||||
return;
|
||||
}
|
||||
await Promise.all(
|
||||
apiKeyIds.map(async (id) => {
|
||||
await getBalenaSdk().models.apiKey.revoke(Number(id));
|
||||
}),
|
||||
);
|
||||
console.log('Successfully revoked the given API keys');
|
||||
}
|
||||
}
|
77
src/commands/app/create.ts
Normal file
77
src/commands/app/create.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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.
|
||||
*/
|
||||
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an app.
|
||||
|
||||
Create a new balena app.
|
||||
|
||||
You can specify the organization the app 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 organization list\` command.
|
||||
|
||||
The app's default device type is specified with the \`--type\` option.
|
||||
The \`balena device-type list\` 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: Args.string({
|
||||
description: 'app name',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static flags = {
|
||||
organization: Flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the app should belong to',
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'app device type (Check available types with `balena device-type list`)',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(AppCreateCmd);
|
||||
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('app', options, params);
|
||||
}
|
||||
}
|
77
src/commands/block/create.ts
Normal file
77
src/commands/block/create.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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.
|
||||
*/
|
||||
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class BlockCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an block.
|
||||
|
||||
Create a new balena block.
|
||||
|
||||
You can specify the organization the block 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 organization list\` command.
|
||||
|
||||
The block's default device type is specified with the \`--type\` option.
|
||||
The \`balena device-type list\` 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 block create MyBlock',
|
||||
'$ balena block create MyBlock --organization mmyorg',
|
||||
'$ balena block create MyBlock -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'block name',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static flags = {
|
||||
organization: Flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the block should belong to',
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'block device type (Check available types with `balena device-type list`)',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(BlockCreateCmd);
|
||||
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('block', options, params);
|
||||
}
|
||||
}
|
@ -15,31 +15,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import { Args, Flags, Command } from '@oclif/core';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as compose from '../../utils/compose';
|
||||
import type {
|
||||
ApplicationType,
|
||||
BalenaSDK,
|
||||
DeviceType,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import {
|
||||
buildArgDeprecation,
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
} from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
} from '../../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||
import { dockerCliFlags } from '../../utils/docker';
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
|
||||
|
||||
interface PrepareBuildOpts
|
||||
extends ComposeCliFlags,
|
||||
DockerCliFlags,
|
||||
ComposeGenerateOptsParam {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
@ -68,75 +73,73 @@ ${dockerignoreHelp}
|
||||
public static examples = [
|
||||
'$ balena build --fleet myFleet',
|
||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||
'$ balena build --deviceType raspberrypi3 --emulated',
|
||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'source',
|
||||
description: 'path of project source directory',
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
source: Args.string({ description: 'path of project source directory' }),
|
||||
};
|
||||
|
||||
public static usage = 'build [source]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
arch: flags.string({
|
||||
public static flags = {
|
||||
arch: Flags.string({
|
||||
description: 'the architecture to build for',
|
||||
char: 'A',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
deviceType: Flags.string({
|
||||
description: 'the type of device this build is for',
|
||||
char: 'd',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
BuildCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(BuildCmd);
|
||||
|
||||
await Command.checkLoggedInIf(!!options.fleet);
|
||||
const Logger = await import('../../utils/logger');
|
||||
const { checkLoggedInIf } = await import('../../utils/patterns');
|
||||
|
||||
await checkLoggedInIf(!!options.fleet);
|
||||
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
const logger = await Command.getLogger();
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// `build` accepts `source` as a parameter, but compose expects it as an option
|
||||
options.source = params.source;
|
||||
delete params.source;
|
||||
const prepareBuildOpts = {
|
||||
...options,
|
||||
source: params.source,
|
||||
};
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
|
||||
|
||||
await this.validateOptions(prepareBuildOpts, sdk);
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
if (prepareBuildOpts.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
const app = await this.getAppAndResolveArch(prepareBuildOpts);
|
||||
|
||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||
const { docker, buildOpts, composeOpts } =
|
||||
await this.prepareBuild(prepareBuildOpts);
|
||||
|
||||
try {
|
||||
await this.buildProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
arch: options.arch!,
|
||||
deviceType: options.deviceType!,
|
||||
buildEmulated: options.emulated,
|
||||
appType: app?.application_type?.[0],
|
||||
arch: prepareBuildOpts.arch!,
|
||||
deviceType: prepareBuildOpts.deviceType!,
|
||||
buildEmulated: prepareBuildOpts.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
} catch (err) {
|
||||
@ -148,20 +151,20 @@ ${dockerignoreHelp}
|
||||
logger.logSuccess('Build succeeded!');
|
||||
}
|
||||
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||
) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||
'You must specify either a fleet (-f), or the device type (-d) and optionally the architecture (-A)',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate project directory
|
||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
||||
const { validateProjectDirectory } = await import('../../utils/compose_ts');
|
||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||
sdk,
|
||||
{
|
||||
@ -176,9 +179,45 @@ ${dockerignoreHelp}
|
||||
opts['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||
protected async resolveArchFromDeviceType(
|
||||
sdk: BalenaSDK,
|
||||
opts: PrepareBuildOpts,
|
||||
) {
|
||||
if (opts.deviceType != null && opts.arch == null) {
|
||||
try {
|
||||
const deviceTypeOpts = {
|
||||
$select: 'is_of__cpu_architecture',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} satisfies PineOptions<DeviceType>;
|
||||
opts.arch = (
|
||||
(await sdk.models.deviceType.get(
|
||||
opts.deviceType,
|
||||
deviceTypeOpts,
|
||||
)) as PineTypedResult<DeviceType, typeof deviceTypeOpts>
|
||||
).is_of__cpu_architecture[0].slug;
|
||||
} catch (err) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
if (err instanceof sdk.errors.BalenaInvalidDeviceType) {
|
||||
let message = err.message;
|
||||
if (!(await sdk.auth.isLoggedIn())) {
|
||||
message = `${message}. In case you are trying to use a private device type, please try to log in first.`;
|
||||
}
|
||||
throw new ExpectedError(message);
|
||||
}
|
||||
throw new ExpectedError(
|
||||
'Failed to resolve the architecture of the provided device type. If you are in an air-gapped environment please also define the architecture (-A) parameter.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
|
||||
if (opts.fleet) {
|
||||
const { getAppWithArch } = await import('../utils/helpers');
|
||||
const { getAppWithArch } = await import('../../utils/helpers');
|
||||
const app = await getAppWithArch(opts.fleet);
|
||||
opts.arch = app.arch;
|
||||
opts.deviceType = app.is_for__device_type[0].slug;
|
||||
@ -186,8 +225,8 @@ ${dockerignoreHelp}
|
||||
}
|
||||
}
|
||||
|
||||
protected async prepareBuild(options: FlagsDef) {
|
||||
const { getDocker, generateBuildOpts } = await import('../utils/docker');
|
||||
protected async prepareBuild(options: PrepareBuildOpts) {
|
||||
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
getDocker(options),
|
||||
generateBuildOpts(options),
|
||||
@ -208,24 +247,24 @@ ${dockerignoreHelp}
|
||||
* buildEmulated
|
||||
* buildOpts: arguments to forward to docker build command
|
||||
*
|
||||
* @param {DockerToolbelt} docker
|
||||
* @param {Dockerode} docker
|
||||
* @param {Logger} logger
|
||||
* @param {ComposeOpts} composeOpts
|
||||
* @param opts
|
||||
*/
|
||||
protected async buildProject(
|
||||
docker: import('dockerode'),
|
||||
logger: import('../utils/logger'),
|
||||
logger: import('../../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app?: Application;
|
||||
appType?: Pick<ApplicationType, 'supports_multicontainer'>;
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: BuildOpts;
|
||||
},
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
const { loadProject } = await import('../../utils/compose_ts');
|
||||
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
@ -234,11 +273,10 @@ ${dockerignoreHelp}
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
if (
|
||||
appType != null &&
|
||||
opts.appType != null &&
|
||||
project.descriptors.length > 1 &&
|
||||
!appType.supports_multicontainer
|
||||
!opts.appType.supports_multicontainer
|
||||
) {
|
||||
logger.logWarn(
|
||||
'Target fleet does not support multiple containers.\n' +
|
@ -15,31 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Flags, Command } from '@oclif/core';
|
||||
import type { Interfaces } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
fleet?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
'generate-device-api-key': boolean;
|
||||
output?: string;
|
||||
// Options for non-interactive configuration
|
||||
network?: string;
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
help: void;
|
||||
}
|
||||
import {
|
||||
applicationIdInfo,
|
||||
devModeInfo,
|
||||
secureBootInfo,
|
||||
} from '../../utils/messages';
|
||||
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
||||
|
||||
export default class ConfigGenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
@ -51,6 +36,8 @@ export default class ConfigGenerateCmd extends Command {
|
||||
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${secureBootInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
@ -66,20 +53,20 @@ export default class ConfigGenerateCmd extends Command {
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
];
|
||||
|
||||
public static usage = 'config generate';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
version: flags.string({
|
||||
public static flags = {
|
||||
version: Flags.string({
|
||||
description: 'a balenaOS version',
|
||||
required: true,
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
secureBoot: cf.secureBoot,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
@ -88,64 +75,71 @@ export default class ConfigGenerateCmd extends Command {
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
deviceApiKey: Flags.string({
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
char: 'k',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
deviceType: Flags.string({
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
"device type slug (run 'balena device-type list' for possible values)",
|
||||
}),
|
||||
'generate-device-api-key': flags.boolean({
|
||||
'generate-device-api-key': Flags.boolean({
|
||||
description: 'generate a fresh device key for the device',
|
||||
}),
|
||||
output: flags.string({
|
||||
output: Flags.string({
|
||||
description: 'path of output file',
|
||||
char: 'o',
|
||||
}),
|
||||
// Options for non-interactive configuration
|
||||
network: flags.string({
|
||||
network: Flags.string({
|
||||
description: 'the network type to use: ethernet or wifi',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
wifiSsid: flags.string({
|
||||
wifiSsid: Flags.string({
|
||||
description:
|
||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
wifiKey: flags.string({
|
||||
wifiKey: Flags.string({
|
||||
description:
|
||||
'the wifi key to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
appUpdatePollInterval: flags.string({
|
||||
appUpdatePollInterval: Flags.string({
|
||||
description:
|
||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
'provisioning-key-name': Flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
'provisioning-key-expiry-date': Flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
||||
|
||||
public async getApplication(balena: BalenaSDK, fleet: string) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
return await getApplication(balena, fleet, {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = await this.parse(ConfigGenerateCmd);
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await this.validateOptions(options);
|
||||
|
||||
let resourceDeviceType: string;
|
||||
let application: ApplicationWithDeviceType | null = null;
|
||||
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
||||
null;
|
||||
let device:
|
||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||
| null = null;
|
||||
@ -165,11 +159,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
application = (await getApplication(balena, options.fleet!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
application = await this.getApplication(balena, options.fleet!);
|
||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||
}
|
||||
|
||||
@ -191,8 +181,16 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||
const deviceManifest =
|
||||
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
|
||||
|
||||
const { validateSecureBootOptionAndWarn } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
await validateSecureBootOptionAndWarn(
|
||||
options.secureBoot,
|
||||
deviceType,
|
||||
options.version,
|
||||
);
|
||||
|
||||
// Prompt for values
|
||||
@ -203,6 +201,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
});
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.secureBoot = options.secureBoot;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
@ -244,7 +243,9 @@ export default class ConfigGenerateCmd extends Command {
|
||||
protected readonly deviceTypeNotAllowedMessage =
|
||||
'The --deviceType option can only be used alongside the --fleet option';
|
||||
|
||||
protected async validateOptions(options: FlagsDef) {
|
||||
protected async validateOptions(
|
||||
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
|
||||
) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
if (options.device == null && options.fleet == null) {
|
||||
@ -254,6 +255,8 @@ export default class ConfigGenerateCmd extends Command {
|
||||
if (!options.fleet && options.deviceType) {
|
||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||
}
|
||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||
options.version = normalizeOsVersion(options.version);
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, options.version);
|
||||
}
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default class ConfigInjectCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Inject a config.json file to a balenaOS image or attached media.
|
||||
@ -46,28 +35,22 @@ export default class ConfigInjectCmd extends Command {
|
||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'file',
|
||||
public static args = {
|
||||
file: Args.string({
|
||||
description: 'the path to the config.json file to inject',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigInjectCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
|
||||
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
@ -15,18 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export default class ConfigReadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Read the config.json file of a balenaOS image or attached media.
|
||||
@ -44,11 +36,8 @@ export default class ConfigReadCmd extends Command {
|
||||
'$ balena config read --drive balena.img',
|
||||
];
|
||||
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
@ -56,7 +45,7 @@ export default class ConfigReadCmd extends Command {
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
const { flags: options } = await this.parse(ConfigReadCmd);
|
||||
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
@ -15,19 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Flags, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string;
|
||||
drive?: string;
|
||||
advanced: boolean;
|
||||
help: void;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export default class ConfigReconfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Interactively reconfigure a balenaOS image file or attached media.
|
||||
@ -47,16 +38,13 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
'$ balena config reconfigure --drive balena.img --advanced',
|
||||
];
|
||||
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
advanced: flags.boolean({
|
||||
advanced: Flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
char: 'v',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
version: Flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
};
|
||||
@ -65,7 +53,7 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
||||
const { flags: options } = await this.parse(ConfigReconfigureCmd);
|
||||
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
@ -15,22 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
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 the config.json file of an OS image or attached media.
|
||||
@ -48,33 +36,26 @@ export default class ConfigWriteCmd extends Command {
|
||||
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'key',
|
||||
public static args = {
|
||||
key: Args.string({
|
||||
description: 'the key of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
}),
|
||||
value: Args.string({
|
||||
description: 'the value of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigWriteCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
|
||||
|
||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||
|
@ -15,45 +15,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args, Flags, Command } from '@oclif/core';
|
||||
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
buildArgDeprecation,
|
||||
} from '../utils/messages';
|
||||
import * as ca from '../utils/common-args';
|
||||
import * as compose from '../utils/compose';
|
||||
} from '../../utils/messages';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import * as compose from '../../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
ComposeOpts,
|
||||
Release as ComposeReleaseInfo,
|
||||
} from '../utils/compose-types';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
} from '../../utils/compose-types';
|
||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
buildProject,
|
||||
composeCliFlags,
|
||||
isBuildConfig,
|
||||
parseReleaseTagKeysAndValues,
|
||||
} from '../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import type {
|
||||
Application,
|
||||
ApplicationType,
|
||||
DeviceType,
|
||||
Release,
|
||||
} from 'balena-sdk';
|
||||
} from '../../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../../utils/docker';
|
||||
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
|
||||
|
||||
interface ApplicationWithArch extends Application {
|
||||
interface ApplicationWithArch {
|
||||
id: number;
|
||||
arch: string;
|
||||
is_for__device_type: [Pick<DeviceType, 'slug'>];
|
||||
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
|
||||
}
|
||||
|
||||
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
|
||||
// because of the 'registry-secrets' type which is defined in the actual code
|
||||
// as a path (string | undefined) but then the cli turns it into an object
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
source?: string;
|
||||
build: boolean;
|
||||
@ -61,12 +60,6 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
note?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export default class DeployCmd extends Command {
|
||||
@ -107,31 +100,26 @@ ${dockerignoreHelp}
|
||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
ca.fleetRequired,
|
||||
{
|
||||
name: 'image',
|
||||
description: 'the image to deploy',
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
image: Args.string({ description: 'the image to deploy' }),
|
||||
};
|
||||
|
||||
public static usage = 'deploy <fleet> [image]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
public static flags = {
|
||||
source: Flags.string({
|
||||
description:
|
||||
'specify an alternate source directory; default is the working directory',
|
||||
char: 's',
|
||||
}),
|
||||
build: flags.boolean({
|
||||
build: Flags.boolean({
|
||||
description: 'force a rebuild before deploy',
|
||||
char: 'b',
|
||||
}),
|
||||
nologupload: flags.boolean({
|
||||
nologupload: Flags.boolean({
|
||||
description:
|
||||
"don't upload build logs to the dashboard with image (if building)",
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
'release-tag': Flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the image deployment is successful. Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
@ -139,7 +127,7 @@ ${dockerignoreHelp}
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
draft: flags.boolean({
|
||||
draft: Flags.boolean({
|
||||
description: stripIndent`
|
||||
Deploy the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
@ -147,12 +135,9 @@ ${dockerignoreHelp}
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
note: flags.string({ description: 'The notes for this release' }),
|
||||
note: Flags.string({ description: 'The notes for this release' }),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -160,13 +145,13 @@ ${dockerignoreHelp}
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeployCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeployCmd);
|
||||
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const logger = await Command.getLogger();
|
||||
const Logger = await import('../../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
const { fleet, image } = params;
|
||||
@ -184,7 +169,7 @@ ${dockerignoreHelp}
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||
'../utils/compose_ts'
|
||||
'../../utils/compose_ts'
|
||||
);
|
||||
|
||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||
@ -192,7 +177,7 @@ ${dockerignoreHelp}
|
||||
);
|
||||
|
||||
if (image) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
@ -205,16 +190,16 @@ ${dockerignoreHelp}
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
});
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
(options as FlagsDef)['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
const helpers = await import('../utils/helpers');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const app = await helpers.getAppWithArch(fleet);
|
||||
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const dockerUtils = await import('../../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
dockerUtils.generateBuildOpts(options as FlagsDef),
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
|
||||
@ -235,13 +220,13 @@ ${dockerignoreHelp}
|
||||
releaseTagValues,
|
||||
);
|
||||
if (options.note) {
|
||||
await sdk.models.release.note(release.id, options.note);
|
||||
await sdk.models.release.setNote(release.id, options.note);
|
||||
}
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
docker: import('dockerode'),
|
||||
logger: import('../utils/logger'),
|
||||
logger: import('../../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app: ApplicationWithArch; // the application instance to deploy to
|
||||
@ -259,10 +244,10 @@ ${dockerignoreHelp}
|
||||
const doodles = await import('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const { deployProject: $deployProject, loadProject } = await import(
|
||||
'../utils/compose_ts'
|
||||
'../../utils/compose_ts'
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
const appType = opts.app.application_type[0];
|
||||
|
||||
try {
|
||||
const project = await loadProject(
|
||||
@ -319,7 +304,7 @@ ${dockerignoreHelp}
|
||||
projectName: project.name,
|
||||
composition: compositionToBuild,
|
||||
arch: opts.app.arch,
|
||||
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||
deviceType: opts.app.is_for__device_type[0].slug,
|
||||
emulated: opts.buildEmulated,
|
||||
buildOpts: opts.buildOpts,
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
@ -340,17 +325,17 @@ ${dockerignoreHelp}
|
||||
);
|
||||
|
||||
let release: Release | ComposeReleaseInfo['release'];
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||
const { deployLegacy } = require('../../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target fleet requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
const [token, username, url, options] = await Promise.all([
|
||||
const [token, { username }, url, options] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.auth.getUserInfo(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
@ -373,23 +358,17 @@ ${dockerignoreHelp}
|
||||
$select: ['commit'],
|
||||
});
|
||||
} else {
|
||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserId(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
]);
|
||||
release = await $deployProject(
|
||||
docker,
|
||||
sdk,
|
||||
logger,
|
||||
project.composition,
|
||||
images,
|
||||
opts.app.id,
|
||||
userId,
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
composeOpts.projectPath,
|
||||
opts.createAsDraft,
|
||||
project.descriptors,
|
||||
);
|
||||
}
|
||||
|
@ -14,25 +14,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags, Command } from '@oclif/core';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
json?: boolean;
|
||||
}
|
||||
export default class DeviceTypeListCmd extends Command {
|
||||
public static aliases = ['devices supported'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
export default class DevicesSupportedCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
By default, only actively supported device types are listed.
|
||||
The --all option can be used to list all device types, including those that are
|
||||
no longer supported by balena.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
@ -41,38 +39,39 @@ export default class DevicesSupportedCmd extends Command {
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices supported',
|
||||
'$ balena devices supported --json',
|
||||
'$ balena device-type list',
|
||||
'$ balena device-type list --all',
|
||||
'$ balena device-type list --json',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'devices supported ' +
|
||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
public static flags = {
|
||||
json: Flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
all: Flags.boolean({
|
||||
description: 'include device types no longer supported by balena',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
const { flags: options } = await this.parse(DeviceTypeListCmd);
|
||||
const pineOptions = {
|
||||
$select: (['slug', 'name'] as const).slice(),
|
||||
$select: ['slug', 'name'],
|
||||
$expand: {
|
||||
is_of__cpu_architecture: { $select: 'slug' },
|
||||
device_type_alias: {
|
||||
$select: 'is_referenced_by__alias',
|
||||
$orderby: 'is_referenced_by__alias asc',
|
||||
$orderby: { is_referenced_by__alias: 'asc' },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
|
||||
pineOptions,
|
||||
)) as Array<
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const dts = (
|
||||
options.all
|
||||
? await getBalenaSdk().models.deviceType.getAll(pineOptions)
|
||||
: await getBalenaSdk().models.deviceType.getAllSupported(pineOptions)
|
||||
) as Array<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>;
|
||||
interface DT {
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceDeactivateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Deactivate a device.
|
||||
@ -44,27 +33,22 @@ export default class DeviceDeactivateCmd extends Command {
|
||||
'$ balena device deactivate 7cf02a6 --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the UUID of the device to be deactivated',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device deactivate <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceDeactivateCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceDeactivateCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
@ -15,20 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { LocalBalenaOsDevice } from 'balena-sync';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getCliUx, stripIndent } from '../utils/lazy';
|
||||
import { Flags, Command } from '@oclif/core';
|
||||
import { getCliUx, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
json?: boolean;
|
||||
verbose: boolean;
|
||||
timeout?: number;
|
||||
help: void;
|
||||
}
|
||||
export default class DeviceDetectCmd extends Command {
|
||||
public static aliases = ['scan'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
export default class ScanCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for balenaOS devices on your local network.
|
||||
|
||||
@ -41,25 +34,22 @@ export default class ScanCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena scan',
|
||||
'$ balena scan --timeout 120',
|
||||
'$ balena scan --verbose',
|
||||
'$ balena device detect',
|
||||
'$ balena device detect --timeout 120',
|
||||
'$ balena device detect --verbose',
|
||||
];
|
||||
|
||||
public static usage = 'scan';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
verbose: flags.boolean({
|
||||
public static flags = {
|
||||
verbose: Flags.boolean({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'display full info',
|
||||
}),
|
||||
timeout: flags.integer({
|
||||
timeout: Flags.integer({
|
||||
char: 't',
|
||||
description: 'scan timeout in seconds',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
json: Flags.boolean({
|
||||
default: false,
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
@ -72,14 +62,16 @@ export default class ScanCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const _ = await import('lodash');
|
||||
const { discover } = await import('balena-sync');
|
||||
const { discoverLocalBalenaOsDevices } = await import(
|
||||
'../../utils/discover'
|
||||
);
|
||||
const prettyjson = await import('prettyjson');
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const dockerUtils = await import('../../utils/docker');
|
||||
|
||||
const dockerPort = 2375;
|
||||
const dockerTimeout = 2000;
|
||||
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
||||
const { flags: options } = await this.parse(DeviceDetectCmd);
|
||||
|
||||
const discoverTimeout =
|
||||
options.timeout != null ? options.timeout * 1000 : undefined;
|
||||
@ -88,8 +80,7 @@ export default class ScanCmd extends Command {
|
||||
const ux = getCliUx();
|
||||
ux.action.start('Scanning for local balenaOS devices');
|
||||
|
||||
const localDevices: LocalBalenaOsDevice[] =
|
||||
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
|
||||
const localDevices = await discoverLocalBalenaOsDevices(discoverTimeout);
|
||||
const engineReachableDevices: boolean[] = await Promise.all(
|
||||
localDevices.map(async ({ address }: { address: string }) => {
|
||||
const docker = await dockerUtils.createClient({
|
||||
@ -100,13 +91,13 @@ export default class ScanCmd extends Command {
|
||||
try {
|
||||
await docker.ping();
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
|
||||
const developmentDevices = localDevices.filter(
|
||||
(_localDevice, index) => engineReachableDevices[index],
|
||||
);
|
||||
|
||||
@ -116,18 +107,15 @@ export default class ScanCmd extends Command {
|
||||
_.isEqual,
|
||||
);
|
||||
|
||||
const productionDevicesInfo = _.map(
|
||||
productionDevices,
|
||||
(device: LocalBalenaOsDevice) => {
|
||||
return {
|
||||
host: device.host,
|
||||
address: device.address,
|
||||
osVariant: 'production',
|
||||
dockerInfo: undefined,
|
||||
dockerVersion: undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
const productionDevicesInfo = productionDevices.map((device) => {
|
||||
return {
|
||||
host: device.host,
|
||||
address: device.address,
|
||||
osVariant: 'production',
|
||||
dockerInfo: undefined,
|
||||
dockerVersion: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
// Query devices for info
|
||||
const devicesInfo = await Promise.all(
|
||||
@ -157,10 +145,10 @@ export default class ScanCmd extends Command {
|
||||
if (!options.verbose) {
|
||||
devicesInfo.forEach((d: any) => {
|
||||
d.dockerInfo = _.isObject(d.dockerInfo)
|
||||
? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties)
|
||||
? _.pick(d.dockerInfo, DeviceDetectCmd.dockerInfoProperties)
|
||||
: d.dockerInfo;
|
||||
d.dockerVersion = _.isObject(d.dockerVersion)
|
||||
? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties)
|
||||
? _.pick(d.dockerVersion, DeviceDetectCmd.dockerVersionProperties)
|
||||
: d.dockerVersion;
|
||||
});
|
||||
}
|
||||
@ -177,8 +165,9 @@ export default class ScanCmd extends Command {
|
||||
if (!options.json && cmdOutput.length === 0) {
|
||||
console.error(
|
||||
process.platform === 'win32'
|
||||
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
||||
: ScanCmd.noDevicesFoundMessage,
|
||||
? DeviceDetectCmd.noDevicesFoundMessage +
|
||||
DeviceDetectCmd.windowsTipMessage
|
||||
: DeviceDetectCmd.noDevicesFoundMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -210,11 +199,11 @@ export default class ScanCmd extends Command {
|
||||
protected static windowsTipMessage = `
|
||||
|
||||
Note for Windows users:
|
||||
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
|
||||
The 'device detect' command relies on the Bonjour service. Check whether Bonjour is
|
||||
installed (Control Panel > Programs and Features). If not, you can download
|
||||
Bonjour for Windows (included with Bonjour Print Services) from here:
|
||||
https://support.apple.com/kb/DL999
|
||||
|
||||
After installing Bonjour, restart your PC and run the 'balena scan' command
|
||||
After installing Bonjour, restart your PC and run the 'balena device detect' command
|
||||
again.`;
|
||||
}
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceIdentifyCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Identify a device.
|
||||
@ -38,24 +27,17 @@ export default class DeviceIdentifyCmd extends Command {
|
||||
`;
|
||||
public static examples = ['$ balena device identify 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to identify',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device identify <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
||||
const { args: params } = await this.parse(DeviceIdentifyCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -15,12 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { jsonInfo } from '../../utils/messages';
|
||||
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
|
||||
@ -41,39 +40,30 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
view: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Show info about a single device.
|
||||
|
||||
Show information about a single device.
|
||||
|
||||
${jsonInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device 7cf02a6',
|
||||
'$ balena device 7cf02a6 --view',
|
||||
'$ balena device 7cf02a6 --json',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the device uuid',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
view: flags.boolean({
|
||||
public static flags = {
|
||||
json: cf.json,
|
||||
view: Flags.boolean({
|
||||
default: false,
|
||||
description: 'open device dashboard page',
|
||||
}),
|
||||
@ -83,39 +73,63 @@ export default class DeviceCmd extends Command {
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device = (await balena.models.device.get(params.uuid, {
|
||||
$select: [
|
||||
'device_name',
|
||||
'id',
|
||||
'overall_status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'last_connectivity_event',
|
||||
'uuid',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'memory_usage',
|
||||
'memory_total',
|
||||
'public_address',
|
||||
'storage_block_device',
|
||||
'storage_usage',
|
||||
'storage_total',
|
||||
'cpu_usage',
|
||||
'cpu_temp',
|
||||
'cpu_id',
|
||||
'is_undervolted',
|
||||
],
|
||||
...expandForAppName,
|
||||
})) as ExtendedDevice;
|
||||
let device: ExtendedDevice;
|
||||
if (options.json) {
|
||||
const [deviceBase, deviceComputed] = await Promise.all([
|
||||
balena.models.device.get(params.uuid, {
|
||||
$expand: {
|
||||
device_tag: {
|
||||
$select: ['tag_key', 'value'],
|
||||
},
|
||||
...expandForAppName.$expand,
|
||||
},
|
||||
}),
|
||||
balena.models.device.get(params.uuid, {
|
||||
$select: [
|
||||
'overall_status',
|
||||
'overall_progress',
|
||||
'should_be_running__release',
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
device = {
|
||||
...deviceBase,
|
||||
...deviceComputed,
|
||||
} as ExtendedDevice;
|
||||
} else {
|
||||
device = (await balena.models.device.get(params.uuid, {
|
||||
$select: [
|
||||
'device_name',
|
||||
'id',
|
||||
'overall_status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'last_connectivity_event',
|
||||
'uuid',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'memory_usage',
|
||||
'memory_total',
|
||||
'public_address',
|
||||
'storage_block_device',
|
||||
'storage_usage',
|
||||
'storage_total',
|
||||
'cpu_usage',
|
||||
'cpu_temp',
|
||||
'cpu_id',
|
||||
'is_undervolted',
|
||||
],
|
||||
...expandForAppName,
|
||||
})) as ExtendedDevice;
|
||||
}
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
@ -179,6 +193,11 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(device, null, 4));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
@ -15,8 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Flags, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
@ -29,7 +28,6 @@ interface FlagsDef {
|
||||
'os-version'?: string;
|
||||
drive?: string;
|
||||
config?: string;
|
||||
help: void;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
@ -43,17 +41,17 @@ export default class DeviceInitCmd extends Command {
|
||||
This command effectively combines several other balena CLI commands in one,
|
||||
namely:
|
||||
|
||||
'balena device register'
|
||||
'balena os download'
|
||||
'balena os build-config' or 'balena config generate'
|
||||
'balena os configure'
|
||||
'balena device register'
|
||||
'balena os download'
|
||||
'balena os build-config' or 'balena config generate'
|
||||
'balena os configure'
|
||||
'balena os local flash'
|
||||
|
||||
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
|
||||
be listed respectively with the commands:
|
||||
|
||||
'balena fleets'
|
||||
'balena os versions'
|
||||
'balena fleet list'
|
||||
'balena os versions'
|
||||
'balena util available-drives'
|
||||
|
||||
If the '--fleet' or '--drive' options are omitted, interactive menus will be
|
||||
@ -74,16 +72,14 @@ export default class DeviceInitCmd extends Command {
|
||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
|
||||
];
|
||||
|
||||
public static usage = 'device init';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
yes: cf.yes,
|
||||
advanced: flags.boolean({
|
||||
advanced: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'show advanced configuration options',
|
||||
}),
|
||||
'os-version': flags.string({
|
||||
'os-version': Flags.string({
|
||||
description: stripIndent`
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
@ -93,23 +89,22 @@ export default class DeviceInitCmd extends Command {
|
||||
`,
|
||||
}),
|
||||
drive: cf.drive,
|
||||
config: flags.string({
|
||||
config: Flags.string({
|
||||
description: 'path to the config JSON file, see `balena os build-config`',
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
'provisioning-key-name': Flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
'provisioning-key-expiry-date': Flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
|
||||
const { flags: options } = await this.parse(DeviceInitCmd);
|
||||
|
||||
// Imports
|
||||
const { promisify } = await import('util');
|
||||
@ -119,25 +114,22 @@ export default class DeviceInitCmd extends Command {
|
||||
tmp.setGracefulCleanup();
|
||||
const { downloadOSImage } = await import('../../utils/cloud');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const Logger = await import('../../utils/logger');
|
||||
|
||||
const logger = await Command.getLogger();
|
||||
const logger = Logger.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get application and
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options.fleet ||
|
||||
(
|
||||
await (await import('../../utils/patterns')).selectApplication()
|
||||
).slug,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
const application = options.fleet
|
||||
? await getApplication(balena, options.fleet, {
|
||||
$select: ['id', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)) as ApplicationWithDeviceType;
|
||||
})
|
||||
: await (await import('../../utils/patterns')).selectApplication();
|
||||
|
||||
// Register new device
|
||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
||||
@ -163,7 +155,7 @@ export default class DeviceInitCmd extends Command {
|
||||
try {
|
||||
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
||||
await balena.models.device.remove(device.uuid);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore removal failures, and throw original error
|
||||
}
|
||||
throw e;
|
@ -15,28 +15,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { Command } from '@oclif/core';
|
||||
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';
|
||||
import type { Device, PineOptions } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
fleet?: string | null; // 'org/name' slug
|
||||
device_type?: string | null;
|
||||
}
|
||||
const devicesSelectFields = {
|
||||
$select: [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
],
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
interface FlagsDef {
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
export default class DeviceListCmd extends Command {
|
||||
public static aliases = ['devices'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
|
||||
@ -49,17 +51,14 @@ export default class DevicesCmd extends Command {
|
||||
${jsonInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
'$ balena devices --fleet MyFleet',
|
||||
'$ balena devices -f myorg/myfleet',
|
||||
'$ balena device list',
|
||||
'$ balena device list --fleet MyFleet',
|
||||
'$ balena device list -f myorg/myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
@ -67,39 +66,42 @@ export default class DevicesCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
const { flags: options } = await this.parse(DeviceListCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const devicesOptions = {
|
||||
...devicesSelectFields,
|
||||
...expandForAppName,
|
||||
$orderby: { device_name: 'asc' },
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
let devices;
|
||||
const devices = (
|
||||
await (async () => {
|
||||
if (options.fleet != null) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, options.fleet, {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
owns__device: devicesOptions,
|
||||
},
|
||||
});
|
||||
return application.owns__device;
|
||||
}
|
||||
|
||||
if (options.fleet != null) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, options.fleet);
|
||||
devices = (await balena.models.device.getAllByApplication(
|
||||
application.id,
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
} else {
|
||||
devices = (await balena.models.device.getAll(
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
}
|
||||
return await balena.pine.get({
|
||||
resource: 'device',
|
||||
options: devicesOptions,
|
||||
});
|
||||
})()
|
||||
).map((device) => ({
|
||||
...device,
|
||||
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
|
||||
fleet: device.belongs_to__application?.[0]?.slug || null,
|
||||
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
|
||||
device_type: device.is_of__device_type?.[0]?.slug || null,
|
||||
}));
|
||||
|
||||
devices = devices.map(function (device) {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.fleet = belongsToApplication?.[0]?.slug || 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 = [
|
||||
const fields: Array<keyof (typeof devices)[number]> = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
@ -15,23 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
disable: boolean;
|
||||
status: boolean;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string | number;
|
||||
}
|
||||
|
||||
export default class DeviceLocalModeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get or manage the local mode status for a device.
|
||||
@ -47,38 +33,33 @@ export default class DeviceLocalModeCmd extends Command {
|
||||
'$ balena device local-mode 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to manage',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device local-mode <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
enable: flags.boolean({
|
||||
public static flags = {
|
||||
enable: Flags.boolean({
|
||||
description: 'enable local mode',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: flags.boolean({
|
||||
disable: Flags.boolean({
|
||||
description: 'disable local mode',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: flags.boolean({
|
||||
status: Flags.boolean({
|
||||
description: 'output boolean indicating local mode status',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceLocalModeCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceLocalModeCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -15,28 +15,16 @@
|
||||
* 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 { LogMessage } from 'balena-sdk';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
|
||||
interface FlagsDef {
|
||||
'max-retry'?: number;
|
||||
tail?: boolean;
|
||||
service?: string[];
|
||||
system?: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
device: string;
|
||||
}
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import type { LogMessage } from 'balena-sdk';
|
||||
|
||||
const MAX_RETRY = 1000;
|
||||
|
||||
export default class LogsCmd extends Command {
|
||||
export default class DeviceLogsCmd extends Command {
|
||||
public static aliases = ['logs'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
public static description = stripIndent`
|
||||
Show device logs.
|
||||
|
||||
@ -56,70 +44,64 @@ export default class LogsCmd extends Command {
|
||||
Note: --service and --system flags must come after the device parameter, as per examples.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena logs 23c73a1',
|
||||
'$ balena logs 23c73a1 --tail',
|
||||
'$ balena device logs 23c73a1',
|
||||
'$ balena device logs 23c73a1 --tail',
|
||||
'',
|
||||
'$ balena logs 192.168.0.31',
|
||||
'$ balena logs 192.168.0.31 --service my-service',
|
||||
'$ balena logs 192.168.0.31 --service my-service-1 --service my-service-2',
|
||||
'$ balena device logs 192.168.0.31',
|
||||
'$ balena device logs 192.168.0.31 --service my-service',
|
||||
'$ balena device logs 192.168.0.31 --service my-service-1 --service my-service-2',
|
||||
'',
|
||||
'$ balena logs 23c73a1.local --system',
|
||||
'$ balena logs 23c73a1.local --system --service my-service',
|
||||
'$ balena device logs 23c73a1.local --system',
|
||||
'$ balena device logs 23c73a1.local --system --service my-service',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'device',
|
||||
public static args = {
|
||||
device: Args.string({
|
||||
description: 'device UUID, IP, or .local address',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'logs <device>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
'max-retry': flags.integer({
|
||||
public static flags = {
|
||||
'max-retry': Flags.integer({
|
||||
description: stripIndent`
|
||||
Maximum number of reconnection attempts on "connection lost" errors
|
||||
(use 0 to disable auto reconnection).`,
|
||||
}),
|
||||
tail: flags.boolean({
|
||||
tail: Flags.boolean({
|
||||
default: false,
|
||||
description: 'continuously stream output',
|
||||
char: 't',
|
||||
}),
|
||||
service: flags.string({
|
||||
service: Flags.string({
|
||||
description: stripIndent`
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system or other --service flags.`,
|
||||
char: 's',
|
||||
multiple: true,
|
||||
}),
|
||||
system: flags.boolean({
|
||||
system: Flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'Only show system logs. This can be used in combination with --service.',
|
||||
char: 'S',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
LogsCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceLogsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { serviceIdToName } = await import('../utils/cloud');
|
||||
const { serviceIdToName } = await import('../../utils/cloud');
|
||||
const { connectAndDisplayDeviceLogs, displayLogObject } = await import(
|
||||
'../utils/device/logs'
|
||||
'../../utils/device/logs'
|
||||
);
|
||||
const { validateIPAddress, validateDotLocalUrl } = await import(
|
||||
'../utils/validation'
|
||||
'../../utils/validation'
|
||||
);
|
||||
const Logger = await import('../utils/logger');
|
||||
const Logger = await import('../../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
|
||||
@ -148,13 +130,13 @@ export default class LogsCmd extends Command {
|
||||
validateDotLocalUrl(params.device)
|
||||
) {
|
||||
// Logs from local device
|
||||
const { DeviceAPI } = await import('../utils/device/api');
|
||||
const { DeviceAPI } = await import('../../utils/device/api');
|
||||
const deviceApi = new DeviceAPI(logger, params.device);
|
||||
logger.logDebug('Checking we can access device');
|
||||
try {
|
||||
await deviceApi.ping();
|
||||
} catch (e) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
} catch {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
||||
);
|
||||
@ -169,8 +151,9 @@ export default class LogsCmd extends Command {
|
||||
maxAttempts: 1 + (options['max-retry'] ?? MAX_RETRY),
|
||||
});
|
||||
} else {
|
||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
||||
// Logs from cloud
|
||||
await Command.checkLoggedIn();
|
||||
await checkLoggedIn();
|
||||
if (options.tail) {
|
||||
const logStream = await balena.logs.subscribe(params.device, {
|
||||
count: 100,
|
@ -15,36 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
DeviceType,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
type ExtendedDevice = PineTypedResult<
|
||||
Device,
|
||||
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
|
||||
> & {
|
||||
application_name?: string;
|
||||
};
|
||||
|
||||
interface FlagsDef {
|
||||
fleet?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceMoveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move one or more devices to another fleet.
|
||||
@ -63,61 +45,63 @@ export default class DeviceMoveCmd extends Command {
|
||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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> = {
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceMoveCmd,
|
||||
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
|
||||
const deviceOptions = {
|
||||
$select: 'belongs_to__application',
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'is_of__cpu_architecture',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
// TODO: Refacor once `device.get()` accepts an array of uuids`
|
||||
const devices = await Promise.all(
|
||||
deviceUuids.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, deviceOptions) as Promise<
|
||||
PineTypedResult<Device, typeof deviceOptions>
|
||||
>,
|
||||
),
|
||||
);
|
||||
return devices;
|
||||
}
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(DeviceMoveCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||
|
||||
// Split uuids string into array of uuids
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
|
||||
// Get devices
|
||||
const devices = await Promise.all(
|
||||
deviceUuids.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(
|
||||
uuid,
|
||||
expandForAppNameAndCpuArch,
|
||||
) as Promise<ExtendedDevice>,
|
||||
),
|
||||
);
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication = device.belongs_to__application;
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
}
|
||||
const devices = await this.getDevices(balena, deviceUuids);
|
||||
|
||||
// Disambiguate application
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
// Get destination application
|
||||
const application = options.fleet
|
||||
? await getApplication(balena, options.fleet)
|
||||
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
|
||||
: await this.interactivelySelectApplication(balena, devices);
|
||||
|
||||
// Move each device
|
||||
@ -134,9 +118,8 @@ export default class DeviceMoveCmd extends Command {
|
||||
|
||||
async interactivelySelectApplication(
|
||||
balena: BalenaSDK,
|
||||
devices: ExtendedDevice[],
|
||||
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
||||
) {
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
// deduplicate the slugs
|
||||
const deviceCpuArchs = Array.from(
|
||||
new Set(
|
||||
@ -146,46 +129,44 @@ export default class DeviceMoveCmd extends Command {
|
||||
),
|
||||
);
|
||||
|
||||
const deviceTypeOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
const allCpuArches = await balena.pine.get({
|
||||
resource: 'cpu_architecture',
|
||||
options: {
|
||||
$select: ['id', 'slug'],
|
||||
},
|
||||
} as const;
|
||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||
deviceTypeOptions,
|
||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||
});
|
||||
|
||||
const compatibleDeviceTypeSlugs = new Set(
|
||||
deviceTypes
|
||||
.filter((deviceType) => {
|
||||
const deviceTypeArch = getExpandedProp(
|
||||
deviceType.is_of__cpu_architecture,
|
||||
'slug',
|
||||
)!;
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
deviceTypeArch,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.slug),
|
||||
);
|
||||
const compatibleCpuArchIds = allCpuArches
|
||||
.filter((cpuArch) => {
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
cpuArch.slug,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.id);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
{
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (!compatibleDeviceTypeSlugs.size) {
|
||||
if (!compatibleCpuArchIds.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
@ -15,23 +15,15 @@
|
||||
* 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 { Flags, Args, Command } from '@oclif/core';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
device?: string; // device UUID
|
||||
dev?: string; // Alias for device.
|
||||
help: void;
|
||||
}
|
||||
export default class DeviceNoteCmd extends Command {
|
||||
public static aliases = ['notes'];
|
||||
public static deprecateAliases = true;
|
||||
|
||||
interface ArgsDef {
|
||||
note: string;
|
||||
}
|
||||
|
||||
export default class NoteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a device note.
|
||||
|
||||
@ -42,40 +34,30 @@ export default class NoteCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena note "My useful note" --device 7cf02a6',
|
||||
'$ cat note.txt | balena note --device 7cf02a6',
|
||||
'$ balena device note "My useful note" --device 7cf02a6',
|
||||
'$ cat note.txt | balena device note --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'note',
|
||||
public static args = {
|
||||
note: Args.string({
|
||||
description: 'note content',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'note <|note>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
device: { exclusive: ['dev'], ...cf.device },
|
||||
dev: flags.string({
|
||||
dev: Flags.string({
|
||||
exclusive: ['device'],
|
||||
hidden: true,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static readStdin = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
NoteCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceNoteCmd);
|
||||
|
||||
params.note = params.note || this.stdin;
|
||||
|
||||
if (params.note.length === 0) {
|
||||
if (params.note?.length === 0) {
|
||||
throw new ExpectedError('Missing note content');
|
||||
}
|
||||
|
||||
@ -88,6 +70,6 @@ export default class NoteCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.device.note(options.device!, params.note);
|
||||
return balena.models.device.setNote(options.device, params.note ?? '');
|
||||
}
|
||||
}
|
@ -15,23 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import type { Device } from 'balena-sdk';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
version?: string;
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
export default class DeviceOsUpdateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
@ -48,32 +37,34 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
'$ balena device os-update 23c73a1',
|
||||
'$ balena device os-update 23c73a1 --version 2.101.7',
|
||||
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
||||
'$ balena device os-update 23c73a1 --include-draft',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to update',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device os-update <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
version: flags.string({
|
||||
public static flags = {
|
||||
version: Flags.string({
|
||||
description: 'a balenaOS version',
|
||||
exclusive: ['include-draft'],
|
||||
}),
|
||||
'include-draft': Flags.boolean({
|
||||
description: 'include pre-release balenaOS versions',
|
||||
default: false,
|
||||
exclusive: ['version'],
|
||||
}),
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceOsUpdateCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceOsUpdateCmd);
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
@ -99,10 +90,25 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
let includeDraft = options['include-draft'];
|
||||
if (!includeDraft && options.version != null) {
|
||||
const bSemver = await import('balena-semver');
|
||||
const parsedVersion = bSemver.parse(options.version);
|
||||
// When the user provides a draft version, we need to pass `includeDraft`
|
||||
// to the os.getSupportedOsUpdateVersions() since w/o it the results
|
||||
// will for sure not include the user provided version and the command
|
||||
// would return a "not in the Host OS update targets" error.
|
||||
includeDraft =
|
||||
parsedVersion != null && parsedVersion.prerelease.length > 0;
|
||||
}
|
||||
|
||||
// Get supported OS update versions
|
||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||
is_of__device_type[0].slug,
|
||||
currentOsVersion,
|
||||
{
|
||||
includeDraft,
|
||||
},
|
||||
);
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
throw new ExpectedError(
|
||||
@ -113,33 +119,72 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
// Get target OS version
|
||||
let targetOsVersion = options.version;
|
||||
if (targetOsVersion != null) {
|
||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||
targetOsVersion = normalizeOsVersion(targetOsVersion);
|
||||
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
||||
throw new ExpectedError(
|
||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const choices = await Promise.all(
|
||||
hupVersionInfo.versions.map(async (version) => {
|
||||
const takeoverRequired =
|
||||
(await sdk.models.os.getOsUpdateType(
|
||||
getExpandedProp(is_of__device_type, 'slug')!,
|
||||
currentOsVersion,
|
||||
version,
|
||||
)) === 'takeover';
|
||||
|
||||
return {
|
||||
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
|
||||
value: version,
|
||||
};
|
||||
}),
|
||||
);
|
||||
targetOsVersion = await getCliForm().ask({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map((version) => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
const takeoverRequired =
|
||||
(await sdk.models.os.getOsUpdateType(
|
||||
getExpandedProp(is_of__device_type, 'slug')!,
|
||||
currentOsVersion,
|
||||
targetOsVersion,
|
||||
)) === 'takeover';
|
||||
const patterns = await import('../../utils/patterns');
|
||||
// Warn the user if the update requires a takeover
|
||||
if (takeoverRequired) {
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
|
||||
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
|
||||
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
|
||||
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
|
||||
`,
|
||||
);
|
||||
}
|
||||
// Confirm and start update
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||
);
|
||||
|
||||
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
|
||||
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
|
||||
await sdk.models.device
|
||||
.startOsUpdate(uuid, targetOsVersion, {
|
||||
runDetached: true,
|
||||
})
|
||||
.then(() => {
|
||||
console.log(
|
||||
`The balena OS update has started. You can keep track of the progress via the dashboard.\n` +
|
||||
`To open the dashboard page related to a device via the CLI, you can use \`balena device UUID --view\``,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to start OS update for device ${uuid}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
@ -15,22 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
releaseToPinTo?: string;
|
||||
}
|
||||
|
||||
export default class DevicePinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Pin a device to a release.
|
||||
@ -44,34 +32,26 @@ export default class DevicePinCmd extends Command {
|
||||
'$ balena device pin 7cf02a6 91165e5',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to pin to a release',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseToPinTo',
|
||||
}),
|
||||
releaseToPinTo: Args.string({
|
||||
description: 'the commit of the release for the device to get pinned to',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device pin <uuid> [releaseToPinTo]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
|
||||
const { args: params } = await this.parse(DevicePinCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device = await balena.models.device.get(params.uuid, {
|
||||
$expand: {
|
||||
should_be_running__release: {
|
||||
is_pinned_on__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
belongs_to__application: {
|
||||
@ -81,7 +61,7 @@ export default class DevicePinCmd extends Command {
|
||||
});
|
||||
|
||||
const pinnedRelease = getExpandedProp(
|
||||
device.should_be_running__release,
|
||||
device.is_pinned_on__release,
|
||||
'commit',
|
||||
);
|
||||
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
||||
@ -94,7 +74,7 @@ export default class DevicePinCmd extends Command {
|
||||
pinnedRelease
|
||||
? `This device is currently pinned to ${pinnedRelease}.`
|
||||
: 'This device is not currently pinned to any release.'
|
||||
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
|
||||
} \n\nTo see a list of all releases this device can be pinned to, run \`balena release list ${appSlug}\`.`,
|
||||
);
|
||||
} else {
|
||||
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
@ -15,24 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
disable: boolean;
|
||||
status: boolean;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DevicePublicUrlCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get or manage the public URL for a device.
|
||||
@ -49,38 +35,33 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
'$ balena device public-url 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to manage',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device public-url <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
enable: flags.boolean({
|
||||
public static flags = {
|
||||
enable: Flags.boolean({
|
||||
description: 'enable the public URL',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: flags.boolean({
|
||||
disable: Flags.boolean({
|
||||
description: 'disable the public URL',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: flags.boolean({
|
||||
status: Flags.boolean({
|
||||
description: 'determine if public URL is enabled',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DevicePublicUrlCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DevicePublicUrlCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -15,20 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DevicePurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from a device.
|
||||
@ -44,24 +33,17 @@ export default class DevicePurgeCmd extends Command {
|
||||
'$ balena device purge 55d43b3,23c73a1',
|
||||
];
|
||||
|
||||
public static usage = 'device purge <uuid>';
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
||||
const { args: params } = await this.parse(DevicePurgeCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user