mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-25 02:47:55 +00:00
Compare commits
728 Commits
custom-doc
...
manual-hel
Author | SHA1 | Date | |
---|---|---|---|
e10d7816a9 | |||
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 | |||
d1a0660a3d | |||
ee1987f188 | |||
39e9997d9e | |||
97b8c75043 | |||
7cb8349f29 | |||
6063f4c776 | |||
4899d545f1 | |||
115bf6433d | |||
e5ce1ade89 | |||
9c4174ea8a | |||
cf16957195 | |||
4de369ff95 | |||
ac3ebff8ee | |||
76b01d92d3 | |||
19144163ee | |||
535ffccbad | |||
6f5ada9692 | |||
1c7d9255ae | |||
807e6ea2ad | |||
c76f019fd0 | |||
3c2c925eed | |||
14b54be15e | |||
7fb82f7447 | |||
4a5d44a0f1 | |||
1cba0284df | |||
6e4fe229bf | |||
7033075900 | |||
ded268ff3c | |||
a366f0b7eb | |||
507c8a1bfd | |||
1fb46bfa5d | |||
2e115968d5 | |||
83020797b0 | |||
0c4647e980 | |||
a20d2a04a8 | |||
57b0dccc7d | |||
d1e3bdf29a | |||
bdf7fedd7a | |||
c163662f4a | |||
a2823fd3ec | |||
d717352b84 | |||
e46902e683 | |||
e96ef6697e | |||
6f54197b7b | |||
34b4ac2d9f | |||
f99244603a | |||
523c0af0fb | |||
2206b475c6 | |||
a117dc0382 | |||
cf3e8ff909 | |||
36d1af1e33 | |||
18f83092fe | |||
ee3c796787 | |||
934c3ddf38 | |||
66e6daf78c | |||
97eb107de4 | |||
def205f1fb | |||
5c8f78678b | |||
769f1ca5b4 | |||
cb26a736fc | |||
d28847d5aa | |||
c0902bb119 | |||
26aae0afab | |||
5f3cf75c1a | |||
8a7fbdb55d | |||
b260f80bcc | |||
9ec37975f3 | |||
73c487c2f5 | |||
3cb35ea318 | |||
efe6fd22ce | |||
6ee8d8a899 | |||
c735f13636 | |||
edb0fdc3c1 | |||
14a07ac7f7 | |||
264cd94be5 | |||
2664f4e7fb | |||
3ce2653881 | |||
719860366f | |||
21ded85c7a | |||
c91f67d27e | |||
18eedfec7f | |||
1fe0480a8a | |||
c7f56d92dd | |||
a92f58134f | |||
cc6a8ef76e | |||
88f4a3d88e | |||
f6d668684a | |||
be7c0dc897 | |||
566b7f97e0 | |||
f55dd81a19 | |||
dba5349390 | |||
6a8dfcc664 | |||
59e35d866f | |||
9235c928f1 | |||
3d88f0144a | |||
a6b461ba91 | |||
b96da951db | |||
8235cead07 | |||
30b9d9141d | |||
03b41d9989 | |||
aab3af2153 | |||
600457de61 | |||
17db857e10 | |||
eb45ae2a30 | |||
2eaf70bff3 | |||
226f45f732 | |||
c4990f3a26 | |||
0195a3b18c | |||
3d90aeb122 | |||
0571039bfe | |||
ee668a4c5c | |||
ead4dbfab1 | |||
0b498d09df | |||
2b2c40c22d |
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/completion/*
|
||||||
|
/bin/*
|
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.dev.json',
|
||||||
|
},
|
||||||
|
root: true,
|
||||||
|
rules: {
|
||||||
|
ignoreDefinitionFiles: 0,
|
||||||
|
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
},
|
||||||
|
};
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -4,6 +4,10 @@
|
|||||||
*.* -eol
|
*.* -eol
|
||||||
|
|
||||||
*.sh text eol=lf
|
*.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
|
# 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
|
docs/balena-cli.md text eol=lf
|
||||||
|
143
.github/actions/publish/action.yml
vendored
Normal file
143
.github/actions/publish/action.yml
vendored
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
name: package and draft GitHub release
|
||||||
|
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||||
|
inputs:
|
||||||
|
json:
|
||||||
|
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||||
|
required: true
|
||||||
|
variables:
|
||||||
|
description: 'JSON stringified object containing all the variables from the calling workflow'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
# --- custom environment
|
||||||
|
XCODE_APP_LOADER_EMAIL:
|
||||||
|
type: string
|
||||||
|
default: 'accounts+apple@balena.io'
|
||||||
|
NODE_VERSION:
|
||||||
|
type: string
|
||||||
|
default: '20.x'
|
||||||
|
VERBOSE:
|
||||||
|
type: string
|
||||||
|
default: 'true'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Download custom source artifact
|
||||||
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
|
with:
|
||||||
|
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
path: ${{ runner.temp }}
|
||||||
|
|
||||||
|
- name: Extract custom source artifact
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: .
|
||||||
|
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.NODE_VERSION }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Set up Python 3.11
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Install additional tools
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
choco install yq
|
||||||
|
|
||||||
|
- name: Install additional tools
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
brew install coreutils
|
||||||
|
|
||||||
|
# https://www.electron.build/code-signing.html
|
||||||
|
# https://github.com/Apple-Actions/import-codesign-certs
|
||||||
|
- name: Import Apple code signing certificate
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||||
|
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Import Windows code signing certificate
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:SM_CLIENT_CERT_FILE_B64
|
||||||
|
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
|
||||||
|
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||||
|
env:
|
||||||
|
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
|
||||||
|
|
||||||
|
# https://github.com/product-os/scripts/tree/master/shared
|
||||||
|
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||||
|
- name: Package release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -ea
|
||||||
|
|
||||||
|
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||||
|
|
||||||
|
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
if [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||||
|
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||||
|
CSC_KEYCHAIN=signing_temp
|
||||||
|
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||||
|
|
||||||
|
elif [[ $runner_os =~ windows|win ]]; then
|
||||||
|
SM_HOST=${{ fromJSON(inputs.secrets).SM_HOST }}
|
||||||
|
SM_API_KEY=${{ fromJSON(inputs.secrets).SM_API_KEY }}
|
||||||
|
SM_CLIENT_CERT_FILE='${{ runner.temp }}\Certificate_pkcs12.p12'
|
||||||
|
SM_CLIENT_CERT_PASSWORD=${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
|
||||||
|
SM_CODE_SIGNING_CERT_SHA1_HASH=${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||||
|
|
||||||
|
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
|
||||||
|
-H "x-api-key:$SM_API_KEY" \
|
||||||
|
-o smtools-windows-x64.msi
|
||||||
|
msiexec -i smtools-windows-x64.msi -qn
|
||||||
|
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||||
|
smksp_registrar.exe list
|
||||||
|
smctl.exe keypair ls
|
||||||
|
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||||
|
smksp_cert_sync.exe
|
||||||
|
|
||||||
|
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
||||||
|
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm run package
|
||||||
|
|
||||||
|
find dist -type f -maxdepth 1
|
||||||
|
|
||||||
|
env:
|
||||||
|
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||||
|
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
# https://docs.digicert.com/es/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html
|
||||||
|
TIMESTAMP_SERVER: http://timestamp.digicert.com
|
||||||
|
# Apple notarization (automation/build-bin.ts)
|
||||||
|
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||||
|
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||||
|
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||||
|
with:
|
||||||
|
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||||
|
path: dist
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
65
.github/actions/test/action.yml
vendored
Normal file
65
.github/actions/test/action.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: test release
|
||||||
|
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||||
|
inputs:
|
||||||
|
json:
|
||||||
|
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||||
|
required: true
|
||||||
|
variables:
|
||||||
|
description: "JSON stringified object containing all the variables from the calling workflow"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
# --- custom environment
|
||||||
|
NODE_VERSION:
|
||||||
|
type: string
|
||||||
|
default: '20.x'
|
||||||
|
VERBOSE:
|
||||||
|
type: string
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.NODE_VERSION }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Set up Python 3.11
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Test release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -ea
|
||||||
|
|
||||||
|
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||||
|
|
||||||
|
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm i
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
npm run test:core
|
||||||
|
|
||||||
|
- name: Compress custom source
|
||||||
|
shell: pwsh
|
||||||
|
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||||
|
|
||||||
|
- name: Upload custom artifact
|
||||||
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||||
|
with:
|
||||||
|
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
path: ${{ runner.temp }}/custom.tgz
|
||||||
|
retention-days: 1
|
4
.github/renovate.json
vendored
Normal file
4
.github/renovate.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": ["github>balena-io/renovate-config"],
|
||||||
|
"postUpdateOptions": ["npmDedupe"]
|
||||||
|
}
|
45
.github/workflows/flowzone.yml
vendored
Normal file
45
.github/workflows/flowzone.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Flowzone
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, closed]
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, closed]
|
||||||
|
branches: [main, master]
|
||||||
|
jobs:
|
||||||
|
flowzone:
|
||||||
|
name: Flowzone
|
||||||
|
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||||
|
# prevent duplicate workflow executions for pull_request and pull_request_target
|
||||||
|
if: |
|
||||||
|
(
|
||||||
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||||
|
github.event_name == 'pull_request'
|
||||||
|
) || (
|
||||||
|
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||||
|
github.event_name == 'pull_request_target'
|
||||||
|
)
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
custom_test_matrix: >
|
||||||
|
{
|
||||||
|
"os": [
|
||||||
|
["self-hosted", "X64"],
|
||||||
|
["self-hosted", "ARM64"],
|
||||||
|
["macos-12"],
|
||||||
|
["windows-2019"],
|
||||||
|
["macos-latest-xlarge"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
custom_publish_matrix: >
|
||||||
|
{
|
||||||
|
"os": [
|
||||||
|
["self-hosted", "X64"],
|
||||||
|
["self-hosted", "ARM64"],
|
||||||
|
["macos-12"],
|
||||||
|
["windows-2019"],
|
||||||
|
["macos-latest-xlarge"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
github_prerelease: false
|
||||||
|
restrict_custom_actions: false
|
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
|
20
.resinci.yml
20
.resinci.yml
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
npm:
|
|
||||||
platforms:
|
|
||||||
- name: linux
|
|
||||||
os: ubuntu
|
|
||||||
architecture: x86_64
|
|
||||||
node_versions:
|
|
||||||
- "12"
|
|
||||||
- "14"
|
|
||||||
##
|
|
||||||
## Temporarily skip Alpine tests until the following issues are resolved:
|
|
||||||
## * https://github.com/concourse/concourse/issues/7905
|
|
||||||
## * https://github.com/product-os/balena-concourse/issues/631
|
|
||||||
##
|
|
||||||
# - name: linux
|
|
||||||
# os: alpine
|
|
||||||
# architecture: x86_64
|
|
||||||
# node_versions:
|
|
||||||
# - "12"
|
|
||||||
# - "14"
|
|
File diff suppressed because it is too large
Load Diff
3427
CHANGELOG.md
3427
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
|
* [Selected
|
||||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||||
of the README file.
|
of the README file.
|
||||||
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
|
* The CLI's command documentation in source code (`src/commands/` folder), for example:
|
||||||
* `lib/commands/push.ts`
|
* `src/commands/push.ts`
|
||||||
* `lib/commands/env/add.ts`
|
* `src/commands/env/add.ts`
|
||||||
|
|
||||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||||
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
||||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
[`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.
|
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||||
|
|
||||||
## Patches folder
|
## 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
|
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](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
|
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
|
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||||
`node_modules` folder. In this situation:
|
`node_modules` folder. In this situation:
|
||||||
|
@ -40,7 +40,7 @@ By default, the CLI is installed to the following folders:
|
|||||||
OS | Folders
|
OS | Folders
|
||||||
--- | ---
|
--- | ---
|
||||||
Windows: | `C:\Program Files\balena-cli\`
|
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
|
## 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
|
The npm installation involves building native (platform-specific) binary modules, which require
|
||||||
some development tools to be installed first, as follows.
|
some development tools to be installed first, as follows.
|
||||||
|
|
||||||
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
> **The balena CLI currently requires Node.js version ^20.6.0**
|
||||||
> **Versions 13 and later are not yet fully supported.**
|
> **Versions 21 and later are not yet fully supported.**
|
||||||
|
|
||||||
### Install development tools
|
### 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++
|
$ 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
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 12
|
$ nvm install 20
|
||||||
```
|
```
|
||||||
|
|
||||||
The `curl` command line above uses
|
The `curl` command line above uses
|
||||||
@ -106,15 +106,15 @@ recommended.
|
|||||||
```sh
|
```sh
|
||||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 12
|
$ nvm install 20
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Windows** (not WSL)
|
#### **Windows** (not WSL)
|
||||||
|
|
||||||
Install:
|
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
|
* 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)
|
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||||
instead.
|
instead.
|
||||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||||
|
@ -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:
|
To uninstall it, run the following command on a terminal prompt:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
sudo /usr/local/lib/balena-cli/bin/uninstall
|
sudo /usr/local/src/balena-cli/bin/uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
## Additional Dependencies
|
## Additional Dependencies
|
||||||
|
@ -15,21 +15,20 @@
|
|||||||
* limitations under the License.
|
* 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 archiver from 'archiver';
|
||||||
import * as Bluebird from 'bluebird';
|
import { exec, execFile } from 'child_process';
|
||||||
import { execFile } from 'child_process';
|
|
||||||
import * as filehound from 'filehound';
|
import * as filehound from 'filehound';
|
||||||
import { Stats } from 'fs';
|
import type { Stats } from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as klaw from 'klaw';
|
import * as klaw from 'klaw';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import { notarize } from '@electron/notarize';
|
||||||
|
|
||||||
import { stripIndent } from '../build/utils/lazy';
|
import { stripIndent } from '../build/utils/lazy';
|
||||||
import {
|
import {
|
||||||
@ -41,12 +40,12 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const rimrafAsync = promisify(rimraf);
|
||||||
|
|
||||||
export const packageJSON = loadPackageJson();
|
export const packageJSON = loadPackageJson();
|
||||||
export const version = 'v' + packageJSON.version;
|
export const version = 'v' + packageJSON.version;
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
const MSYS2_BASH =
|
|
||||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
|
||||||
|
|
||||||
function dPath(...paths: string[]) {
|
function dPath(...paths: string[]) {
|
||||||
return path.join(ROOT, 'dist', ...paths);
|
return path.join(ROOT, 'dist', ...paths);
|
||||||
@ -62,9 +61,13 @@ const standaloneZips: PathByPlatform = {
|
|||||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const oclifInstallers: PathByPlatform = {
|
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
|
||||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
||||||
win32: dPath('win32', `balena-${version}-${arch}.exe`),
|
const sha = stdout.trim();
|
||||||
|
return {
|
||||||
|
darwin: dPath('macos', `balena-${version}-${sha}-${arch}.pkg`),
|
||||||
|
win32: dPath('win32', `balena-${version}-${sha}-${arch}.exe`),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const renamedOclifInstallers: PathByPlatform = {
|
const renamedOclifInstallers: PathByPlatform = {
|
||||||
@ -89,7 +92,7 @@ async function diffPkgOutput(pkgOut: string) {
|
|||||||
'tests',
|
'tests',
|
||||||
'test-data',
|
'test-data',
|
||||||
'pkg',
|
'pkg',
|
||||||
`expected-warnings-${process.platform}.txt`,
|
`expected-warnings-${process.platform}-${arch}.txt`,
|
||||||
);
|
);
|
||||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||||
const ignoreStartsWith = [
|
const ignoreStartsWith = [
|
||||||
@ -157,7 +160,7 @@ ${sep}
|
|||||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
* messages (stdout and stderr) in order to call diffPkgOutput().
|
||||||
*/
|
*/
|
||||||
async function execPkg(...args: any[]) {
|
async function execPkg(...args: any[]) {
|
||||||
const { exec: pkgExec } = await import('pkg');
|
const { exec: pkgExec } = await import('@yao-pkg/pkg');
|
||||||
const outTap = new StdOutTap(true);
|
const outTap = new StdOutTap(true);
|
||||||
try {
|
try {
|
||||||
outTap.tap();
|
outTap.tap();
|
||||||
@ -182,9 +185,18 @@ async function execPkg(...args: any[]) {
|
|||||||
* to be directly executed from inside another binary executable.)
|
* to be directly executed from inside another binary executable.)
|
||||||
*/
|
*/
|
||||||
async function buildPkg() {
|
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 = [
|
const args = [
|
||||||
'--target',
|
'--targets',
|
||||||
'host',
|
targets,
|
||||||
'--output',
|
'--output',
|
||||||
'build-bin/balena',
|
'build-bin/balena',
|
||||||
'package.json',
|
'package.json',
|
||||||
@ -199,7 +211,6 @@ async function buildPkg() {
|
|||||||
const paths: Array<[string, string[], string[]]> = [
|
const paths: Array<[string, string[], string[]]> = [
|
||||||
// [platform, [source path], [destination path]]
|
// [platform, [source path], [destination path]]
|
||||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
|
||||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||||
];
|
];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -315,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...');
|
console.log('Deleting unneeded zip files...');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
klaw('node_modules/')
|
klaw('node_modules/')
|
||||||
@ -415,6 +430,7 @@ export async function buildStandaloneZip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renameInstallerFiles() {
|
async function renameInstallerFiles() {
|
||||||
|
const oclifInstallers = await getOclifInstallersOriginalNames();
|
||||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
oclifInstallers[process.platform],
|
oclifInstallers[process.platform],
|
||||||
@ -425,20 +441,30 @@ async function renameInstallerFiles() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||||
*/
|
*/
|
||||||
async function signWindowsInstaller() {
|
async function signWindowsInstaller() {
|
||||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
|
||||||
const exeName = renamedOclifInstallers[process.platform];
|
const exeName = renamedOclifInstallers[process.platform];
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
await execFileAsync(MSYS2_BASH, [
|
// trust ...
|
||||||
'sign-exe.sh',
|
await execFileAsync('signtool.exe', [
|
||||||
'-f',
|
'sign',
|
||||||
exeName,
|
'-sha1',
|
||||||
|
process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
|
||||||
|
'-tr',
|
||||||
|
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||||
|
'-td',
|
||||||
|
'SHA256',
|
||||||
|
'-fd',
|
||||||
|
'SHA256',
|
||||||
'-d',
|
'-d',
|
||||||
`balena-cli ${version}`,
|
`balena-cli ${version}`,
|
||||||
|
exeName,
|
||||||
]);
|
]);
|
||||||
|
// ... but verify
|
||||||
|
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
'Skipping installer signing step because CSC_* env vars are not set',
|
'Skipping installer signing step because CSC_* env vars are not set',
|
||||||
@ -450,14 +476,20 @@ async function signWindowsInstaller() {
|
|||||||
* Wait for Apple Installer Notarization to continue
|
* Wait for Apple Installer Notarization to continue
|
||||||
*/
|
*/
|
||||||
async function notarizeMacInstaller(): Promise<void> {
|
async function notarizeMacInstaller(): Promise<void> {
|
||||||
const appleId = 'accounts+apple@balena.io';
|
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
|
||||||
const { notarize } = await import('electron-notarize');
|
const appleId =
|
||||||
await notarize({
|
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||||
appBundleId: 'io.balena.etcher',
|
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||||
appPath: renamedOclifInstallers.darwin,
|
|
||||||
appleId,
|
if (appleIdPassword && teamId) {
|
||||||
appleIdPassword: '@keychain:CLI_PASSWORD',
|
await notarize({
|
||||||
});
|
tool: 'notarytool',
|
||||||
|
teamId,
|
||||||
|
appPath: renamedOclifInstallers.darwin,
|
||||||
|
appleId,
|
||||||
|
appleIdPassword,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -471,9 +503,10 @@ export async function buildOclifInstaller() {
|
|||||||
let packOpts = ['-r', ROOT];
|
let packOpts = ['-r', ROOT];
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
packOS = 'macos';
|
packOS = 'macos';
|
||||||
|
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
packOS = 'win';
|
packOS = 'win';
|
||||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
packOpts = packOpts.concat('--targets', 'win32-x64');
|
||||||
}
|
}
|
||||||
if (packOS) {
|
if (packOS) {
|
||||||
console.log(`Building oclif installer for CLI ${version}`);
|
console.log(`Building oclif installer for CLI ${version}`);
|
||||||
@ -484,17 +517,14 @@ export async function buildOclifInstaller() {
|
|||||||
}
|
}
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
console.log(`rimraf(${dir})`);
|
console.log(`rimraf(${dir})`);
|
||||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
await rimrafAsync(dir);
|
||||||
}
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
console.log('Signing files for notarization...');
|
|
||||||
await signFilesForNotarization();
|
|
||||||
}
|
}
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
|
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
await oclifRun([packCmd].concat(...packOpts));
|
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||||
|
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||||
await renameInstallerFiles();
|
await renameInstallerFiles();
|
||||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||||
// The macOS installer is automatically signed by oclif (which runs the
|
// The macOS installer is automatically signed by oclif (which runs the
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MarkdownFileParser } from './utils';
|
import { MarkdownFileParser } from './utils';
|
||||||
|
import { GlobSync } from 'glob';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the skeleton of CLI documentation/reference web page at:
|
* This is the skeleton of CLI documentation/reference web page at:
|
||||||
@ -24,173 +25,117 @@ import { MarkdownFileParser } from './utils';
|
|||||||
*
|
*
|
||||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||||
* some content to this object.
|
* 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',
|
interface Category {
|
||||||
introduction: '',
|
title: string;
|
||||||
categories: [
|
files: string[];
|
||||||
{
|
}
|
||||||
title: 'API keys',
|
|
||||||
files: ['build/commands/api-key/generate.js'],
|
interface Documentation {
|
||||||
},
|
title: string;
|
||||||
{
|
introduction: string;
|
||||||
title: 'Fleet',
|
categories: Category[];
|
||||||
files: [
|
}
|
||||||
'build/commands/fleets.js',
|
|
||||||
'build/commands/fleet/index.js',
|
// Mapping folders names to custom headings in the docs
|
||||||
'build/commands/fleet/create.js',
|
const commandHeadings: { [key: string]: string } = {
|
||||||
'build/commands/fleet/purge.js',
|
'api-key': 'API Keys',
|
||||||
'build/commands/fleet/rename.js',
|
'api-keys': 'API Keys',
|
||||||
'build/commands/fleet/restart.js',
|
login: 'Authentication',
|
||||||
'build/commands/fleet/rm.js',
|
whoami: 'Authentication',
|
||||||
],
|
logout: 'Authentication',
|
||||||
},
|
env: 'Environment Variables',
|
||||||
{
|
envs: 'Environment Variables',
|
||||||
title: 'Authentication',
|
help: 'Help and Version',
|
||||||
files: [
|
key: 'SSH Keys',
|
||||||
'build/commands/login.js',
|
keys: 'SSH Keys',
|
||||||
'build/commands/logout.js',
|
orgs: 'Organizations',
|
||||||
'build/commands/whoami.js',
|
os: 'OS',
|
||||||
],
|
util: 'Utilities',
|
||||||
},
|
ssh: 'Network',
|
||||||
{
|
scan: 'Network',
|
||||||
title: 'Device',
|
tunnel: 'Network',
|
||||||
files: [
|
build: 'Deploy',
|
||||||
'build/commands/devices/index.js',
|
join: 'Platform',
|
||||||
'build/commands/devices/supported.js',
|
leave: 'Platform',
|
||||||
'build/commands/device/index.js',
|
app: 'Apps',
|
||||||
'build/commands/device/deactivate.js',
|
block: 'Blocks',
|
||||||
'build/commands/device/identify.js',
|
device: 'Devices',
|
||||||
'build/commands/device/init.js',
|
fleet: 'Fleets',
|
||||||
'build/commands/device/local-mode.js',
|
release: 'Releases',
|
||||||
'build/commands/device/move.js',
|
tag: 'Tags',
|
||||||
'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'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
* Modify and return the `capitanoDoc` object above in order to generate the
|
||||||
* CLI documentation/reference web page at:
|
* CLI documentation at docs/balena-cli.md
|
||||||
* https://www.balena.io/docs/reference/cli/
|
|
||||||
*
|
*
|
||||||
* This function parses the README.md file to extract relevant sections
|
* This function parses the README.md file to extract relevant sections
|
||||||
* for the documentation web page.
|
* for the documentation web page.
|
||||||
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Command as OclifCommandClass } from '@oclif/command';
|
import type { Command as OclifCommandClass } from '@oclif/core';
|
||||||
|
|
||||||
type OclifCommand = typeof OclifCommandClass;
|
type OclifCommand = typeof OclifCommandClass;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export interface Document {
|
|||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
title: string;
|
title: string;
|
||||||
commands: OclifCommand[];
|
commands: Array<OclifCommand & { name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OclifCommand };
|
export { OclifCommand };
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCapitanoDoc } from './capitanodoc';
|
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 * as markdown from './markdown';
|
||||||
import { stripIndent } from '../../lib/utils/lazy';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the markdown document (as a string) for the CLI documentation
|
* 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) {
|
for (const jsFilename of commandCategory.files) {
|
||||||
category.commands.push(...importOclifCommands(jsFilename));
|
category.commands.push(await importOclifCommands(jsFilename));
|
||||||
}
|
}
|
||||||
result.categories.push(category);
|
result.categories.push(category);
|
||||||
}
|
}
|
||||||
@ -47,49 +46,23 @@ export async function renderMarkdown(): Promise<string> {
|
|||||||
return markdown.render(result);
|
return markdown.render(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help is now managed via a plugin
|
async function importOclifCommands(jsFilename: string) {
|
||||||
// This fake command allows capitanodoc to include help in docs
|
const command = (await import(path.join(process.cwd(), jsFilename)))
|
||||||
class FakeHelpCommand {
|
.default as OclifCommand;
|
||||||
description = stripIndent`
|
|
||||||
List balena commands, or get detailed help for a specific command.
|
|
||||||
|
|
||||||
List balena commands, or get detailed help for a specific command.
|
return {
|
||||||
`;
|
...command,
|
||||||
|
// build/commands/device/index.js -> device
|
||||||
examples = [
|
// build/commands/device/list.js -> device list
|
||||||
'$ balena help',
|
name: jsFilename
|
||||||
'$ balena help login',
|
.split('/')
|
||||||
'$ balena help os download',
|
.slice(2)
|
||||||
];
|
.join(' ')
|
||||||
|
.split('.')
|
||||||
args = [
|
.slice(0, 1)
|
||||||
{
|
.join(' ')
|
||||||
name: 'command',
|
.split(' index')[0],
|
||||||
description: 'command to show help for',
|
} as Category['commands'][0];
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,5 +78,5 @@ async function printMarkdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
printMarkdown();
|
printMarkdown();
|
||||||
|
@ -14,16 +14,25 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flagUsages } from '@oclif/parser';
|
import { Parser } from '@oclif/core';
|
||||||
import * as ent from 'ent';
|
import * as ent from 'ent';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
import { getManualSortCompareFunction } from '../../src/utils/helpers';
|
||||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
import { capitanoizeOclifUsage } from '../../src/utils/oclif-utils';
|
||||||
import { Category, Document, OclifCommand } from './doc-types';
|
import type { Category, Document, OclifCommand } from './doc-types';
|
||||||
|
|
||||||
function renderOclifCommand(command: OclifCommand): string[] {
|
function renderOclifCommand(command: Category['commands'][0]): string[] {
|
||||||
const result = [`## ${ent.encode(command.usage || '')}`];
|
const result = [`## ${ent.encode(command.name || '')}`];
|
||||||
|
if (command.aliases?.length) {
|
||||||
|
result.push('### Aliases');
|
||||||
|
result.push(command.aliases.map((alias) => `- \`${alias}\``).join('\n'));
|
||||||
|
result.push(
|
||||||
|
`\nTo use one of the aliases, replace \`${command.name}\` with the alias.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push('### Description');
|
||||||
const description = (command.description || '')
|
const description = (command.description || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.slice(1) // remove the first line, which oclif uses as help header
|
.slice(1) // remove the first line, which oclif uses as help header
|
||||||
@ -37,8 +46,8 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
|||||||
|
|
||||||
if (!_.isEmpty(command.args)) {
|
if (!_.isEmpty(command.args)) {
|
||||||
result.push('### Arguments');
|
result.push('### Arguments');
|
||||||
for (const arg of command.args!) {
|
for (const [name, arg] of Object.entries(command.args!)) {
|
||||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +58,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
flag.name = name;
|
flag.name = name;
|
||||||
const flagUsage = flagUsages([flag])
|
const flagUsage = Parser.flagUsages([flag])
|
||||||
.map(([usage, _description]) => usage)
|
.map(([usage, _description]) => usage)
|
||||||
.join()
|
.join()
|
||||||
.trim();
|
.trim();
|
||||||
@ -80,7 +89,7 @@ function renderToc(categories: Category[]): string[] {
|
|||||||
result.push(
|
result.push(
|
||||||
category.commands
|
category.commands
|
||||||
.map((command) => {
|
.map((command) => {
|
||||||
const signature = capitanoizeOclifUsage(command.usage);
|
const signature = capitanoizeOclifUsage(command.name);
|
||||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||||
})
|
})
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
|
@ -15,41 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { OptionDefinition } from 'capitano';
|
|
||||||
import * as ent from 'ent';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as readline from 'readline';
|
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 {
|
export class MarkdownFileParser {
|
||||||
constructor(public mdFilePath: string) {}
|
constructor(public mdFilePath: string) {}
|
||||||
|
|
||||||
|
@ -15,11 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const stripIndent = require('common-tags/lib/stripIndent');
|
// eslint-disable-next-line no-restricted-imports
|
||||||
const _ = require('lodash');
|
import { stripIndent } from 'common-tags';
|
||||||
const { promises: fs } = require('fs');
|
import * as _ from 'lodash';
|
||||||
const path = require('path');
|
import { promises as fs } from 'fs';
|
||||||
const simplegit = require('simple-git/promise');
|
import * as path from 'path';
|
||||||
|
import { simpleGit } from 'simple-git';
|
||||||
|
|
||||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
|
|||||||
* using `touch`.
|
* using `touch`.
|
||||||
*/
|
*/
|
||||||
async function checkBuildTimestamps() {
|
async function checkBuildTimestamps() {
|
||||||
const git = simplegit(ROOT);
|
const git = simpleGit(ROOT);
|
||||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||||
const [docStat, gitStatus] = await Promise.all([
|
const [docStat, gitStatus] = await Promise.all([
|
||||||
fs.stat(docFile),
|
fs.stat(docFile),
|
||||||
@ -42,8 +43,8 @@ async function checkBuildTimestamps() {
|
|||||||
...gitStatus.staged,
|
...gitStatus.staged,
|
||||||
...gitStatus.renamed.map((o) => o.to),
|
...gitStatus.renamed.map((o) => o.to),
|
||||||
])
|
])
|
||||||
// select only staged files that start with lib/ or typings/
|
// select only staged files that start with src/ or typings/
|
||||||
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
.filter((f) => f.match(/^(src|typings)[/\\]/))
|
||||||
.map((f) => path.join(ROOT, f));
|
.map((f) => path.join(ROOT, f));
|
||||||
|
|
||||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||||
@ -81,4 +82,5 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
run();
|
run();
|
@ -23,8 +23,8 @@ function parseSemver(version) {
|
|||||||
* @param {string} v2
|
* @param {string} v2
|
||||||
*/
|
*/
|
||||||
function semverGte(v1, v2) {
|
function semverGte(v1, v2) {
|
||||||
let v1Array = parseSemver(v1);
|
const v1Array = parseSemver(v1);
|
||||||
let v2Array = parseSemver(v2);
|
const v2Array = parseSemver(v2);
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
if (v1Array[i] < v2Array[i]) {
|
if (v1Array[i] < v2Array[i]) {
|
||||||
return false;
|
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,
|
buildOclifInstaller,
|
||||||
buildStandaloneZip,
|
buildStandaloneZip,
|
||||||
catchUncommitted,
|
catchUncommitted,
|
||||||
|
signFilesForNotarization,
|
||||||
testShrinkwrap,
|
testShrinkwrap,
|
||||||
} from './build-bin';
|
} from './build-bin';
|
||||||
import {
|
|
||||||
release,
|
|
||||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
|
||||||
} from './deploy-bin';
|
|
||||||
|
|
||||||
// DEBUG set to falsy for negative values else is truthy
|
// DEBUG set to falsy for negative values else is truthy
|
||||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
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:
|
* of the following strings, then call the appropriate functions:
|
||||||
* 'build:installer' (to build a native oclif installer)
|
* 'build:installer' (to build a native oclif installer)
|
||||||
* 'build:standalone' (to build a standalone pkg package)
|
* '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))
|
* @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> } = {
|
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||||
'build:installer': buildOclifInstaller,
|
'build:installer': buildOclifInstaller,
|
||||||
'build:standalone': buildStandaloneZip,
|
'build:standalone': buildStandaloneZip,
|
||||||
|
'sign:binaries': signFilesForNotarization,
|
||||||
'catch-uncommitted': catchUncommitted,
|
'catch-uncommitted': catchUncommitted,
|
||||||
'test-shrinkwrap': testShrinkwrap,
|
'test-shrinkwrap': testShrinkwrap,
|
||||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
|
||||||
release,
|
|
||||||
};
|
};
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (!commands.hasOwnProperty(arg)) {
|
if (!Object.hasOwn(commands, arg)) {
|
||||||
throw new Error(`command unknown: ${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) {
|
for (const arg of args) {
|
||||||
try {
|
try {
|
||||||
const cmdFunc = commands[arg];
|
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();
|
run();
|
||||||
|
@ -107,11 +107,11 @@ async function $main() {
|
|||||||
|
|
||||||
const changeType = process.argv[4]
|
const changeType = process.argv[4]
|
||||||
? // if the caller specified a change type, use that one
|
? // 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
|
: // use the same change type as in the dependency, but avoid major bumps
|
||||||
semverChangeType && semverChangeType !== 'major'
|
semverChangeType && semverChangeType !== 'major'
|
||||||
? semverChangeType
|
? semverChangeType
|
||||||
: 'minor';
|
: 'minor';
|
||||||
console.log(`Using Change-type: ${changeType}`);
|
console.log(`Using Change-type: ${changeType}`);
|
||||||
|
|
||||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||||
@ -136,5 +136,5 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
main();
|
main();
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { diffTrimmedLines } from 'diff';
|
||||||
|
|
||||||
export const ROOT = path.join(__dirname, '..');
|
export const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
@ -65,7 +66,6 @@ export class StdOutTap {
|
|||||||
* https://www.npmjs.com/package/diff
|
* https://www.npmjs.com/package/diff
|
||||||
*/
|
*/
|
||||||
export function diffLines(str1: string, str2: string): string {
|
export function diffLines(str1: string, str2: string): string {
|
||||||
const { diffTrimmedLines } = require('diff');
|
|
||||||
const diffObjs = diffTrimmedLines(str1, str2);
|
const diffObjs = diffTrimmedLines(str1, str2);
|
||||||
const prefix = (chunk: string, char: string) =>
|
const prefix = (chunk: string, char: string) =>
|
||||||
chunk
|
chunk
|
||||||
@ -77,15 +77,18 @@ export function diffLines(str1: string, str2: string): string {
|
|||||||
return part.added
|
return part.added
|
||||||
? prefix(part.value, '+')
|
? prefix(part.value, '+')
|
||||||
: part.removed
|
: part.removed
|
||||||
? prefix(part.value, '-')
|
? prefix(part.value, '-')
|
||||||
: prefix(part.value, ' ');
|
: prefix(part.value, ' ');
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
return diffStr;
|
return diffStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPackageJson() {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
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" %*
|
87
bin/dev.js
Executable file
87
bin/dev.js
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// ****************************************************************************
|
||||||
|
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||||
|
// Before opening a PR you should build and test your changes using bin/balena
|
||||||
|
// ****************************************************************************
|
||||||
|
|
||||||
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
|
// operations otherwise, if the pool runs out.
|
||||||
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
|
|
||||||
|
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||||
|
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||||
|
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||||
|
// behavior when changes are made to dependencies during development. This is
|
||||||
|
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||||
|
// `package.json` to modify oclif paths, and this results in cache
|
||||||
|
// invalidation and a performance hit rather than speedup.
|
||||||
|
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||||
|
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||||
|
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||||
|
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||||
|
// when the `node_modules` folder is manually modified (affecting transitive
|
||||||
|
// dependencies) during development (e.g. bug investigations). A workaround
|
||||||
|
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||||
|
// `CONTRIBUTING.md`.
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const rootDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// Allow balena-dev to work with oclif by temporarily
|
||||||
|
// pointing oclif config options to src/ instead of build/
|
||||||
|
modifyOclifPaths();
|
||||||
|
// Undo changes on exit
|
||||||
|
process.on('exit', function () {
|
||||||
|
modifyOclifPaths(true);
|
||||||
|
});
|
||||||
|
// Undo changes in case of ctrl-c
|
||||||
|
process.on('SIGINT', function () {
|
||||||
|
modifyOclifPaths(true);
|
||||||
|
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||||
|
// but without it commands can not be exited.
|
||||||
|
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||||
|
// Ideally a better solution is needed.
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the desired es version for downstream modules that support it
|
||||||
|
require('@balena/es-version').set('es2018');
|
||||||
|
|
||||||
|
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||||
|
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||||
|
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||||
|
// it is supposed to run faster. We still benefit from type checking when
|
||||||
|
// running 'npm run build'.
|
||||||
|
require('ts-node').register({
|
||||||
|
project: path.join(rootDir, 'tsconfig.json'),
|
||||||
|
transpileOnly: true,
|
||||||
|
});
|
||||||
|
require('../src/app').run(undefined, { dir: __dirname, development: true });
|
||||||
|
|
||||||
|
// Modify package.json oclif paths from build/ -> src/, or vice versa
|
||||||
|
function modifyOclifPaths(revert) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||||
|
|
||||||
|
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||||
|
const packageObj = JSON.parse(packageJson);
|
||||||
|
|
||||||
|
if (!packageObj.oclif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||||
|
if (!revert) {
|
||||||
|
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/src/');
|
||||||
|
} else {
|
||||||
|
oclifSectionText = oclifSectionText.replace(/\/src\//g, '/build/');
|
||||||
|
}
|
||||||
|
|
||||||
|
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
3
bin/run.cmd
Normal file
3
bin/run.cmd
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
node "%~dp0\run" %*
|
21
bin/run.js
Executable file
21
bin/run.js
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
|
// operations otherwise, if the pool runs out.
|
||||||
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
|
|
||||||
|
// Disable oclif registering ts-node
|
||||||
|
process.env.OCLIF_TS_NODE = 0;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// Use fast-boot to cache require lookups, speeding up startup
|
||||||
|
await require('../build/fast-boot').start();
|
||||||
|
|
||||||
|
// Set the desired es version for downstream modules that support it
|
||||||
|
require('@balena/es-version').set('es2018');
|
||||||
|
|
||||||
|
// Run the CLI
|
||||||
|
await require('../build/app').run(undefined, { dir: __dirname });
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
@ -8,19 +8,21 @@ _balena() {
|
|||||||
local context state line curcontext="$curcontext"
|
local context state line curcontext="$curcontext"
|
||||||
|
|
||||||
# Valid top-level completions
|
# 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 api-keys app block build config deploy device devices env envs fleet fleets internal join key keys leave local login logout logs notes orgs os preload push release releases scan settings ssh support tag tags tunnel util version whoami )
|
||||||
# Sub-completions
|
# Sub-completions
|
||||||
api_key_cmds=( generate )
|
api_key_cmds=( generate revoke )
|
||||||
|
app_cmds=( create )
|
||||||
|
block_cmds=( create )
|
||||||
config_cmds=( generate inject read reconfigure write )
|
config_cmds=( generate inject read reconfigure write )
|
||||||
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
|
device_cmds=( deactivate identify init list local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
|
||||||
devices_cmds=( supported )
|
devices_cmds=( supported )
|
||||||
env_cmds=( add rename rm )
|
env_cmds=( add rename rm )
|
||||||
fleet_cmds=( create purge rename restart rm )
|
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||||
internal_cmds=( osinit )
|
internal_cmds=( osinit )
|
||||||
key_cmds=( add rm )
|
key_cmds=( add rm )
|
||||||
local_cmds=( configure flash )
|
local_cmds=( configure flash )
|
||||||
os_cmds=( build-config configure download initialize versions )
|
os_cmds=( build-config configure download initialize versions )
|
||||||
release_cmds=( finalize )
|
release_cmds=( finalize invalidate validate )
|
||||||
tag_cmds=( rm set )
|
tag_cmds=( rm set )
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +45,12 @@ _balena_sec_cmds() {
|
|||||||
"api-key")
|
"api-key")
|
||||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
_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")
|
"config")
|
||||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
@ -7,19 +7,21 @@ _balena_complete()
|
|||||||
local cur prev
|
local cur prev
|
||||||
|
|
||||||
# Valid top-level completions
|
# 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 api-keys app block build config deploy device devices env envs fleet fleets internal join key keys leave local login logout logs notes orgs os preload push release releases scan settings ssh support tag tags tunnel util version whoami"
|
||||||
# Sub-completions
|
# Sub-completions
|
||||||
api_key_cmds="generate"
|
api_key_cmds="generate revoke"
|
||||||
|
app_cmds="create"
|
||||||
|
block_cmds="create"
|
||||||
config_cmds="generate inject read reconfigure write"
|
config_cmds="generate inject read reconfigure write"
|
||||||
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
|
device_cmds="deactivate identify init list local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet"
|
||||||
devices_cmds="supported"
|
devices_cmds="supported"
|
||||||
env_cmds="add rename rm"
|
env_cmds="add rename rm"
|
||||||
fleet_cmds="create purge rename restart rm"
|
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||||
internal_cmds="osinit"
|
internal_cmds="osinit"
|
||||||
key_cmds="add rm"
|
key_cmds="add rm"
|
||||||
local_cmds="configure flash"
|
local_cmds="configure flash"
|
||||||
os_cmds="build-config configure download initialize versions"
|
os_cmds="build-config configure download initialize versions"
|
||||||
release_cmds="finalize"
|
release_cmds="finalize invalidate validate"
|
||||||
tag_cmds="rm set"
|
tag_cmds="rm set"
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +39,12 @@ _balena_complete()
|
|||||||
api-key)
|
api-key)
|
||||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
app)
|
||||||
|
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
|
block)
|
||||||
|
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
config)
|
config)
|
||||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
@ -31,9 +31,9 @@ if (fs.existsSync(commandsFilePath)) {
|
|||||||
|
|
||||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||||
|
|
||||||
var mainCommands = [];
|
const mainCommands = [];
|
||||||
var additionalCommands = [];
|
const additionalCommands = [];
|
||||||
for (const key of Object.keys(commandsJson.commands)) {
|
for (const key of Object.keys(commandsJson.commands).sort()) {
|
||||||
const cmd = key.split(':');
|
const cmd = key.split(':');
|
||||||
if (cmd.length > 1) {
|
if (cmd.length > 1) {
|
||||||
additionalCommands.push(cmd);
|
additionalCommands.push(cmd);
|
||||||
@ -72,8 +72,8 @@ fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands="' + mainCommandsStr + '"',
|
'main_commands="' + mainCommandsStr + '"',
|
||||||
);
|
);
|
||||||
var subCommands = [];
|
let subCommands = [];
|
||||||
var prevElement = additionalCommands[0][0];
|
let prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
@ -134,8 +134,8 @@ fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands=( ' + mainCommandsStr + ' )',
|
'main_commands=( ' + mainCommandsStr + ' )',
|
||||||
);
|
);
|
||||||
var subCommands = [];
|
let subCommands = [];
|
||||||
var prevElement = additionalCommands[0][0];
|
let prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
|
3762
docs/balena-cli.md
3762
docs/balena-cli.md
File diff suppressed because it is too large
Load Diff
@ -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,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,151 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2019-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 { stripIndent } from './lazy';
|
|
||||||
import { lowercaseIfSlug } from './normalization';
|
|
||||||
|
|
||||||
import { isV14 } from './version';
|
|
||||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
|
||||||
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
|
||||||
|
|
||||||
export const fleet = flags.string({
|
|
||||||
char: 'f',
|
|
||||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
|
||||||
parse: lowercaseIfSlug,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const device = flags.string({
|
|
||||||
char: 'd',
|
|
||||||
description: 'device UUID',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const help: IBooleanFlag<void> = flags.help({ char: 'h' });
|
|
||||||
|
|
||||||
export const quiet: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
char: 'q',
|
|
||||||
description: 'suppress warning messages',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const release = flags.string({
|
|
||||||
char: 'r',
|
|
||||||
description: 'release id',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const service = flags.string({
|
|
||||||
char: 's',
|
|
||||||
description: 'service name',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const verbose: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
char: 'v',
|
|
||||||
description: 'produce verbose output',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const yes: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
char: 'y',
|
|
||||||
description: 'answer "yes" to all questions (non interactive use)',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const force: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
char: 'f',
|
|
||||||
description: 'force action if the update lock is set',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dev: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
description: 'Configure balenaOS to operate in development mode',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const drive = flags.string({
|
|
||||||
char: 'd',
|
|
||||||
description: stripIndent`
|
|
||||||
the drive to write the image to, eg. \`/dev/sdb\` or \`/dev/mmcblk0\`.
|
|
||||||
Careful with this as you can erase your hard drive.
|
|
||||||
Check \`balena util available-drives\` for available options.
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const driveOrImg = flags.string({
|
|
||||||
char: 'd',
|
|
||||||
description:
|
|
||||||
'path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deviceType = flags.string({
|
|
||||||
description:
|
|
||||||
'device type (Check available types with `balena devices supported`)',
|
|
||||||
char: 't',
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deviceTypeIgnored = {
|
|
||||||
...(isV14()
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
type: flags.string({
|
|
||||||
description: 'ignored - no longer required',
|
|
||||||
char: 't',
|
|
||||||
required: false,
|
|
||||||
hidden: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const json: IBooleanFlag<boolean> = flags.boolean({
|
|
||||||
char: 'j',
|
|
||||||
description: 'produce JSON output instead of tabular output',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dataOutputFlags: flags.Input<DataOutputOptions> = {
|
|
||||||
fields: flags.string({
|
|
||||||
description: 'only show provided fields (comma-separated)',
|
|
||||||
}),
|
|
||||||
json: flags.boolean({
|
|
||||||
char: 'j',
|
|
||||||
exclusive: ['no-truncate'],
|
|
||||||
description: 'output in json format',
|
|
||||||
default: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dataSetOutputFlags: flags.Input<DataOutputOptions> &
|
|
||||||
flags.Input<DataSetOutputOptions> = {
|
|
||||||
...dataOutputFlags,
|
|
||||||
filter: flags.string({
|
|
||||||
description:
|
|
||||||
'filter results by substring matching of a given field, eg: --filter field=foo',
|
|
||||||
}),
|
|
||||||
'no-header': flags.boolean({
|
|
||||||
exclusive: ['json'],
|
|
||||||
description: 'hide table header from output',
|
|
||||||
default: false,
|
|
||||||
}),
|
|
||||||
'no-truncate': flags.boolean({
|
|
||||||
exclusive: ['json'],
|
|
||||||
description: 'do not truncate output to fit screen',
|
|
||||||
default: false,
|
|
||||||
}),
|
|
||||||
sort: flags.string({
|
|
||||||
description: `field to sort by (prepend '-' for descending order)`,
|
|
||||||
}),
|
|
||||||
};
|
|
116
lib/utils/sdk.ts
116
lib/utils/sdk.ts
@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2020 Balena Ltd.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Application,
|
|
||||||
BalenaSDK,
|
|
||||||
Organization,
|
|
||||||
PineOptions,
|
|
||||||
} from 'balena-sdk';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a fleet object, disambiguating the fleet identifier which may be a
|
|
||||||
* a fleet slug, name or numeric database ID (as a string).
|
|
||||||
* TODO: add support for fleet UUIDs.
|
|
||||||
*/
|
|
||||||
export async function getApplication(
|
|
||||||
sdk: BalenaSDK,
|
|
||||||
nameOrSlugOrId: string | number,
|
|
||||||
options?: PineOptions<Application>,
|
|
||||||
): Promise<Application> {
|
|
||||||
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
|
|
||||||
if (
|
|
||||||
typeof nameOrSlugOrId === 'string' &&
|
|
||||||
looksLikeFleetSlug(nameOrSlugOrId)
|
|
||||||
) {
|
|
||||||
return await sdk.models.application.getDirectlyAccessible(
|
|
||||||
nameOrSlugOrId,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
|
|
||||||
try {
|
|
||||||
// Test for existence of app with this numerical ID
|
|
||||||
return await sdk.models.application.getDirectlyAccessible(
|
|
||||||
Number(nameOrSlugOrId),
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (typeof nameOrSlugOrId === 'number') {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const { instanceOf } = await import('../errors');
|
|
||||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
|
||||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
// App with this numerical ID not found, but there may be an app with this numerical name.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Not a slug and not a numeric database ID: must be an app name.
|
|
||||||
// TODO: revisit this logic when we add support for fleet UUIDs.
|
|
||||||
return await sdk.models.application.getAppByName(
|
|
||||||
nameOrSlugOrId,
|
|
||||||
options,
|
|
||||||
'directly_accessible',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a fleet name, slug or numeric database ID, return its slug.
|
|
||||||
* This function conditionally makes an async SDK/API call to retrieve the
|
|
||||||
* application object, which can be wasteful if the application object is
|
|
||||||
* required before or after the call to this function. If this is the case,
|
|
||||||
* consider calling `getApplication()` and reusing the application object.
|
|
||||||
*/
|
|
||||||
export async function getFleetSlug(
|
|
||||||
sdk: BalenaSDK,
|
|
||||||
nameOrSlugOrId: string | number,
|
|
||||||
): Promise<string> {
|
|
||||||
const { looksLikeFleetSlug } = await import('./validation');
|
|
||||||
if (
|
|
||||||
typeof nameOrSlugOrId === 'string' &&
|
|
||||||
looksLikeFleetSlug(nameOrSlugOrId)
|
|
||||||
) {
|
|
||||||
return nameOrSlugOrId.toLowerCase();
|
|
||||||
}
|
|
||||||
return (await getApplication(sdk, nameOrSlugOrId)).slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the sdk organization.getAll method,
|
|
||||||
* restricting to those orgs user is a member of
|
|
||||||
*/
|
|
||||||
export async function getOwnOrganizations(
|
|
||||||
sdk: BalenaSDK,
|
|
||||||
): Promise<Organization[]> {
|
|
||||||
return await sdk.models.organization.getAll({
|
|
||||||
$filter: {
|
|
||||||
organization_membership: {
|
|
||||||
$any: {
|
|
||||||
$alias: 'orm',
|
|
||||||
$expr: {
|
|
||||||
orm: {
|
|
||||||
user: await sdk.auth.getUserId(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
$orderby: 'name asc',
|
|
||||||
});
|
|
||||||
}
|
|
28886
npm-shrinkwrap.json
generated
28886
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
175
package.json
175
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "balena-cli",
|
"name": "balena-cli",
|
||||||
"version": "13.4.1",
|
"version": "19.1.0",
|
||||||
"description": "The official balena Command Line Interface",
|
"description": "The official balena Command Line Interface",
|
||||||
"main": "./build/app.js",
|
"main": "./build/app.js",
|
||||||
"homepage": "https://github.com/balena-io/balena-cli",
|
"homepage": "https://github.com/balena-io/balena-cli",
|
||||||
@ -14,33 +14,32 @@
|
|||||||
"bin/",
|
"bin/",
|
||||||
"build/",
|
"build/",
|
||||||
"doc/",
|
"doc/",
|
||||||
"lib/",
|
"src/",
|
||||||
"patches/",
|
"patches/",
|
||||||
|
"!patches/**/**.dev.patch",
|
||||||
"*.md",
|
"*.md",
|
||||||
"npm-shrinkwrap.json",
|
"npm-shrinkwrap.json",
|
||||||
"oclif.manifest.json"
|
"oclif.manifest.json"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"balena": "./bin/balena"
|
"balena": "./bin/run.js"
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"build/**/*.js",
|
"build/**/*.js",
|
||||||
"node_modules/balena-sdk/es2018/index.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/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
|
||||||
"node_modules/resin-compose-parse/build/schemas/*.json"
|
"node_modules/@balena/compose/dist/parse/schemas/*.json"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"build/auth/pages/*.ejs",
|
"build/auth/pages/*.ejs",
|
||||||
"node_modules/resin-discoverable-services/services/**/*",
|
|
||||||
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
|
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
|
||||||
"node_modules/balena-pine/**/*",
|
"node_modules/balena-pine/**/*",
|
||||||
"node_modules/pinejs-client-core/**/*",
|
"node_modules/pinejs-client-core/**/*",
|
||||||
"node_modules/opn/xdg-open",
|
|
||||||
"node_modules/open/xdg-open",
|
"node_modules/open/xdg-open",
|
||||||
"node_modules/windosu/*.bat",
|
"node_modules/windosu/*.bat",
|
||||||
"node_modules/windosu/*.cmd",
|
"node_modules/windosu/*.cmd",
|
||||||
|
"node_modules/axios/**/*",
|
||||||
"npm-shrinkwrap.json",
|
"npm-shrinkwrap.json",
|
||||||
"oclif.manifest.json"
|
"oclif.manifest.json"
|
||||||
]
|
]
|
||||||
@ -48,10 +47,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node patches/apply-patches.js",
|
"postinstall": "node patches/apply-patches.js",
|
||||||
"prebuild": "rimraf build/ build-bin/",
|
"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": "npm run build:src && npm run catch-uncommitted",
|
||||||
"build:t": "npm run lint && npm run build:fast && npm run build:test",
|
"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: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:fast": "npm run build:pages && tsc && npx oclif manifest",
|
||||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit",
|
"build:test": "tsc -P ./tsconfig.dev.json --noEmit",
|
||||||
"build:doc": "ts-node --transpile-only automation/capitanodoc/index.ts > docs/balena-cli.md",
|
"build:doc": "ts-node --transpile-only automation/capitanodoc/index.ts > docs/balena-cli.md",
|
||||||
@ -59,23 +59,24 @@
|
|||||||
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
||||||
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
|
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
|
||||||
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
||||||
"release": "ts-node --transpile-only automation/run.ts release",
|
|
||||||
"pretest": "npm run build",
|
"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:shrinkwrap": "ts-node --transpile-only automation/run.ts test-shrinkwrap",
|
||||||
"test:source": "cross-env BALENA_CLI_TEST_TYPE=source mocha",
|
"test:source": "cross-env BALENA_CLI_TEST_TYPE=source mocha",
|
||||||
"test:standalone": "npm run build:standalone && npm run test:standalone:fast",
|
"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: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": "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: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\"",
|
"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",
|
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||||
"ci": "npm run test && npm run catch-uncommitted",
|
"ci": "npm run test && npm run catch-uncommitted",
|
||||||
"lint": "npm run lint-tsconfig && npm run lint-other",
|
"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-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/balena bin/balena-dev completion/ .mocharc.js .mocharc-standalone.js",
|
"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",
|
"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"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@ -90,147 +91,133 @@
|
|||||||
"author": "Balena Inc. (https://balena.io/)",
|
"author": "Balena Inc. (https://balena.io/)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.8.0 <13.0.0",
|
"node": "^20.6.0"
|
||||||
"npm": "<7.0.0"
|
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "node automation/check-npm-version.js && node automation/check-doc.js"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"oclif": {
|
"oclif": {
|
||||||
"bin": "balena",
|
"bin": "balena",
|
||||||
"commands": "./build/commands",
|
"commands": "./build/commands",
|
||||||
"helpClass": "./build/help",
|
"helpClass": "./build/help",
|
||||||
|
"topicSeparator": " ",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"prerun": "./build/hooks/prerun/track",
|
"prerun": "./build/hooks/prerun/track",
|
||||||
"command_not_found": "./build/hooks/command-not-found/suggest"
|
"command_not_found": "./build/hooks/command-not-found/suggest"
|
||||||
},
|
},
|
||||||
|
"additionalHelpFlags": [
|
||||||
|
"help",
|
||||||
|
"-h"
|
||||||
|
],
|
||||||
"macos": {
|
"macos": {
|
||||||
"identifier": "io.balena.cli",
|
"identifier": "io.balena.cli",
|
||||||
"sign": "Developer ID Installer: Balena Ltd (66H43P8FRG)"
|
"sign": "\"Developer ID Installer: Balena Ltd (66H43P8FRG)\""
|
||||||
},
|
}
|
||||||
"plugins": [
|
|
||||||
"@oclif/plugin-help"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@balena/lint": "^6.2.0",
|
"@balena/lint": "^8.0.0",
|
||||||
"@oclif/config": "^1.18.2",
|
"@electron/notarize": "^2.0.0",
|
||||||
"@oclif/parser": "^3.8.6",
|
"@types/archiver": "^6.0.2",
|
||||||
"@octokit/plugin-throttling": "^3.5.1",
|
|
||||||
"@octokit/rest": "^18.6.7",
|
|
||||||
"@types/archiver": "^5.1.1",
|
|
||||||
"@types/bluebird": "^3.5.36",
|
"@types/bluebird": "^3.5.36",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
"@types/cli-truncate": "^2.0.0",
|
"@types/cli-truncate": "^2.0.0",
|
||||||
"@types/common-tags": "^1.8.1",
|
"@types/common-tags": "^1.8.1",
|
||||||
"@types/dockerode": "^3.3.8",
|
"@types/diff": "^5.0.3",
|
||||||
|
"@types/dockerode": "3.3.23",
|
||||||
"@types/ejs": "^3.1.0",
|
"@types/ejs": "^3.1.0",
|
||||||
"@types/express": "^4.17.13",
|
"@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-agent": "^2.1.1",
|
||||||
"@types/global-tunnel-ng": "^2.1.1",
|
"@types/global-tunnel-ng": "^2.1.1",
|
||||||
"@types/http-proxy": "^1.17.8",
|
"@types/http-proxy": "^1.17.8",
|
||||||
|
"@types/inquirer": "^7.3.3",
|
||||||
"@types/intercept-stdout": "^0.1.0",
|
"@types/intercept-stdout": "^0.1.0",
|
||||||
"@types/is-root": "^2.1.2",
|
"@types/is-root": "^2.1.2",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsonwebtoken": "^8.5.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/klaw": "^3.0.3",
|
"@types/klaw": "^3.0.6",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/mixpanel": "^2.14.3",
|
"@types/mime": "^3.0.4",
|
||||||
"@types/mocha": "^8.2.3",
|
"@types/mocha": "^10.0.7",
|
||||||
|
"@types/mock-fs": "^4.13.4",
|
||||||
"@types/mock-require": "^2.0.1",
|
"@types/mock-require": "^2.0.1",
|
||||||
"@types/moment-duration-format": "^2.2.3",
|
|
||||||
"@types/ndjson": "^2.0.1",
|
"@types/ndjson": "^2.0.1",
|
||||||
"@types/net-keepalive": "^0.4.1",
|
"@types/node": "^20.0.0",
|
||||||
"@types/nock": "^11.1.0",
|
|
||||||
"@types/node": "^12.20.42",
|
|
||||||
"@types/node-cleanup": "^2.1.2",
|
"@types/node-cleanup": "^2.1.2",
|
||||||
"@types/parse-link-header": "^1.0.1",
|
"@types/prettyjson": "^0.0.33",
|
||||||
"@types/prettyjson": "^0.0.30",
|
|
||||||
"@types/progress-stream": "^2.0.2",
|
"@types/progress-stream": "^2.0.2",
|
||||||
"@types/request": "^2.48.7",
|
"@types/request": "^2.48.7",
|
||||||
"@types/rewire": "^2.5.28",
|
"@types/rewire": "^2.5.30",
|
||||||
"@types/rimraf": "^3.0.2",
|
"@types/rimraf": "^3.0.2",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/shell-escape": "^0.2.0",
|
"@types/shell-escape": "^0.2.0",
|
||||||
"@types/sinon": "^10.0.6",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/split": "^1.0.0",
|
"@types/split": "^1.0.0",
|
||||||
"@types/stream-to-promise": "^2.2.1",
|
"@types/stream-to-promise": "^2.2.1",
|
||||||
"@types/tar-stream": "^2.2.2",
|
"@types/tar-stream": "^2.2.2",
|
||||||
"@types/through2": "^2.0.36",
|
"@types/through2": "^2.0.36",
|
||||||
"@types/tmp": "^0.2.3",
|
"@types/tmp": "^0.2.3",
|
||||||
|
"@types/update-notifier": "^4.1.1",
|
||||||
"@types/which": "^2.0.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",
|
"catch-uncommitted": "^2.0.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"deep-object-diff": "^1.1.0",
|
"deep-object-diff": "^1.1.0",
|
||||||
"diff": "^5.0.0",
|
"diff": "^5.0.0",
|
||||||
"electron-notarize": "^1.0.0",
|
|
||||||
"ent": "^2.2.0",
|
"ent": "^2.2.0",
|
||||||
"filehound": "^1.17.5",
|
"filehound": "^1.17.5",
|
||||||
"fs-extra": "^9.1.0",
|
"fs-extra": "^11.2.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^9.1.5",
|
||||||
"inline-source-cli": "^2.0.0",
|
"inline-source-cli": "^2.0.0",
|
||||||
"intercept-stdout": "^0.1.2",
|
"intercept-stdout": "^0.1.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"mkdirp": "^1.0.4",
|
"klaw": "^4.1.0",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^10.6.0",
|
||||||
|
"mock-fs": "^5.2.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"nock": "^13.2.1",
|
"nock": "^13.2.1",
|
||||||
"parse-link-header": "^1.0.1",
|
"oclif": "^4.14.0",
|
||||||
"pkg": "^5.5.1",
|
"rewire": "^7.0.0",
|
||||||
"publish-release": "^1.6.1",
|
"simple-git": "^3.14.1",
|
||||||
"rewire": "^5.0.0",
|
"sinon": "^18.0.0",
|
||||||
"simple-git": "^2.48.0",
|
"string-to-stream": "^3.0.1",
|
||||||
"sinon": "^11.1.2",
|
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@balena/compose": "^4.0.1",
|
||||||
"@balena/dockerignore": "^1.0.2",
|
"@balena/dockerignore": "^1.0.2",
|
||||||
|
"@balena/env-parsing": "^1.1.8",
|
||||||
"@balena/es-version": "^1.0.1",
|
"@balena/es-version": "^1.0.1",
|
||||||
"@oclif/command": "^1.8.16",
|
"@oclif/core": "^4.0.8",
|
||||||
"@resin.io/valid-email": "^0.1.0",
|
|
||||||
"@sentry/node": "^6.16.1",
|
"@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-config-json": "^4.2.0",
|
||||||
"balena-device-init": "^6.0.0",
|
"balena-device-init": "^7.0.1",
|
||||||
"balena-errors": "^4.7.1",
|
"balena-errors": "^4.7.3",
|
||||||
"balena-image-fs": "^7.0.6",
|
"balena-image-fs": "^7.0.6",
|
||||||
"balena-image-manager": "^7.1.1",
|
"balena-preload": "^15.0.6",
|
||||||
"balena-preload": "^12.0.0",
|
"balena-sdk": "^19.7.3",
|
||||||
"balena-release": "^3.2.0",
|
|
||||||
"balena-sdk": "^16.9.0",
|
|
||||||
"balena-semver": "^2.3.0",
|
"balena-semver": "^2.3.0",
|
||||||
"balena-settings-client": "^4.0.7",
|
"balena-settings-client": "^5.0.2",
|
||||||
"balena-settings-storage": "^7.0.0",
|
"balena-settings-storage": "^8.1.0",
|
||||||
"balena-sync": "^11.0.2",
|
|
||||||
"bluebird": "^3.7.2",
|
|
||||||
"body-parser": "^1.19.1",
|
"body-parser": "^1.19.1",
|
||||||
|
"bonjour-service": "^1.2.1",
|
||||||
"chalk": "^3.0.0",
|
"chalk": "^3.0.0",
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.2",
|
||||||
"cli-truncate": "^2.1.0",
|
"cli-truncate": "^2.1.0",
|
||||||
"cli-ux": "^5.5.1",
|
|
||||||
"color-hash": "^1.1.1",
|
"color-hash": "^1.1.1",
|
||||||
"columnify": "^1.5.2",
|
|
||||||
"common-tags": "^1.7.2",
|
"common-tags": "^1.7.2",
|
||||||
"denymount": "^2.3.0",
|
"denymount": "^2.3.0",
|
||||||
"docker-modem": "3.0.0",
|
"docker-modem": "^5.0.3",
|
||||||
"docker-progress": "^5.0.1",
|
"docker-progress": "^5.1.3",
|
||||||
"docker-qemu-transpose": "^1.1.1",
|
"dockerode": "^4.0.2",
|
||||||
"dockerode": "^3.3.1",
|
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"etcher-sdk": "^6.2.1",
|
"etcher-sdk": "9.1.0",
|
||||||
"event-stream": "3.3.4",
|
|
||||||
"express": "^4.17.2",
|
"express": "^4.17.2",
|
||||||
"fast-boot2": "^1.1.0",
|
"fast-boot2": "^1.1.0",
|
||||||
"fast-levenshtein": "^3.0.0",
|
"fast-levenshtein": "^3.0.0",
|
||||||
@ -245,28 +232,23 @@
|
|||||||
"is-elevated": "^3.0.0",
|
"is-elevated": "^3.0.0",
|
||||||
"is-root": "^2.1.0",
|
"is-root": "^2.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"klaw": "^3.0.0",
|
"JSONStream": "^1.0.3",
|
||||||
"livepush": "^3.5.1",
|
"livepush": "^3.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimatch": "^3.0.4",
|
"mime": "^2.4.6",
|
||||||
"moment": "^2.29.1",
|
"mkdirp": "^3.0.1",
|
||||||
"moment-duration-format": "^2.3.2",
|
|
||||||
"ndjson": "^2.0.0",
|
"ndjson": "^2.0.0",
|
||||||
"net-keepalive": "^3.0.0",
|
|
||||||
"node-cleanup": "^2.1.2",
|
"node-cleanup": "^2.1.2",
|
||||||
"node-unzip-2": "^0.2.8",
|
"node-unzip-2": "^0.2.8",
|
||||||
"oclif": "^1.18.4",
|
|
||||||
"open": "^7.1.0",
|
"open": "^7.1.0",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^8.0.0",
|
||||||
"prettyjson": "^1.2.5",
|
"prettyjson": "^1.2.5",
|
||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"resin-cli-form": "^2.0.2",
|
"resin-cli-form": "^3.0.0",
|
||||||
"resin-cli-visuals": "^1.8.0",
|
"resin-cli-visuals": "^2.0.1",
|
||||||
"resin-compose-parse": "^2.1.3",
|
|
||||||
"resin-doodles": "^0.2.0",
|
"resin-doodles": "^0.2.0",
|
||||||
"resin-multibuild": "^4.12.2",
|
|
||||||
"resin-stream-logger": "^0.1.2",
|
"resin-stream-logger": "^0.1.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
@ -287,7 +269,12 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"windosu": "^0.3.0"
|
"windosu": "^0.3.0"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"inline-source-cli": {
|
||||||
|
"inline-source": "^8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"versionist": {
|
"versionist": {
|
||||||
"publishedAt": "2022-04-11T16:10:47.566Z"
|
"publishedAt": "2024-10-11T17:15:03.656Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
diff --git a/node_modules/@oclif/config/lib/config.js b/node_modules/@oclif/config/lib/config.js
|
|
||||||
index aba1da9..830f800 100644
|
|
||||||
--- a/node_modules/@oclif/config/lib/config.js
|
|
||||||
+++ b/node_modules/@oclif/config/lib/config.js
|
|
||||||
@@ -165,7 +165,9 @@ class Config {
|
|
||||||
debug('runCommand %s %o', id, argv);
|
|
||||||
const c = this.findCommand(id);
|
|
||||||
if (!c) {
|
|
||||||
- await this.runHook('command_not_found', { id });
|
|
||||||
+ // argv added to command_not_found hook
|
|
||||||
+ // We should try to upstream this change
|
|
||||||
+ await this.runHook('command_not_found', { id, argv });
|
|
||||||
throw new errors_1.CLIError(`command ${id} not found`);
|
|
||||||
}
|
|
||||||
const command = c.load();
|
|
79
patches/all/@oclif+core+4.0.18.patch
Normal file
79
patches/all/@oclif+core+4.0.18.patch
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
diff --git a/node_modules/@oclif/core/lib/help/command.js b/node_modules/@oclif/core/lib/help/command.js
|
||||||
|
index 90922c8..6b7f417 100644
|
||||||
|
--- a/node_modules/@oclif/core/lib/help/command.js
|
||||||
|
+++ b/node_modules/@oclif/core/lib/help/command.js
|
||||||
|
@@ -58,7 +58,8 @@ class CommandHelp extends formatter_1.HelpFormatter {
|
||||||
|
return;
|
||||||
|
return args.map((a) => {
|
||||||
|
// Add ellipsis to indicate that the argument takes multiple values if strict is false
|
||||||
|
- const name = this.command.strict === false ? `${a.name.toUpperCase()}...` : a.name.toUpperCase();
|
||||||
|
+ let name = this.command.strict === false ? `${a.name.toUpperCase()}...` : a.name.toUpperCase();
|
||||||
|
+ name = a.required ? `<${name}>` : `[${name}]`;
|
||||||
|
let description = a.description || '';
|
||||||
|
if (a.default)
|
||||||
|
description = `${(0, theme_1.colorize)(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}`;
|
||||||
|
diff --git a/node_modules/@oclif/core/lib/help/index.js b/node_modules/@oclif/core/lib/help/index.js
|
||||||
|
index 4a34b89..d7eb6ac 100644
|
||||||
|
--- a/node_modules/@oclif/core/lib/help/index.js
|
||||||
|
+++ b/node_modules/@oclif/core/lib/help/index.js
|
||||||
|
@@ -172,11 +172,12 @@ class Help extends HelpBase {
|
||||||
|
}
|
||||||
|
this.log(this.formatCommand(command));
|
||||||
|
this.log('');
|
||||||
|
- if (subTopics.length > 0) {
|
||||||
|
+ const SUPPRESS_SUBTOPICS = true;
|
||||||
|
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||||
|
this.log(this.formatTopics(subTopics));
|
||||||
|
this.log('');
|
||||||
|
}
|
||||||
|
- if (subCommands.length > 0) {
|
||||||
|
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
|
||||||
|
const aliases = [];
|
||||||
|
const uniqueSubCommands = subCommands.filter((p) => {
|
||||||
|
aliases.push(...p.aliases);
|
||||||
|
diff --git a/node_modules/@oclif/core/lib/parser/errors.js b/node_modules/@oclif/core/lib/parser/errors.js
|
||||||
|
index 168da99..538a880 100644
|
||||||
|
--- a/node_modules/@oclif/core/lib/parser/errors.js
|
||||||
|
+++ b/node_modules/@oclif/core/lib/parser/errors.js
|
||||||
|
@@ -15,7 +15,8 @@ class CLIParseError extends errors_1.CLIError {
|
||||||
|
parse;
|
||||||
|
showHelp = false;
|
||||||
|
constructor(options) {
|
||||||
|
- options.message += '\nSee more help with --help';
|
||||||
|
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
|
||||||
|
+ options.message += `\nSee more help with ${help}`;
|
||||||
|
super(options.message, { exit: options.exit });
|
||||||
|
this.parse = options.parse;
|
||||||
|
}
|
||||||
|
@@ -38,7 +39,8 @@ exports.InvalidArgsSpecError = InvalidArgsSpecError;
|
||||||
|
class RequiredArgsError extends CLIParseError {
|
||||||
|
args;
|
||||||
|
constructor({ args, exit, flagsWithMultiple, parse, }) {
|
||||||
|
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
|
||||||
|
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||||
|
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
|
||||||
|
const namedArgs = args.filter((a) => a.name);
|
||||||
|
if (namedArgs.length > 0) {
|
||||||
|
const list = (0, list_1.default)(namedArgs.map((a) => {
|
||||||
|
@@ -52,7 +54,7 @@ class RequiredArgsError extends CLIParseError {
|
||||||
|
message += `\n\nNote: ${flags} allow${flagsWithMultiple.length === 1 ? 's' : ''} multiple values. Because of this you need to provide all arguments before providing ${flagsWithMultiple.length === 1 ? 'that flag' : 'those flags'}.`;
|
||||||
|
message += '\nAlternatively, you can use "--" to signify the end of the flags and the beginning of arguments.';
|
||||||
|
}
|
||||||
|
- super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse });
|
||||||
|
+ super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse, command });
|
||||||
|
this.args = args;
|
||||||
|
this.showHelp = true;
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/@oclif/core/lib/ux/list.js b/node_modules/@oclif/core/lib/ux/list.js
|
||||||
|
index 954954c..0e507c7 100644
|
||||||
|
--- a/node_modules/@oclif/core/lib/ux/list.js
|
||||||
|
+++ b/node_modules/@oclif/core/lib/ux/list.js
|
||||||
|
@@ -22,7 +22,7 @@ function renderList(items) {
|
||||||
|
}
|
||||||
|
left = left.padEnd(maxLength);
|
||||||
|
right = linewrap(maxLength + 2, right);
|
||||||
|
- return `${left} ${right}`;
|
||||||
|
+ return `${left} : ${right}`;
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
@ -1,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;
|
|
35
patches/all/oclif+4.14.0.dev.patch
Normal file
35
patches/all/oclif+4.14.0.dev.patch
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||||
|
index ef7f90e..8264b7c 100644
|
||||||
|
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||||
|
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||||
|
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||||
|
${customization}
|
||||||
|
|
||||||
|
Section "${config.name} CLI \${VERSION}"
|
||||||
|
+ ; First remove any old client files.
|
||||||
|
+ ; (Remnants of old versions were causing CLI errors)
|
||||||
|
+ ; Initially tried running the Uninstall.exe, but was
|
||||||
|
+ ; unable to make script wait for completion (despite using _?)
|
||||||
|
+ DetailPrint "Removing files from previous version."
|
||||||
|
+ RMDir /r "$INSTDIR\\client"
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
File /r bin
|
||||||
|
File /r client
|
||||||
|
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||||
|
index 14d5a6e..7b42a6f 100644
|
||||||
|
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||||
|
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||||
|
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
|
||||||
|
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
|
||||||
|
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
|
||||||
|
]);
|
||||||
|
+
|
||||||
|
+ // The oclif installers are a production installation, while the source
|
||||||
|
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
|
||||||
|
+ // This has previously led to issues preventing the CLI from starting, so
|
||||||
|
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
||||||
|
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
|
||||||
|
+
|
||||||
|
};
|
||||||
|
const buildTarget = async (target, c, options) => {
|
||||||
|
const workspace = c.workspace(target);
|
@ -1,15 +1,16 @@
|
|||||||
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
|
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
|
--- a/node_modules/open/index.js
|
||||||
+++ b/node_modules/open/index.js
|
+++ b/node_modules/open/index.js
|
||||||
@@ -11,7 +11,9 @@ const pAccess = promisify(fs.access);
|
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
|
||||||
const pExecFile = promisify(childProcess.execFile);
|
const pReadFile = promisify(fs.readFile);
|
||||||
|
|
||||||
// Path to included `xdg-open`.
|
// Path to included `xdg-open`.
|
||||||
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
|
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
|
||||||
+const localXdgOpenPath = process.pkg
|
+const localXdgOpenPath = process.pkg
|
||||||
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
|
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
|
||||||
+ : path.join(__dirname, '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
|
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
|
||||||
index b5096ed..7cd451a 100644
|
index 61b398e..3cc3be8 100644
|
||||||
--- a/node_modules/node-gyp-build/index.js
|
--- a/node_modules/node-gyp-build/node-gyp-build.js
|
||||||
+++ b/node_modules/node-gyp-build/index.js
|
+++ b/node_modules/node-gyp-build/node-gyp-build.js
|
||||||
@@ -29,6 +29,9 @@ load.path = function (dir) {
|
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
|
||||||
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
|
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
6
repo.yml
6
repo.yml
@ -10,11 +10,9 @@ upstream:
|
|||||||
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
||||||
- repo: 'balena-preload'
|
- repo: 'balena-preload'
|
||||||
url: 'https://github.com/balena-io-modules/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'
|
- repo: 'etcher-sdk'
|
||||||
url: 'https://github.com/balena-io-modules/etcher-sdk/'
|
url: 'https://github.com/balena-io-modules/etcher-sdk/'
|
||||||
- repo: 'resin-compose-parse'
|
- repo: '@balena/compose'
|
||||||
url: 'https://github.com/balena-io-modules/resin-compose-parse'
|
url: 'https://github.com/balena-io-modules/balena-compose'
|
||||||
- repo: 'docker-progress'
|
- repo: 'docker-progress'
|
||||||
url: 'https://github.com/balena-io-modules/docker-progress'
|
url: 'https://github.com/balena-io-modules/docker-progress'
|
||||||
|
@ -16,14 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as packageJSON from '../package.json';
|
import * as packageJSON from '../package.json';
|
||||||
|
import type { AppOptions } from './preparser';
|
||||||
import {
|
import {
|
||||||
AppOptions,
|
|
||||||
checkDeletedCommand,
|
checkDeletedCommand,
|
||||||
preparseArgs,
|
preparseArgs,
|
||||||
unsupportedFlag,
|
unsupportedFlag,
|
||||||
} from './preparser';
|
} from './preparser';
|
||||||
import { CliSettings } from './utils/bootstrap';
|
import { CliSettings } from './utils/bootstrap';
|
||||||
import { onceAsync } from './utils/lazy';
|
import { onceAsync } from './utils/lazy';
|
||||||
|
import { run as mainRun, settings } from '@oclif/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry.io setup
|
* Sentry.io setup
|
||||||
@ -114,10 +115,16 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runPromise = (async function (shouldFlush: boolean) {
|
const runPromise = (async function (shouldFlush: boolean) {
|
||||||
const { CustomMain } = await import('./utils/oclif-utils');
|
|
||||||
let isEEXIT = false;
|
let isEEXIT = false;
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||||
// for example the `balena help` command.
|
// for example the `balena help` command.
|
||||||
@ -130,7 +137,8 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldFlush) {
|
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
|
// 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.
|
// addition to calling it in the main `run()` function in this file.
|
||||||
@ -150,8 +158,8 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
|
||||||
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
export async function run(cliArgs = process.argv, options: AppOptions) {
|
||||||
try {
|
try {
|
||||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||||
'./utils/bootstrap'
|
'./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 |
@ -15,13 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Command from '@oclif/command';
|
import { Command } from '@oclif/core';
|
||||||
import {
|
import {
|
||||||
InsufficientPrivilegesError,
|
InsufficientPrivilegesError,
|
||||||
NotAvailableInOfflineModeError,
|
NotAvailableInOfflineModeError,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { stripIndent } from './utils/lazy';
|
import { stripIndent } from './utils/lazy';
|
||||||
import * as output from './framework/output';
|
|
||||||
|
|
||||||
export default abstract class BalenaCommand extends Command {
|
export default abstract class BalenaCommand extends Command {
|
||||||
/**
|
/**
|
||||||
@ -168,7 +167,4 @@ export default abstract class BalenaCommand extends Command {
|
|||||||
await this.getStdin();
|
await this.getStdin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected outputMessage = output.outputMessage;
|
|
||||||
protected outputData = output.outputData;
|
|
||||||
}
|
}
|
@ -15,20 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GenerateCmd extends Command {
|
export default class GenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Generate a new balenaCloud API key.
|
Generate a new balenaCloud API key.
|
||||||
@ -41,24 +32,17 @@ export default class GenerateCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
|
||||||
description: 'the API key name',
|
description: 'the API key name',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'api-key generate <name>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
const { args: params } = await this.parse(GenerateCmd);
|
||||||
|
|
||||||
let key;
|
let key;
|
||||||
try {
|
try {
|
60
src/commands/api-key/revoke.ts
Normal file
60
src/commands/api-key/revoke.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
80
src/commands/api-keys/index.ts
Normal file
80
src/commands/api-keys/index.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @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/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
export default class ApiKeysCmd extends Command {
|
||||||
|
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-keys'];
|
||||||
|
|
||||||
|
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(ApiKeysCmd);
|
||||||
|
|
||||||
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
const actorId = options.fleet
|
||||||
|
? (
|
||||||
|
await getApplication(getBalenaSdk(), options.fleet, {
|
||||||
|
$select: 'actor',
|
||||||
|
})
|
||||||
|
).actor
|
||||||
|
: 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
79
src/commands/app/create.ts
Normal file
79
src/commands/app/create.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
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 orgs\` command.
|
||||||
|
|
||||||
|
The app's default device type is specified with the \`--type\` option.
|
||||||
|
The \`balena devices supported\` command can be used to list the available
|
||||||
|
device types.
|
||||||
|
|
||||||
|
Interactive dropdowns will be shown for selection if no device type or
|
||||||
|
organization is specified and there are multiple options to choose from.
|
||||||
|
If there is a single option to choose from, it will be chosen automatically.
|
||||||
|
This interactive behavior can be disabled by explicitly specifying a device
|
||||||
|
type and organization.
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = [
|
||||||
|
'$ balena app create MyApp',
|
||||||
|
'$ balena app create MyApp --organization mmyorg',
|
||||||
|
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
name: 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 devices supported`)',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
79
src/commands/block/create.ts
Normal file
79
src/commands/block/create.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
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 orgs\` command.
|
||||||
|
|
||||||
|
The block'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 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 devices supported`)',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
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,22 +15,31 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args, Flags } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { getBalenaSdk } from '../utils/lazy';
|
import { getBalenaSdk } from '../../utils/lazy';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../../utils/compose';
|
||||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
import type {
|
||||||
|
ApplicationType,
|
||||||
|
BalenaSDK,
|
||||||
|
DeviceType,
|
||||||
|
PineOptions,
|
||||||
|
PineTypedResult,
|
||||||
|
} from 'balena-sdk';
|
||||||
import {
|
import {
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
} from '../utils/messages';
|
} from '../../utils/messages';
|
||||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types';
|
||||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||||
import { dockerCliFlags } from '../utils/docker';
|
import { dockerCliFlags } from '../../utils/docker';
|
||||||
|
|
||||||
|
// 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 {
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
arch?: string;
|
arch?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
@ -39,10 +48,6 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
|||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
source?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class BuildCmd extends Command {
|
export default class BuildCmd extends Command {
|
||||||
public static description = `\
|
public static description = `\
|
||||||
Build a project locally.
|
Build a project locally.
|
||||||
@ -68,27 +73,23 @@ ${dockerignoreHelp}
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena build --fleet myFleet',
|
'$ balena build --fleet myFleet',
|
||||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||||
|
'$ balena build --deviceType raspberrypi3 --emulated',
|
||||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
'$ 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',
|
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
source: Args.string({ description: 'path of project source directory' }),
|
||||||
name: 'source',
|
};
|
||||||
description: 'path of project source directory',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'build [source]';
|
public static flags = {
|
||||||
|
arch: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
arch: flags.string({
|
|
||||||
description: 'the architecture to build for',
|
description: 'the architecture to build for',
|
||||||
char: 'A',
|
char: 'A',
|
||||||
}),
|
}),
|
||||||
deviceType: flags.string({
|
deviceType: Flags.string({
|
||||||
description: 'the type of device this build is for',
|
description: 'the type of device this build is for',
|
||||||
char: 'd',
|
char: 'd',
|
||||||
}),
|
}),
|
||||||
@ -97,15 +98,13 @@ ${dockerignoreHelp}
|
|||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
// Revisit this in future release.
|
// Revisit this in future release.
|
||||||
help: flags.help({}),
|
help: Flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(BuildCmd);
|
||||||
BuildCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Command.checkLoggedInIf(!!options.fleet);
|
await Command.checkLoggedInIf(!!options.fleet);
|
||||||
|
|
||||||
@ -120,6 +119,8 @@ ${dockerignoreHelp}
|
|||||||
options.source = params.source;
|
options.source = params.source;
|
||||||
delete params.source;
|
delete params.source;
|
||||||
|
|
||||||
|
await this.resolveArchFromDeviceType(sdk, options);
|
||||||
|
|
||||||
await this.validateOptions(options, sdk);
|
await this.validateOptions(options, sdk);
|
||||||
|
|
||||||
// Build args are under consideration for removal - warn user
|
// Build args are under consideration for removal - warn user
|
||||||
@ -133,7 +134,7 @@ ${dockerignoreHelp}
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.buildProject(docker, logger, composeOpts, {
|
await this.buildProject(docker, logger, composeOpts, {
|
||||||
app,
|
appType: app?.application_type?.[0],
|
||||||
arch: options.arch!,
|
arch: options.arch!,
|
||||||
deviceType: options.deviceType!,
|
deviceType: options.deviceType!,
|
||||||
buildEmulated: options.emulated,
|
buildEmulated: options.emulated,
|
||||||
@ -154,14 +155,14 @@ ${dockerignoreHelp}
|
|||||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||||
(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(
|
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
|
// Validate project directory
|
||||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
const { validateProjectDirectory } = await import('../../utils/compose_ts');
|
||||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||||
sdk,
|
sdk,
|
||||||
{
|
{
|
||||||
@ -176,9 +177,42 @@ ${dockerignoreHelp}
|
|||||||
opts['registry-secrets'] = registrySecrets;
|
opts['registry-secrets'] = registrySecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async resolveArchFromDeviceType(sdk: BalenaSDK, opts: FlagsDef) {
|
||||||
|
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: FlagsDef) {
|
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||||
if (opts.fleet) {
|
if (opts.fleet) {
|
||||||
const { getAppWithArch } = await import('../utils/helpers');
|
const { getAppWithArch } = await import('../../utils/helpers');
|
||||||
const app = await getAppWithArch(opts.fleet);
|
const app = await getAppWithArch(opts.fleet);
|
||||||
opts.arch = app.arch;
|
opts.arch = app.arch;
|
||||||
opts.deviceType = app.is_for__device_type[0].slug;
|
opts.deviceType = app.is_for__device_type[0].slug;
|
||||||
@ -187,7 +221,7 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async prepareBuild(options: FlagsDef) {
|
protected async prepareBuild(options: FlagsDef) {
|
||||||
const { getDocker, generateBuildOpts } = await import('../utils/docker');
|
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
getDocker(options),
|
getDocker(options),
|
||||||
generateBuildOpts(options),
|
generateBuildOpts(options),
|
||||||
@ -208,24 +242,24 @@ ${dockerignoreHelp}
|
|||||||
* buildEmulated
|
* buildEmulated
|
||||||
* buildOpts: arguments to forward to docker build command
|
* buildOpts: arguments to forward to docker build command
|
||||||
*
|
*
|
||||||
* @param {DockerToolbelt} docker
|
* @param {Dockerode} docker
|
||||||
* @param {Logger} logger
|
* @param {Logger} logger
|
||||||
* @param {ComposeOpts} composeOpts
|
* @param {ComposeOpts} composeOpts
|
||||||
* @param opts
|
* @param opts
|
||||||
*/
|
*/
|
||||||
protected async buildProject(
|
protected async buildProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../utils/logger'),
|
logger: import('../../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
app?: Application;
|
appType?: Pick<ApplicationType, 'supports_multicontainer'>;
|
||||||
arch: string;
|
arch: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
buildEmulated: boolean;
|
buildEmulated: boolean;
|
||||||
buildOpts: BuildOpts;
|
buildOpts: BuildOpts;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { loadProject } = await import('../utils/compose_ts');
|
const { loadProject } = await import('../../utils/compose_ts');
|
||||||
|
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
logger,
|
logger,
|
||||||
@ -234,11 +268,10 @@ ${dockerignoreHelp}
|
|||||||
opts.buildOpts.t,
|
opts.buildOpts.t,
|
||||||
);
|
);
|
||||||
|
|
||||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
|
||||||
if (
|
if (
|
||||||
appType != null &&
|
opts.appType != null &&
|
||||||
project.descriptors.length > 1 &&
|
project.descriptors.length > 1 &&
|
||||||
!appType.supports_multicontainer
|
!opts.appType.supports_multicontainer
|
||||||
) {
|
) {
|
||||||
logger.logWarn(
|
logger.logWarn(
|
||||||
'Target fleet does not support multiple containers.\n' +
|
'Target fleet does not support multiple containers.\n' +
|
@ -15,30 +15,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
|
import type { Interfaces } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
import {
|
||||||
import type { PineDeferred } from 'balena-sdk';
|
applicationIdInfo,
|
||||||
|
devModeInfo,
|
||||||
interface FlagsDef {
|
secureBootInfo,
|
||||||
version: string; // OS version
|
} from '../../utils/messages';
|
||||||
fleet?: string;
|
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
||||||
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;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigGenerateCmd extends Command {
|
export default class ConfigGenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -50,6 +37,8 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
|
|
||||||
${devModeInfo.split('\n').join('\n\t\t')}
|
${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
|
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.
|
alongside the --deviceType option to specify the target device type.
|
||||||
|
|
||||||
@ -65,86 +54,100 @@ 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 --deviceApiKey <existingDeviceKey>',
|
||||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
'$ 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 --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 --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 --output config.json',
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
'$ 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 = {
|
||||||
|
version: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
version: flags.string({
|
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
dev: cf.dev,
|
dev: cf.dev,
|
||||||
|
secureBoot: cf.secureBoot,
|
||||||
device: {
|
device: {
|
||||||
...cf.device,
|
...cf.device,
|
||||||
exclusive: ['fleet', 'provisioning-key-name'],
|
exclusive: [
|
||||||
|
'fleet',
|
||||||
|
'provisioning-key-name',
|
||||||
|
'provisioning-key-expiry-date',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
deviceApiKey: flags.string({
|
deviceApiKey: Flags.string({
|
||||||
description:
|
description:
|
||||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||||
char: 'k',
|
char: 'k',
|
||||||
}),
|
}),
|
||||||
deviceType: flags.string({
|
deviceType: Flags.string({
|
||||||
description:
|
description:
|
||||||
"device type slug (run 'balena devices supported' for possible values)",
|
"device type slug (run 'balena devices supported' for possible values)",
|
||||||
}),
|
}),
|
||||||
'generate-device-api-key': flags.boolean({
|
'generate-device-api-key': Flags.boolean({
|
||||||
description: 'generate a fresh device key for the device',
|
description: 'generate a fresh device key for the device',
|
||||||
}),
|
}),
|
||||||
output: flags.string({
|
output: Flags.string({
|
||||||
description: 'path of output file',
|
description: 'path of output file',
|
||||||
char: 'o',
|
char: 'o',
|
||||||
}),
|
}),
|
||||||
// Options for non-interactive configuration
|
// Options for non-interactive configuration
|
||||||
network: flags.string({
|
network: Flags.string({
|
||||||
description: 'the network type to use: ethernet or wifi',
|
description: 'the network type to use: ethernet or wifi',
|
||||||
options: ['ethernet', 'wifi'],
|
options: ['ethernet', 'wifi'],
|
||||||
}),
|
}),
|
||||||
wifiSsid: flags.string({
|
wifiSsid: Flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
wifiKey: flags.string({
|
wifiKey: Flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi key to use (used only if --network is set to wifi)',
|
'the wifi key to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
appUpdatePollInterval: flags.string({
|
appUpdatePollInterval: Flags.string({
|
||||||
description:
|
description:
|
||||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
'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',
|
description: 'custom key name assigned to generated provisioning api key',
|
||||||
exclusive: ['device'],
|
exclusive: ['device'],
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
'provisioning-key-expiry-date': Flags.string({
|
||||||
|
description:
|
||||||
|
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||||
|
exclusive: ['device'],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async getApplication(balena: BalenaSDK, fleet: string) {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
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();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
await this.validateOptions(options);
|
await this.validateOptions(options);
|
||||||
|
|
||||||
let resourceDeviceType: string;
|
let resourceDeviceType: string;
|
||||||
let application: ApplicationWithDeviceType | null = null;
|
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
||||||
|
null;
|
||||||
let device:
|
let device:
|
||||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||||
| null = null;
|
| null = null;
|
||||||
if (options.device != null) {
|
if (options.device != null) {
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
const rawDevice = await balena.models.device.get(options.device, {
|
||||||
const rawDevice = await balena.models.device.get(
|
$expand: { is_of__device_type: { $select: 'slug' } },
|
||||||
tryAsInteger(options.device),
|
});
|
||||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
|
||||||
);
|
|
||||||
if (!rawDevice.belongs_to__application) {
|
if (!rawDevice.belongs_to__application) {
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
throw new ExpectedError(stripIndent`
|
throw new ExpectedError(stripIndent`
|
||||||
@ -157,36 +160,40 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||||
} else {
|
} else {
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||||
application = (await getApplication(balena, options.fleet!, {
|
application = await this.getApplication(balena, options.fleet!);
|
||||||
$expand: {
|
|
||||||
is_for__device_type: { $select: 'slug' },
|
|
||||||
},
|
|
||||||
})) as ApplicationWithDeviceType;
|
|
||||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceType = options.deviceType || resourceDeviceType;
|
const deviceType = options.deviceType || resourceDeviceType;
|
||||||
|
|
||||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
|
||||||
deviceType,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check compatibility if application and deviceType provided
|
// Check compatibility if application and deviceType provided
|
||||||
if (options.fleet && options.deviceType) {
|
if (options.fleet && options.deviceType) {
|
||||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
|
||||||
resourceDeviceType,
|
|
||||||
);
|
|
||||||
|
|
||||||
const helpers = await import('../../utils/helpers');
|
const helpers = await import('../../utils/helpers');
|
||||||
if (
|
if (
|
||||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
!(await helpers.areDeviceTypesCompatible(
|
||||||
|
resourceDeviceType,
|
||||||
|
deviceType,
|
||||||
|
))
|
||||||
) {
|
) {
|
||||||
throw new balena.errors.BalenaInvalidDeviceType(
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
throw new ExpectedError(
|
||||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceManifest =
|
||||||
|
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
|
||||||
|
|
||||||
|
const { validateSecureBootOptionAndWarn } = await import(
|
||||||
|
'../../utils/config'
|
||||||
|
);
|
||||||
|
await validateSecureBootOptionAndWarn(
|
||||||
|
options.secureBoot,
|
||||||
|
deviceType,
|
||||||
|
options.version,
|
||||||
|
);
|
||||||
|
|
||||||
// Prompt for values
|
// Prompt for values
|
||||||
// Pass params as an override: if there is any param with exactly the same name as a
|
// Pass params as an override: if there is any param with exactly the same name as a
|
||||||
// required option, that value is used (and the corresponding question is not asked)
|
// required option, that value is used (and the corresponding question is not asked)
|
||||||
@ -195,7 +202,9 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
});
|
});
|
||||||
answers.version = options.version;
|
answers.version = options.version;
|
||||||
answers.developmentMode = options.dev;
|
answers.developmentMode = options.dev;
|
||||||
|
answers.secureBoot = options.secureBoot;
|
||||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||||
|
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||||
|
|
||||||
// Generate config
|
// Generate config
|
||||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||||
@ -235,7 +244,9 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
protected readonly deviceTypeNotAllowedMessage =
|
protected readonly deviceTypeNotAllowedMessage =
|
||||||
'The --deviceType option can only be used alongside the --fleet option';
|
'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');
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
|
||||||
if (options.device == null && options.fleet == null) {
|
if (options.device == null && options.fleet == null) {
|
||||||
@ -245,6 +256,8 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
if (!options.fleet && options.deviceType) {
|
if (!options.fleet && options.deviceType) {
|
||||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||||
}
|
}
|
||||||
|
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||||
|
options.version = normalizeOsVersion(options.version);
|
||||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||||
await validateDevOptionAndWarn(options.dev, options.version);
|
await validateDevOptionAndWarn(options.dev, options.version);
|
||||||
}
|
}
|
@ -15,21 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type?: string;
|
|
||||||
drive?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
file: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigInjectCmd extends Command {
|
export default class ConfigInjectCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Inject a config.json file to a balenaOS image or attached media.
|
Inject a config.json file to a balenaOS image or attached media.
|
||||||
@ -46,29 +36,22 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
file: Args.string({
|
||||||
name: 'file',
|
|
||||||
description: 'the path to the config.json file to inject',
|
description: 'the path to the config.json file to inject',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'config inject <file>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
...cf.deviceTypeIgnored,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
|
||||||
ConfigInjectCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
@ -15,18 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type?: string;
|
|
||||||
drive?: string;
|
|
||||||
help: void;
|
|
||||||
json: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigReadCmd extends Command {
|
export default class ConfigReadCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Read the config.json file of a balenaOS image or attached media.
|
Read the config.json file of a balenaOS image or attached media.
|
||||||
@ -44,12 +36,8 @@ export default class ConfigReadCmd extends Command {
|
|||||||
'$ balena config read --drive balena.img',
|
'$ balena config read --drive balena.img',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'config read';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
...cf.deviceTypeIgnored,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,7 +45,7 @@ export default class ConfigReadCmd extends Command {
|
|||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
const { flags: options } = await this.parse(ConfigReadCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
@ -15,19 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type?: string;
|
|
||||||
drive?: string;
|
|
||||||
advanced: boolean;
|
|
||||||
help: void;
|
|
||||||
version?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigReconfigureCmd extends Command {
|
export default class ConfigReconfigureCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Interactively reconfigure a balenaOS image file or attached media.
|
Interactively reconfigure a balenaOS image file or attached media.
|
||||||
@ -47,17 +39,13 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
'$ balena config reconfigure --drive balena.img --advanced',
|
'$ balena config reconfigure --drive balena.img --advanced',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'config reconfigure';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
...cf.deviceTypeIgnored,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
advanced: flags.boolean({
|
advanced: Flags.boolean({
|
||||||
description: 'show advanced commands',
|
description: 'show advanced commands',
|
||||||
char: 'v',
|
char: 'v',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
version: Flags.string({
|
||||||
version: flags.string({
|
|
||||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -66,7 +54,7 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
public static root = true;
|
public static root = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
const { flags: options } = await this.parse(ConfigReconfigureCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
@ -15,22 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
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 {
|
export default class ConfigWriteCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Write a key-value pair to the config.json file of an OS image or attached media.
|
Write a key-value pair to the config.json file of an OS image or attached media.
|
||||||
@ -48,34 +37,26 @@ export default class ConfigWriteCmd extends Command {
|
|||||||
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
key: Args.string({
|
||||||
name: 'key',
|
|
||||||
description: 'the key of the config parameter to write',
|
description: 'the key of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
description: 'the value of the config parameter to write',
|
description: 'the value of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'config write <key> <value>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
...cf.deviceTypeIgnored,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
|
||||||
ConfigWriteCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||||
|
|
@ -15,59 +15,56 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args, Flags } from '@oclif/core';
|
||||||
import type { ImageDescriptor } from 'resin-compose-parse';
|
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||||
|
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import {
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
} from '../utils/messages';
|
} from '../../utils/messages';
|
||||||
import * as ca from '../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../../utils/compose';
|
||||||
import type {
|
import type {
|
||||||
BuiltImage,
|
BuiltImage,
|
||||||
ComposeCliFlags,
|
ComposeCliFlags,
|
||||||
ComposeOpts,
|
ComposeOpts,
|
||||||
Release as ComposeReleaseInfo,
|
Release as ComposeReleaseInfo,
|
||||||
} from '../utils/compose-types';
|
} from '../../utils/compose-types';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||||
import {
|
import {
|
||||||
applyReleaseTagKeysAndValues,
|
applyReleaseTagKeysAndValues,
|
||||||
buildProject,
|
buildProject,
|
||||||
composeCliFlags,
|
composeCliFlags,
|
||||||
isBuildConfig,
|
isBuildConfig,
|
||||||
parseReleaseTagKeysAndValues,
|
parseReleaseTagKeysAndValues,
|
||||||
} from '../utils/compose_ts';
|
} from '../../utils/compose_ts';
|
||||||
import { dockerCliFlags } from '../utils/docker';
|
import { dockerCliFlags } from '../../utils/docker';
|
||||||
import type {
|
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
|
||||||
Application,
|
|
||||||
ApplicationType,
|
|
||||||
DeviceType,
|
|
||||||
Release,
|
|
||||||
} from 'balena-sdk';
|
|
||||||
|
|
||||||
interface ApplicationWithArch extends Application {
|
interface ApplicationWithArch {
|
||||||
|
id: number;
|
||||||
arch: string;
|
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 {
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
source?: string;
|
source?: string;
|
||||||
build: boolean;
|
build: boolean;
|
||||||
nologupload: boolean;
|
nologupload: boolean;
|
||||||
'release-tag'?: string[];
|
'release-tag'?: string[];
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
|
note?: string;
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeployCmd extends Command {
|
export default class DeployCmd extends Command {
|
||||||
public static description = `\
|
public static description = `\
|
||||||
Deploy a single image or a multicontainer project to a balena fleet.
|
Deploy a single image or a multicontainer project to a balena fleet.
|
||||||
@ -101,35 +98,31 @@ ${dockerignoreHelp}
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena deploy myFleet',
|
'$ balena deploy myFleet',
|
||||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
||||||
|
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
|
||||||
'$ balena deploy myorg/myfleet myRepo/myImage',
|
'$ balena deploy myorg/myfleet myRepo/myImage',
|
||||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
ca.fleetRequired,
|
fleet: ca.fleetRequired,
|
||||||
{
|
image: Args.string({ description: 'the image to deploy' }),
|
||||||
name: 'image',
|
};
|
||||||
description: 'the image to deploy',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'deploy <fleet> [image]';
|
public static flags = {
|
||||||
// TODO: docker-compose naming
|
source: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
source: flags.string({
|
|
||||||
description:
|
description:
|
||||||
'specify an alternate source directory; default is the working directory',
|
'specify an alternate source directory; default is the working directory',
|
||||||
char: 's',
|
char: 's',
|
||||||
}),
|
}),
|
||||||
build: flags.boolean({
|
build: Flags.boolean({
|
||||||
description: 'force a rebuild before deploy',
|
description: 'force a rebuild before deploy',
|
||||||
char: 'b',
|
char: 'b',
|
||||||
}),
|
}),
|
||||||
nologupload: flags.boolean({
|
nologupload: Flags.boolean({
|
||||||
description:
|
description:
|
||||||
"don't upload build logs to the dashboard with image (if building)",
|
"don't upload build logs to the dashboard with image (if building)",
|
||||||
}),
|
}),
|
||||||
'release-tag': flags.string({
|
'release-tag': Flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Set release tags if the image deployment is successful. Multiple
|
Set release tags if the image deployment is successful. Multiple
|
||||||
arguments may be provided, alternating tag keys and values (see examples).
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
@ -137,7 +130,7 @@ ${dockerignoreHelp}
|
|||||||
`,
|
`,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
}),
|
}),
|
||||||
draft: flags.boolean({
|
draft: Flags.boolean({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Deploy the release as a draft. Draft releases are ignored
|
Deploy the release as a draft. Draft releases are ignored
|
||||||
by the 'track latest' release policy but can be used through release pinning.
|
by the 'track latest' release policy but can be used through release pinning.
|
||||||
@ -145,11 +138,12 @@ ${dockerignoreHelp}
|
|||||||
as final by default unless this option is given.`,
|
as final by default unless this option is given.`,
|
||||||
default: false,
|
default: false,
|
||||||
}),
|
}),
|
||||||
|
note: Flags.string({ description: 'The notes for this release' }),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
// Revisit this in future release.
|
// Revisit this in future release.
|
||||||
help: flags.help({}),
|
help: Flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
@ -157,9 +151,7 @@ ${dockerignoreHelp}
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeployCmd);
|
||||||
DeployCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
(await import('events')).defaultMaxListeners = 1000;
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
@ -181,7 +173,7 @@ ${dockerignoreHelp}
|
|||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||||
'../utils/compose_ts'
|
'../../utils/compose_ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||||
@ -189,7 +181,7 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
options['registry-secrets'] = await getRegistrySecrets(
|
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
|
||||||
sdk,
|
sdk,
|
||||||
options['registry-secrets'],
|
options['registry-secrets'],
|
||||||
);
|
);
|
||||||
@ -202,16 +194,16 @@ ${dockerignoreHelp}
|
|||||||
registrySecretsPath: options['registry-secrets'],
|
registrySecretsPath: options['registry-secrets'],
|
||||||
});
|
});
|
||||||
options.dockerfile = dockerfilePath;
|
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 app = await helpers.getAppWithArch(fleet);
|
||||||
|
|
||||||
const dockerUtils = await import('../utils/docker');
|
const dockerUtils = await import('../../utils/docker');
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
dockerUtils.getDocker(options),
|
dockerUtils.getDocker(options),
|
||||||
dockerUtils.generateBuildOpts(options),
|
dockerUtils.generateBuildOpts(options as FlagsDef),
|
||||||
compose.generateOpts(options),
|
compose.generateOpts(options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -231,11 +223,14 @@ ${dockerignoreHelp}
|
|||||||
releaseTagKeys,
|
releaseTagKeys,
|
||||||
releaseTagValues,
|
releaseTagValues,
|
||||||
);
|
);
|
||||||
|
if (options.note) {
|
||||||
|
await sdk.models.release.setNote(release.id, options.note);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deployProject(
|
async deployProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../utils/logger'),
|
logger: import('../../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
app: ApplicationWithArch; // the application instance to deploy to
|
app: ApplicationWithArch; // the application instance to deploy to
|
||||||
@ -253,10 +248,10 @@ ${dockerignoreHelp}
|
|||||||
const doodles = await import('resin-doodles');
|
const doodles = await import('resin-doodles');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { deployProject: $deployProject, loadProject } = await import(
|
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 {
|
try {
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
@ -313,7 +308,7 @@ ${dockerignoreHelp}
|
|||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
composition: compositionToBuild,
|
composition: compositionToBuild,
|
||||||
arch: opts.app.arch,
|
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,
|
emulated: opts.buildEmulated,
|
||||||
buildOpts: opts.buildOpts,
|
buildOpts: opts.buildOpts,
|
||||||
inlineLogs: composeOpts.inlineLogs,
|
inlineLogs: composeOpts.inlineLogs,
|
||||||
@ -334,17 +329,17 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
let release: Release | ComposeReleaseInfo['release'];
|
let release: Release | ComposeReleaseInfo['release'];
|
||||||
if (appType?.is_legacy) {
|
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
const { deployLegacy } = require('../../utils/deploy-legacy');
|
||||||
|
|
||||||
const msg = getChalk().yellow(
|
const msg = getChalk().yellow(
|
||||||
'Target fleet requires legacy deploy method.',
|
'Target fleet requires legacy deploy method.',
|
||||||
);
|
);
|
||||||
logger.logWarn(msg);
|
logger.logWarn(msg);
|
||||||
|
|
||||||
const [token, username, url, options] = await Promise.all([
|
const [token, { username }, url, options] = await Promise.all([
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
sdk.auth.whoami(),
|
sdk.auth.getUserInfo(),
|
||||||
sdk.settings.get('balenaUrl'),
|
sdk.settings.get('balenaUrl'),
|
||||||
{
|
{
|
||||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||||
@ -367,20 +362,13 @@ ${dockerignoreHelp}
|
|||||||
$select: ['commit'],
|
$select: ['commit'],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
|
||||||
sdk.auth.getUserId(),
|
|
||||||
sdk.auth.getToken(),
|
|
||||||
sdk.settings.get('apiUrl'),
|
|
||||||
]);
|
|
||||||
release = await $deployProject(
|
release = await $deployProject(
|
||||||
docker,
|
docker,
|
||||||
|
sdk,
|
||||||
logger,
|
logger,
|
||||||
project.composition,
|
project.composition,
|
||||||
images,
|
images,
|
||||||
opts.app.id,
|
opts.app.id,
|
||||||
userId,
|
|
||||||
`Bearer ${auth}`,
|
|
||||||
apiEndpoint,
|
|
||||||
!opts.shouldUploadLogs,
|
!opts.shouldUploadLogs,
|
||||||
composeOpts.projectPath,
|
composeOpts.projectPath,
|
||||||
opts.createAsDraft,
|
opts.createAsDraft,
|
@ -15,21 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceDeactivateCmd extends Command {
|
export default class DeviceDeactivateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Deactivate a device.
|
Deactivate a device.
|
||||||
@ -44,27 +34,22 @@ export default class DeviceDeactivateCmd extends Command {
|
|||||||
'$ balena device deactivate 7cf02a6 --yes',
|
'$ balena device deactivate 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the UUID of the device to be deactivated',
|
description: 'the UUID of the device to be deactivated',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device deactivate <uuid>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceDeactivateCmd,
|
await this.parse(DeviceDeactivateCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
@ -15,22 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceIdentifyCmd extends Command {
|
export default class DeviceIdentifyCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Identify a device.
|
Identify a device.
|
||||||
@ -39,25 +28,17 @@ export default class DeviceIdentifyCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device identify 23c73a1'];
|
public static examples = ['$ balena device identify 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to identify',
|
description: 'the uuid of the device to identify',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'device identify <uuid>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
const { args: params } = await this.parse(DeviceIdentifyCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,13 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
import { jsonInfo } from '../../utils/messages';
|
||||||
|
|
||||||
import type { Application, Release } from 'balena-sdk';
|
import type { Application, Release } from 'balena-sdk';
|
||||||
|
|
||||||
@ -42,72 +41,90 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
|||||||
undervoltage_detected?: boolean;
|
undervoltage_detected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceCmd extends Command {
|
export default class DeviceCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Show info about a single device.
|
Show info about a single device.
|
||||||
|
|
||||||
Show information about a single device.
|
Show information about a single device.
|
||||||
`;
|
|
||||||
public static examples = ['$ balena device 7cf02a6'];
|
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
${jsonInfo.split('\n').join('\n\t\t')}
|
||||||
{
|
`;
|
||||||
name: 'uuid',
|
public static examples = [
|
||||||
description: 'the device uuid',
|
'$ balena device 7cf02a6',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
'$ balena device 7cf02a6 --view',
|
||||||
required: true,
|
'$ balena device 7cf02a6 --json',
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'device <uuid>';
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: 'the device uuid',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
json: cf.json,
|
||||||
|
view: Flags.boolean({
|
||||||
|
default: false,
|
||||||
|
description: 'open device dashboard page',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
const { args: params, flags: options } = await this.parse(DeviceCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const device = (await balena.models.device.get(params.uuid, {
|
const device = (await balena.models.device.get(
|
||||||
$select: [
|
params.uuid,
|
||||||
'device_name',
|
options.json
|
||||||
'id',
|
? {
|
||||||
'overall_status',
|
$expand: {
|
||||||
'is_online',
|
device_tag: {
|
||||||
'ip_address',
|
$select: ['tag_key', 'value'],
|
||||||
'mac_address',
|
},
|
||||||
'last_connectivity_event',
|
...expandForAppName.$expand,
|
||||||
'uuid',
|
},
|
||||||
'supervisor_version',
|
}
|
||||||
'is_web_accessible',
|
: {
|
||||||
'note',
|
$select: [
|
||||||
'os_version',
|
'device_name',
|
||||||
'memory_usage',
|
'id',
|
||||||
'memory_total',
|
'overall_status',
|
||||||
'public_address',
|
'is_online',
|
||||||
'storage_block_device',
|
'ip_address',
|
||||||
'storage_usage',
|
'mac_address',
|
||||||
'storage_total',
|
'last_connectivity_event',
|
||||||
'cpu_usage',
|
'uuid',
|
||||||
'cpu_temp',
|
'supervisor_version',
|
||||||
'cpu_id',
|
'is_web_accessible',
|
||||||
'is_undervolted',
|
'note',
|
||||||
],
|
'os_version',
|
||||||
...expandForAppName,
|
'memory_usage',
|
||||||
})) as ExtendedDevice;
|
'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');
|
||||||
|
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
|
||||||
|
await open(dashboardUrl, { wait: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
device.status = device.overall_status;
|
device.status = device.overall_status;
|
||||||
|
|
||||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||||
@ -163,6 +180,11 @@ export default class DeviceCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(device, null, 4));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
getVisuals().table.vertical(device, [
|
getVisuals().table.vertical(device, [
|
||||||
`$${device.device_name}$`,
|
`$${device.device_name}$`,
|
@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
@ -29,8 +29,8 @@ interface FlagsDef {
|
|||||||
'os-version'?: string;
|
'os-version'?: string;
|
||||||
drive?: string;
|
drive?: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
help: void;
|
|
||||||
'provisioning-key-name'?: string;
|
'provisioning-key-name'?: string;
|
||||||
|
'provisioning-key-expiry-date'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DeviceInitCmd extends Command {
|
export default class DeviceInitCmd extends Command {
|
||||||
@ -69,19 +69,18 @@ export default class DeviceInitCmd extends Command {
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device init',
|
'$ balena device init',
|
||||||
'$ balena device init -f myorg/myfleet',
|
'$ balena device init -f myorg/myfleet',
|
||||||
|
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
|
||||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
|
'$ 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 = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
advanced: flags.boolean({
|
advanced: Flags.boolean({
|
||||||
char: 'v',
|
char: 'v',
|
||||||
description: 'show advanced configuration options',
|
description: 'show advanced configuration options',
|
||||||
}),
|
}),
|
||||||
'os-version': flags.string({
|
'os-version': Flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
exact version number, or a valid semver range,
|
exact version number, or a valid semver range,
|
||||||
or 'latest' (includes pre-releases),
|
or 'latest' (includes pre-releases),
|
||||||
@ -91,19 +90,22 @@ export default class DeviceInitCmd extends Command {
|
|||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
drive: cf.drive,
|
drive: cf.drive,
|
||||||
config: flags.string({
|
config: Flags.string({
|
||||||
description: 'path to the config JSON file, see `balena os build-config`',
|
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',
|
description: 'custom key name assigned to generated provisioning api key',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
'provisioning-key-expiry-date': Flags.string({
|
||||||
|
description:
|
||||||
|
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
|
const { flags: options } = await this.parse(DeviceInitCmd);
|
||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
const { promisify } = await import('util');
|
const { promisify } = await import('util');
|
||||||
@ -118,20 +120,16 @@ export default class DeviceInitCmd extends Command {
|
|||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
// Get application and
|
// Get application and
|
||||||
const application = (await getApplication(
|
const application = options.fleet
|
||||||
balena,
|
? await getApplication(balena, options.fleet, {
|
||||||
options.fleet ||
|
$select: ['id', 'slug'],
|
||||||
(
|
$expand: {
|
||||||
await (await import('../../utils/patterns')).selectApplication()
|
is_for__device_type: {
|
||||||
).id,
|
$select: 'slug',
|
||||||
{
|
},
|
||||||
$expand: {
|
|
||||||
is_for__device_type: {
|
|
||||||
$select: 'slug',
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
: await (await import('../../utils/patterns')).selectApplication();
|
||||||
)) as ApplicationWithDeviceType;
|
|
||||||
|
|
||||||
// Register new device
|
// Register new device
|
||||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
const deviceUuid = balena.models.device.generateUniqueKey();
|
||||||
@ -185,6 +183,14 @@ export default class DeviceInitCmd extends Command {
|
|||||||
options['provisioning-key-name'],
|
options['provisioning-key-name'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options['provisioning-key-expiry-date']) {
|
||||||
|
configureCommand.push(
|
||||||
|
'--provisioning-key-expiry-date',
|
||||||
|
options['provisioning-key-expiry-date'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await runCommand(configureCommand);
|
await runCommand(configureCommand);
|
||||||
}
|
}
|
||||||
|
|
@ -15,28 +15,29 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||||
|
|
||||||
import type { Application } from 'balena-sdk';
|
import type { Device, PineOptions } from 'balena-sdk';
|
||||||
|
|
||||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
const devicesSelectFields = {
|
||||||
dashboard_url?: string;
|
$select: [
|
||||||
fleet?: string | null; // 'org/name' slug
|
'id',
|
||||||
device_type?: string | null;
|
'uuid',
|
||||||
}
|
'device_name',
|
||||||
|
'status',
|
||||||
|
'is_online',
|
||||||
|
'supervisor_version',
|
||||||
|
'os_version',
|
||||||
|
],
|
||||||
|
} satisfies PineOptions<Device>;
|
||||||
|
|
||||||
interface FlagsDef {
|
export default class DeviceListCmd extends Command {
|
||||||
fleet?: string;
|
public static aliases = ['devices'];
|
||||||
help: void;
|
|
||||||
json: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicesCmd extends Command {
|
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List all devices.
|
List all devices.
|
||||||
|
|
||||||
@ -49,17 +50,14 @@ export default class DevicesCmd extends Command {
|
|||||||
${jsonInfo.split('\n').join('\n\t\t')}
|
${jsonInfo.split('\n').join('\n\t\t')}
|
||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena devices',
|
'$ balena device list',
|
||||||
'$ balena devices --fleet MyFleet',
|
'$ balena device list --fleet MyFleet',
|
||||||
'$ balena devices -f myorg/myfleet',
|
'$ balena device list -f myorg/myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'devices';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
@ -67,39 +65,42 @@ export default class DevicesCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
const { flags: options } = await this.parse(DeviceListCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
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) {
|
return await balena.pine.get({
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
resource: 'device',
|
||||||
const application = await getApplication(balena, options.fleet);
|
options: devicesOptions,
|
||||||
devices = (await balena.models.device.getAllByApplication(
|
});
|
||||||
application.id,
|
})()
|
||||||
expandForAppName,
|
).map((device) => ({
|
||||||
)) as ExtendedDevice[];
|
...device,
|
||||||
} else {
|
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
|
||||||
devices = (await balena.models.device.getAll(
|
fleet: device.belongs_to__application?.[0]?.slug || null,
|
||||||
expandForAppName,
|
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
|
||||||
)) as ExtendedDevice[];
|
device_type: device.is_of__device_type?.[0]?.slug || null,
|
||||||
}
|
}));
|
||||||
|
|
||||||
devices = devices.map(function (device) {
|
const fields: Array<keyof (typeof devices)[number]> = [
|
||||||
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 = [
|
|
||||||
'id',
|
'id',
|
||||||
'uuid',
|
'uuid',
|
||||||
'device_name',
|
'device_name',
|
@ -15,23 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
enable: boolean;
|
|
||||||
disable: boolean;
|
|
||||||
status: boolean;
|
|
||||||
help?: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceLocalModeCmd extends Command {
|
export default class DeviceLocalModeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -48,39 +34,33 @@ export default class DeviceLocalModeCmd extends Command {
|
|||||||
'$ balena device local-mode 23c73a1 --status',
|
'$ balena device local-mode 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device local-mode <uuid>';
|
public static flags = {
|
||||||
|
enable: Flags.boolean({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
enable: flags.boolean({
|
|
||||||
description: 'enable local mode',
|
description: 'enable local mode',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: flags.boolean({
|
disable: Flags.boolean({
|
||||||
description: 'disable local mode',
|
description: 'disable local mode',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: flags.boolean({
|
status: Flags.boolean({
|
||||||
description: 'output boolean indicating local mode status',
|
description: 'output boolean indicating local mode status',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceLocalModeCmd,
|
await this.parse(DeviceLocalModeCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,12 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import type {
|
import type {
|
||||||
BalenaSDK,
|
BalenaSDK,
|
||||||
Device,
|
Device,
|
||||||
DeviceType,
|
PineOptions,
|
||||||
PineTypedResult,
|
PineTypedResult,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
@ -29,22 +28,6 @@ import { ExpectedError } from '../../errors';
|
|||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
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 {
|
export default class DeviceMoveCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Move one or more devices to another fleet.
|
Move one or more devices to another fleet.
|
||||||
@ -63,68 +46,67 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device move <uuid(s)>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const deviceOptions = {
|
||||||
DeviceMoveCmd,
|
$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 balena = getBalenaSdk();
|
||||||
|
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
// Split uuids string into array of uuids
|
||||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
const deviceUuids = params.uuid.split(',');
|
||||||
|
|
||||||
// Parse ids string into array of correct types
|
const devices = await this.getDevices(balena, deviceUuids);
|
||||||
const deviceIds: Array<string | number> = params.uuid
|
|
||||||
.split(',')
|
|
||||||
.map((id) => tryAsInteger(id));
|
|
||||||
|
|
||||||
// Get devices
|
// Disambiguate application
|
||||||
const devices = await Promise.all(
|
|
||||||
deviceIds.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';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
// Get destination application
|
// Get destination application
|
||||||
const application = options.fleet
|
const application = options.fleet
|
||||||
? await getApplication(balena, options.fleet)
|
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
|
||||||
: await this.interactivelySelectApplication(balena, devices);
|
: await this.interactivelySelectApplication(balena, devices);
|
||||||
|
|
||||||
// Move each device
|
// Move each device
|
||||||
for (const uuid of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
try {
|
try {
|
||||||
await balena.models.device.move(uuid, application.id);
|
await balena.models.device.move(uuid, application.id);
|
||||||
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
||||||
@ -137,9 +119,8 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
|
|
||||||
async interactivelySelectApplication(
|
async interactivelySelectApplication(
|
||||||
balena: BalenaSDK,
|
balena: BalenaSDK,
|
||||||
devices: ExtendedDevice[],
|
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
||||||
) {
|
) {
|
||||||
const { getExpandedProp } = await import('../../utils/pine');
|
|
||||||
// deduplicate the slugs
|
// deduplicate the slugs
|
||||||
const deviceCpuArchs = Array.from(
|
const deviceCpuArchs = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@ -149,46 +130,44 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceTypeOptions = {
|
const allCpuArches = await balena.pine.get({
|
||||||
$select: 'slug',
|
resource: 'cpu_architecture',
|
||||||
$expand: {
|
options: {
|
||||||
is_of__cpu_architecture: {
|
$select: ['id', 'slug'],
|
||||||
$select: 'slug',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
});
|
||||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
|
||||||
deviceTypeOptions,
|
|
||||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
|
||||||
|
|
||||||
const compatibleDeviceTypeSlugs = new Set(
|
const compatibleCpuArchIds = allCpuArches
|
||||||
deviceTypes
|
.filter((cpuArch) => {
|
||||||
.filter((deviceType) => {
|
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||||
const deviceTypeArch = getExpandedProp(
|
balena.models.os.isArchitectureCompatibleWith(
|
||||||
deviceType.is_of__cpu_architecture,
|
deviceCpuArch,
|
||||||
'slug',
|
cpuArch.slug,
|
||||||
)!;
|
),
|
||||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
);
|
||||||
balena.models.os.isArchitectureCompatibleWith(
|
})
|
||||||
deviceCpuArch,
|
.map((deviceType) => deviceType.id);
|
||||||
deviceTypeArch,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((deviceType) => deviceType.slug),
|
|
||||||
);
|
|
||||||
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
try {
|
try {
|
||||||
const application = await patterns.selectApplication(
|
const application = await patterns.selectApplication(
|
||||||
(app) =>
|
{
|
||||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
is_for__device_type: {
|
||||||
devices.some((device) => device.application_name !== app.app_name),
|
$any: {
|
||||||
|
$alias: 'dt',
|
||||||
|
$expr: {
|
||||||
|
dt: {
|
||||||
|
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return application;
|
return application;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!compatibleDeviceTypeSlugs.size) {
|
if (!compatibleCpuArchIds.length) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||||
);
|
);
|
@ -15,25 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import type { Device } from 'balena-sdk';
|
import type { Device } from 'balena-sdk';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
version?: string;
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceOsUpdateCmd extends Command {
|
export default class DeviceOsUpdateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Start a Host OS update for a device.
|
Start a Host OS update for a device.
|
||||||
@ -47,34 +35,36 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device os-update 23c73a1',
|
'$ 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 --version 2.31.0+rev1.prod',
|
||||||
|
'$ balena device os-update 23c73a1 --include-draft',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to update',
|
description: 'the uuid of the device to update',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device os-update <uuid>';
|
public static flags = {
|
||||||
|
version: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
version: flags.string({
|
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
|
exclusive: ['include-draft'],
|
||||||
|
}),
|
||||||
|
'include-draft': Flags.boolean({
|
||||||
|
description: 'include pre-release balenaOS versions',
|
||||||
|
default: false,
|
||||||
|
exclusive: ['version'],
|
||||||
}),
|
}),
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceOsUpdateCmd,
|
await this.parse(DeviceOsUpdateCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
@ -100,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
|
// Get supported OS update versions
|
||||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||||
is_of__device_type[0].slug,
|
is_of__device_type[0].slug,
|
||||||
currentOsVersion,
|
currentOsVersion,
|
||||||
|
{
|
||||||
|
includeDraft,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (hupVersionInfo.versions.length === 0) {
|
if (hupVersionInfo.versions.length === 0) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
@ -114,6 +119,8 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
// Get target OS version
|
// Get target OS version
|
||||||
let targetOsVersion = options.version;
|
let targetOsVersion = options.version;
|
||||||
if (targetOsVersion != null) {
|
if (targetOsVersion != null) {
|
||||||
|
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||||
|
targetOsVersion = normalizeOsVersion(targetOsVersion);
|
||||||
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
84
src/commands/device/pin.ts
Normal file
84
src/commands/device/pin.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
import { getExpandedProp } from '../../utils/pine';
|
||||||
|
|
||||||
|
export default class DevicePinCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Pin a device to a release.
|
||||||
|
|
||||||
|
Pin a device to a release.
|
||||||
|
|
||||||
|
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
|
||||||
|
`;
|
||||||
|
public static examples = [
|
||||||
|
'$ balena device pin 7cf02a6',
|
||||||
|
'$ balena device pin 7cf02a6 91165e5',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: 'the uuid of the device to pin to a release',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
releaseToPinTo: Args.string({
|
||||||
|
description: 'the commit of the release for the device to get pinned to',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = await this.parse(DevicePinCmd);
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
const device = await balena.models.device.get(params.uuid, {
|
||||||
|
$expand: {
|
||||||
|
should_be_running__release: {
|
||||||
|
$select: 'commit',
|
||||||
|
},
|
||||||
|
belongs_to__application: {
|
||||||
|
$select: 'slug',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinnedRelease = getExpandedProp(
|
||||||
|
device.should_be_running__release,
|
||||||
|
'commit',
|
||||||
|
);
|
||||||
|
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
||||||
|
|
||||||
|
const releaseToPinTo = params.releaseToPinTo;
|
||||||
|
|
||||||
|
if (!releaseToPinTo) {
|
||||||
|
console.log(
|
||||||
|
`${
|
||||||
|
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}\`.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,26 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
enable: boolean;
|
|
||||||
disable: boolean;
|
|
||||||
status: boolean;
|
|
||||||
help?: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
// Optional hidden arg to support old command format
|
|
||||||
legacyUuid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicePublicUrlCmd extends Command {
|
export default class DevicePublicUrlCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -43,9 +27,6 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
This command will output the current public URL for the
|
This command will output the current public URL for the
|
||||||
specified device. It can also enable or disable the URL,
|
specified device. It can also enable or disable the URL,
|
||||||
or output the enabled status, using the respective options.
|
or output the enabled status, using the respective options.
|
||||||
|
|
||||||
The old command style 'balena device public-url enable <uuid>'
|
|
||||||
is deprecated, but still supported.
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
@ -55,64 +36,33 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
'$ balena device public-url 23c73a1 --status',
|
'$ balena device public-url 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
};
|
||||||
// Optional hidden arg to support old command format
|
|
||||||
name: 'legacyUuid',
|
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'device public-url <uuid>';
|
public static flags = {
|
||||||
|
enable: Flags.boolean({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
enable: flags.boolean({
|
|
||||||
description: 'enable the public URL',
|
description: 'enable the public URL',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: flags.boolean({
|
disable: Flags.boolean({
|
||||||
description: 'disable the public URL',
|
description: 'disable the public URL',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: flags.boolean({
|
status: Flags.boolean({
|
||||||
description: 'determine if public URL is enabled',
|
description: 'determine if public URL is enabled',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DevicePublicUrlCmd,
|
await this.parse(DevicePublicUrlCmd);
|
||||||
);
|
|
||||||
|
|
||||||
// Legacy command format support.
|
|
||||||
// Previously this command used the following format
|
|
||||||
// (changed due to oclif technicalities):
|
|
||||||
// `balena device public-url enable|disable|status <uuid>`
|
|
||||||
if (params.legacyUuid) {
|
|
||||||
const action = params.uuid;
|
|
||||||
if (!['enable', 'disable', 'status'].includes(action)) {
|
|
||||||
throw new ExpectedError(
|
|
||||||
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.enable = action === 'enable';
|
|
||||||
options.disable = action === 'disable';
|
|
||||||
options.status = action === 'status';
|
|
||||||
params.uuid = params.legacyUuid;
|
|
||||||
delete params.legacyUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,20 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicePurgeCmd extends Command {
|
export default class DevicePurgeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Purge data from a device.
|
Purge data from a device.
|
||||||
@ -44,36 +34,26 @@ export default class DevicePurgeCmd extends Command {
|
|||||||
'$ balena device purge 55d43b3,23c73a1',
|
'$ balena device purge 55d43b3,23c73a1',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'device purge <uuid>';
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
public static args: Array<IArg<any>> = [
|
|
||||||
{
|
|
||||||
name: 'uuid',
|
|
||||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
const { args: params } = await this.parse(DevicePurgeCmd);
|
||||||
|
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
||||||
|
|
||||||
const deviceIds = params.uuid.split(',').map((id) => {
|
const deviceUuids = params.uuid.split(',');
|
||||||
return tryAsInteger(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const deviceId of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
ux.action.start(`Purging data from device ${deviceId}`);
|
ux.action.start(`Purging data from device ${uuid}`);
|
||||||
await balena.models.device.purge(deviceId);
|
await balena.models.device.purge(uuid);
|
||||||
ux.action.stop();
|
ux.action.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,21 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
force: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRebootCmd extends Command {
|
export default class DeviceRebootCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -39,28 +28,21 @@ export default class DeviceRebootCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device reboot 23c73a1'];
|
public static examples = ['$ balena device reboot 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to reboot',
|
description: 'the uuid of the device to reboot',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device reboot <uuid>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRebootCmd);
|
||||||
DeviceRebootCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,23 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
uuid?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRegisterCmd extends Command {
|
export default class DeviceRegisterCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Register a new device.
|
Register a new device.
|
||||||
@ -47,37 +36,46 @@ export default class DeviceRegisterCmd extends Command {
|
|||||||
'$ balena device register MyFleet',
|
'$ balena device register MyFleet',
|
||||||
'$ balena device register MyFleet --uuid <uuid>',
|
'$ balena device register MyFleet --uuid <uuid>',
|
||||||
'$ balena device register myorg/myfleet --uuid <uuid>',
|
'$ balena device register myorg/myfleet --uuid <uuid>',
|
||||||
|
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [ca.fleetRequired];
|
public static args = {
|
||||||
|
fleet: ca.fleetRequired,
|
||||||
|
};
|
||||||
|
|
||||||
public static usage = 'device register <fleet>';
|
public static flags = {
|
||||||
|
uuid: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
uuid: flags.string({
|
|
||||||
description: 'custom uuid',
|
description: 'custom uuid',
|
||||||
char: 'u',
|
char: 'u',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
deviceType: Flags.string({
|
||||||
|
description:
|
||||||
|
"device type slug (run 'balena devices supported' for possible values)",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceRegisterCmd,
|
await this.parse(DeviceRegisterCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const application = await getApplication(balena, params.fleet);
|
const application = await getApplication(balena, params.fleet, {
|
||||||
|
$select: ['id', 'slug'],
|
||||||
|
});
|
||||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||||
|
|
||||||
console.info(`Registering to ${application.slug}: ${uuid}`);
|
console.info(`Registering to ${application.slug}: ${uuid}`);
|
||||||
|
|
||||||
const result = await balena.models.device.register(application.id, uuid);
|
const result = await balena.models.device.register(
|
||||||
|
application.id,
|
||||||
|
uuid,
|
||||||
|
options.deviceType,
|
||||||
|
);
|
||||||
|
|
||||||
return result && result.uuid;
|
return result && result.uuid;
|
||||||
}
|
}
|
@ -15,21 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
newName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRenameCmd extends Command {
|
export default class DeviceRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -44,29 +32,20 @@ export default class DeviceRenameCmd extends Command {
|
|||||||
'$ balena device rename 7cf02a6 MyPi',
|
'$ balena device rename 7cf02a6 MyPi',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to rename',
|
description: 'the uuid of the device to rename',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
newName: Args.string({
|
||||||
name: 'newName',
|
|
||||||
description: 'the new name for the device',
|
description: 'the new name for the device',
|
||||||
},
|
}),
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'device rename <uuid> [newName]';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
|
const { args: params } = await this.parse(DeviceRenameCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,10 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
import type {
|
import type {
|
||||||
BalenaSDK,
|
BalenaSDK,
|
||||||
@ -26,15 +24,6 @@ import type {
|
|||||||
CurrentServiceWithCommit,
|
CurrentServiceWithCommit,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
service?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRestartCmd extends Command {
|
export default class DeviceRestartCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Restart containers on a device.
|
Restart containers on a device.
|
||||||
@ -55,51 +44,42 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
'$ balena device restart 23c73a1 -s myService1,myService2',
|
'$ balena device restart 23c73a1 -s myService1,myService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device restart <uuid>';
|
public static flags = {
|
||||||
|
service: Flags.string({
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
service: flags.string({
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of service names to restart',
|
'comma-separated list (no blank spaces) of service names to restart',
|
||||||
char: 's',
|
char: 's',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRestartCmd);
|
||||||
DeviceRestartCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
||||||
|
|
||||||
const deviceIds = params.uuid.split(',').map((id) => {
|
const deviceUuids = params.uuid.split(',');
|
||||||
return tryAsInteger(id);
|
|
||||||
});
|
|
||||||
const serviceNames = options.service?.split(',');
|
const serviceNames = options.service?.split(',');
|
||||||
|
|
||||||
// Iterate sequentially through deviceIds.
|
// Iterate sequentially through deviceUuids.
|
||||||
// We may later want to add a batching feature,
|
// We may later want to add a batching feature,
|
||||||
// so that n devices are processed in parallel
|
// so that n devices are processed in parallel
|
||||||
for (const deviceId of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
ux.action.start(`Restarting services on device ${deviceId}`);
|
ux.action.start(`Restarting services on device ${uuid}`);
|
||||||
if (serviceNames) {
|
if (serviceNames) {
|
||||||
await this.restartServices(balena, deviceId, serviceNames);
|
await this.restartServices(balena, uuid, serviceNames);
|
||||||
} else {
|
} else {
|
||||||
await this.restartAllServices(balena, deviceId);
|
await this.restartAllServices(balena, uuid);
|
||||||
}
|
}
|
||||||
ux.action.stop();
|
ux.action.stop();
|
||||||
}
|
}
|
||||||
@ -107,7 +87,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
|
|
||||||
async restartServices(
|
async restartServices(
|
||||||
balena: BalenaSDK,
|
balena: BalenaSDK,
|
||||||
deviceId: number | string,
|
deviceUuid: string,
|
||||||
serviceNames: string[],
|
serviceNames: string[],
|
||||||
) {
|
) {
|
||||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||||
@ -116,7 +96,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
// Get device
|
// Get device
|
||||||
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
||||||
try {
|
try {
|
||||||
device = await balena.models.device.getWithServiceDetails(deviceId, {
|
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
|
||||||
$expand: {
|
$expand: {
|
||||||
is_running__release: { $select: 'commit' },
|
is_running__release: { $select: 'commit' },
|
||||||
},
|
},
|
||||||
@ -124,7 +104,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -136,7 +116,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
serviceNames.forEach((service) => {
|
serviceNames.forEach((service) => {
|
||||||
if (!device.current_services[service]) {
|
if (!device.current_services[service]) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Service ${service} not found on device ${deviceId}.`,
|
`Service ${service} not found on device ${deviceUuid}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -155,7 +135,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
if (serviceContainer) {
|
if (serviceContainer) {
|
||||||
restartPromises.push(
|
restartPromises.push(
|
||||||
balena.models.device.restartService(
|
balena.models.device.restartService(
|
||||||
deviceId,
|
deviceUuid,
|
||||||
serviceContainer.image_id,
|
serviceContainer.image_id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -166,32 +146,32 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
await Promise.all(restartPromises);
|
await Promise.all(restartPromises);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.toLowerCase().includes('no online device')) {
|
if (e.message.toLowerCase().includes('no online device')) {
|
||||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
|
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
|
||||||
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
||||||
// Need to use device.get first to distinguish between non-existant and offline devices.
|
// Need to use device.get first to distinguish between non-existant and offline devices.
|
||||||
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||||
try {
|
try {
|
||||||
const device = await balena.models.device.get(deviceId);
|
const device = await balena.models.device.get(deviceUuid);
|
||||||
if (!device.is_online) {
|
if (!device.is_online) {
|
||||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await balena.models.device.restartApplication(deviceId);
|
await balena.models.device.restartApplication(deviceUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,21 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRmCmd extends Command {
|
export default class DeviceRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -46,28 +35,22 @@ export default class DeviceRmCmd extends Command {
|
|||||||
'$ balena device rm 7cf02a6 --yes',
|
'$ balena device rm 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device rm <uuid(s)>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRmCmd);
|
||||||
DeviceRmCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
@ -84,7 +67,7 @@ export default class DeviceRmCmd extends Command {
|
|||||||
// Remove
|
// Remove
|
||||||
for (const uuid of uuids) {
|
for (const uuid of uuids) {
|
||||||
try {
|
try {
|
||||||
await balena.models.device.remove(tryAsInteger(uuid));
|
await balena.models.device.remove(uuid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.info(`${err.message}, uuid: ${uuid}`);
|
console.info(`${err.message}, uuid: ${uuid}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
@ -15,23 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
force: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceShutdownCmd extends Command {
|
export default class DeviceShutdownCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Shutdown a device.
|
Shutdown a device.
|
||||||
@ -40,28 +29,22 @@ export default class DeviceShutdownCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device shutdown 23c73a1'];
|
public static examples = ['$ balena device shutdown 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to shutdown',
|
description: 'the uuid of the device to shutdown',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device shutdown <uuid>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceShutdownCmd,
|
await this.parse(DeviceShutdownCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
132
src/commands/device/start-service.ts
Normal file
132
src/commands/device/start-service.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
|
import type { BalenaSDK } from 'balena-sdk';
|
||||||
|
|
||||||
|
export default class DeviceStartServiceCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Start containers on a device.
|
||||||
|
|
||||||
|
Start containers on a device.
|
||||||
|
|
||||||
|
Multiple devices and services may be specified with a comma-separated list
|
||||||
|
of values (no spaces).
|
||||||
|
`;
|
||||||
|
public static examples = [
|
||||||
|
'$ balena device start-service 23c73a1 myService',
|
||||||
|
'$ balena device start-service 23c73a1 myService1,myService2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
service: Args.string({
|
||||||
|
description: 'comma-separated list (no blank spaces) of service names',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = await this.parse(DeviceStartServiceCmd);
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
const ux = getCliUx();
|
||||||
|
|
||||||
|
const deviceUuids = params.uuid.split(',');
|
||||||
|
const serviceNames = params.service.split(',');
|
||||||
|
|
||||||
|
// Iterate sequentially through deviceUuids.
|
||||||
|
// We may later want to add a batching feature,
|
||||||
|
// so that n devices are processed in parallel
|
||||||
|
for (const uuid of deviceUuids) {
|
||||||
|
ux.action.start(`Starting services on device ${uuid}`);
|
||||||
|
await this.startServices(balena, uuid, serviceNames);
|
||||||
|
ux.action.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startServices(
|
||||||
|
balena: BalenaSDK,
|
||||||
|
deviceUuid: string,
|
||||||
|
serviceNames: string[],
|
||||||
|
) {
|
||||||
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
const { getExpandedProp } = await import('../../utils/pine');
|
||||||
|
|
||||||
|
// Get device
|
||||||
|
const device = await balena.models.device.getWithServiceDetails(
|
||||||
|
deviceUuid,
|
||||||
|
{
|
||||||
|
$expand: {
|
||||||
|
is_running__release: { $select: 'commit' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeReleaseCommit = getExpandedProp(
|
||||||
|
device.is_running__release,
|
||||||
|
'commit',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check specified services exist on this device before startinganything
|
||||||
|
serviceNames.forEach((service) => {
|
||||||
|
if (!device.current_services[service]) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Service ${service} not found on device ${deviceUuid}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start services
|
||||||
|
const startPromises: Array<Promise<void>> = [];
|
||||||
|
for (const serviceName of serviceNames) {
|
||||||
|
const service = device.current_services[serviceName];
|
||||||
|
// Each service is an array of `CurrentServiceWithCommit`
|
||||||
|
// because when service is updating, it will actually hold 2 services
|
||||||
|
// Target commit matching `device.is_running__release`
|
||||||
|
const serviceContainer = service.find((s) => {
|
||||||
|
return s.commit === activeReleaseCommit;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serviceContainer) {
|
||||||
|
startPromises.push(
|
||||||
|
balena.models.device.startService(
|
||||||
|
deviceUuid,
|
||||||
|
serviceContainer.image_id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(startPromises);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.toLowerCase().includes('no online device')) {
|
||||||
|
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
src/commands/device/stop-service.ts
Normal file
132
src/commands/device/stop-service.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
|
import type { BalenaSDK } from 'balena-sdk';
|
||||||
|
|
||||||
|
export default class DeviceStopServiceCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Stop containers on a device.
|
||||||
|
|
||||||
|
Stop containers on a device.
|
||||||
|
|
||||||
|
Multiple devices and services may be specified with a comma-separated list
|
||||||
|
of values (no spaces).
|
||||||
|
`;
|
||||||
|
public static examples = [
|
||||||
|
'$ balena device stop-service 23c73a1 myService',
|
||||||
|
'$ balena device stop-service 23c73a1 myService1,myService2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
service: Args.string({
|
||||||
|
description: 'comma-separated list (no blank spaces) of service names',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = await this.parse(DeviceStopServiceCmd);
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
const ux = getCliUx();
|
||||||
|
|
||||||
|
const deviceUuids = params.uuid.split(',');
|
||||||
|
const serviceNames = params.service.split(',');
|
||||||
|
|
||||||
|
// Iterate sequentially through deviceUuids.
|
||||||
|
// We may later want to add a batching feature,
|
||||||
|
// so that n devices are processed in parallel
|
||||||
|
for (const uuid of deviceUuids) {
|
||||||
|
ux.action.start(`Stopping services on device ${uuid}`);
|
||||||
|
await this.stopServices(balena, uuid, serviceNames);
|
||||||
|
ux.action.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopServices(
|
||||||
|
balena: BalenaSDK,
|
||||||
|
deviceUuid: string,
|
||||||
|
serviceNames: string[],
|
||||||
|
) {
|
||||||
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
const { getExpandedProp } = await import('../../utils/pine');
|
||||||
|
|
||||||
|
// Get device
|
||||||
|
const device = await balena.models.device.getWithServiceDetails(
|
||||||
|
deviceUuid,
|
||||||
|
{
|
||||||
|
$expand: {
|
||||||
|
is_running__release: { $select: 'commit' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeReleaseCommit = getExpandedProp(
|
||||||
|
device.is_running__release,
|
||||||
|
'commit',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check specified services exist on this device before stoppinganything
|
||||||
|
serviceNames.forEach((service) => {
|
||||||
|
if (!device.current_services[service]) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Service ${service} not found on device ${deviceUuid}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop services
|
||||||
|
const stopPromises: Array<Promise<void>> = [];
|
||||||
|
for (const serviceName of serviceNames) {
|
||||||
|
const service = device.current_services[serviceName];
|
||||||
|
// Each service is an array of `CurrentServiceWithCommit`
|
||||||
|
// because when service is updating, it will actually hold 2 services
|
||||||
|
// Target commit matching `device.is_running__release`
|
||||||
|
const serviceContainer = service.find((s) => {
|
||||||
|
return s.commit === activeReleaseCommit;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serviceContainer) {
|
||||||
|
stopPromises.push(
|
||||||
|
balena.models.device.stopService(
|
||||||
|
deviceUuid,
|
||||||
|
serviceContainer.image_id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(stopPromises);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.toLowerCase().includes('no online device')) {
|
||||||
|
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/commands/device/track-fleet.ts
Normal file
46
src/commands/device/track-fleet.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @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 } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
export default class DeviceTrackFleetCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Make a device track the fleet's pinned release.
|
||||||
|
|
||||||
|
Make a device track the fleet's pinned release.
|
||||||
|
`;
|
||||||
|
public static examples = ['$ balena device track-fleet 7cf02a6'];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: "the uuid of the device to make track the fleet's release",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = await this.parse(DeviceTrackFleetCmd);
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
await balena.models.device.trackApplicationRelease(params.uuid);
|
||||||
|
}
|
||||||
|
}
|
@ -14,18 +14,11 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { CommandHelp } from '../../utils/oclif-utils';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
json?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicesSupportedCmd extends Command {
|
export default class DevicesSupportedCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -44,51 +37,47 @@ export default class DevicesSupportedCmd extends Command {
|
|||||||
'$ balena devices supported --json',
|
'$ balena devices supported --json',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = (
|
public static flags = {
|
||||||
'devices supported ' +
|
json: Flags.boolean({
|
||||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
json: flags.boolean({
|
|
||||||
char: 'j',
|
char: 'j',
|
||||||
description: 'produce JSON output instead of tabular output',
|
description: 'produce JSON output instead of tabular output',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
const { flags: options } = await this.parse(DevicesSupportedCmd);
|
||||||
const [dts, configDTs] = await Promise.all([
|
const pineOptions = {
|
||||||
getBalenaSdk().models.deviceType.getAllSupported({
|
$select: ['slug', 'name'],
|
||||||
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
|
$expand: {
|
||||||
$select: ['slug', 'name'],
|
is_of__cpu_architecture: { $select: 'slug' },
|
||||||
}),
|
device_type_alias: {
|
||||||
getBalenaSdk().models.config.getDeviceTypes(),
|
$select: 'is_referenced_by__alias',
|
||||||
]);
|
$orderby: { is_referenced_by__alias: 'asc' },
|
||||||
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
},
|
||||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
},
|
||||||
|
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||||
|
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
|
||||||
|
pineOptions,
|
||||||
|
)) as Array<
|
||||||
|
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||||
|
>;
|
||||||
interface DT {
|
interface DT {
|
||||||
slug: string;
|
slug: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
arch: string;
|
arch: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
let deviceTypes: DT[] = [];
|
let deviceTypes = dts.map((dt): DT => {
|
||||||
for (const slug of Object.keys(dtsBySlug)) {
|
const aliases = dt.device_type_alias
|
||||||
const configDT: Partial<typeof configDTs[0]> =
|
.map((dta) => dta.is_referenced_by__alias)
|
||||||
configDTsBySlug[slug] || {};
|
.filter((alias) => alias !== dt.slug);
|
||||||
const aliases = (configDT.aliases || []).filter(
|
return {
|
||||||
(alias) => alias !== slug,
|
slug: dt.slug,
|
||||||
);
|
|
||||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
|
||||||
deviceTypes.push({
|
|
||||||
slug,
|
|
||||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||||
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
|
||||||
name: dt.name || 'N/A',
|
name: dt.name || 'N/A',
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||||
if (options.json) {
|
if (options.json) {
|
@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
@ -26,7 +26,6 @@ import { applicationIdInfo } from '../../utils/messages';
|
|||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
device?: string; // device UUID
|
device?: string; // device UUID
|
||||||
help: void;
|
|
||||||
quiet: boolean;
|
quiet: boolean;
|
||||||
service?: string; // service name
|
service?: string; // service name
|
||||||
}
|
}
|
||||||
@ -78,34 +77,30 @@ export default class EnvAddCmd extends Command {
|
|||||||
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
|
||||||
required: true,
|
required: true,
|
||||||
description: 'environment or config variable name',
|
description: 'environment or config variable name',
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"variable value; if omitted, use value from this process' environment",
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'env add <name> [value]';
|
// Required for supporting empty string ('') `value` args.
|
||||||
|
public static strict = false;
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
device: { ...cf.device, exclusive: ['fleet'] },
|
device: { ...cf.device, exclusive: ['fleet'] },
|
||||||
help: cf.help,
|
|
||||||
quiet: cf.quiet,
|
quiet: cf.quiet,
|
||||||
service: cf.service,
|
service: cf.service,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(EnvAddCmd);
|
||||||
EnvAddCmd,
|
|
||||||
);
|
|
||||||
const cmd = this;
|
const cmd = this;
|
||||||
|
|
||||||
if (!options.fleet && !options.device) {
|
if (!options.fleet && !options.device) {
|
||||||
@ -151,16 +146,15 @@ export default class EnvAddCmd extends Command {
|
|||||||
|
|
||||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||||
if (options.fleet) {
|
if (options.fleet) {
|
||||||
const { getFleetSlug } = await import('../../utils/sdk');
|
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
|
||||||
for (const app of options.fleet.split(',')) {
|
|
||||||
try {
|
try {
|
||||||
await balena.models.application[varType].set(
|
await balena.models.application[varType].set(
|
||||||
await getFleetSlug(balena, app),
|
appSlug,
|
||||||
params.name,
|
params.name,
|
||||||
params.value,
|
params.value,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${app}`);
|
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +175,25 @@ export default class EnvAddCmd extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Stop accepting application names in the next major
|
||||||
|
// and just drop this in favor of doing the .split(',') directly.
|
||||||
|
async function resolveFleetSlugs(
|
||||||
|
balena: BalenaSdk.BalenaSDK,
|
||||||
|
fleetOption: string,
|
||||||
|
) {
|
||||||
|
const fleetSlugs: string[] = [];
|
||||||
|
const { getFleetSlug } = await import('../../utils/sdk');
|
||||||
|
for (const appNameOrSlug of fleetOption.split(',')) {
|
||||||
|
try {
|
||||||
|
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fleetSlugs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add service variables for a device or fleet.
|
* Add service variables for a device or fleet.
|
||||||
*/
|
*/
|
||||||
@ -190,17 +203,17 @@ async function setServiceVars(
|
|||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
) {
|
) {
|
||||||
if (options.fleet) {
|
if (options.fleet) {
|
||||||
for (const app of options.fleet.split(',')) {
|
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
|
||||||
for (const service of options.service!.split(',')) {
|
for (const service of options.service!.split(',')) {
|
||||||
try {
|
try {
|
||||||
const serviceId = await getServiceIdForApp(sdk, app, service);
|
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
|
||||||
await sdk.models.service.var.set(
|
await sdk.models.service.var.set(
|
||||||
serviceId,
|
serviceId,
|
||||||
params.name,
|
params.name,
|
||||||
params.value!,
|
params.value!,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${app}`);
|
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,11 +258,12 @@ async function setServiceVars(
|
|||||||
*/
|
*/
|
||||||
async function getServiceIdForApp(
|
async function getServiceIdForApp(
|
||||||
sdk: BalenaSdk.BalenaSDK,
|
sdk: BalenaSdk.BalenaSDK,
|
||||||
appName: string,
|
appSlug: string,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let serviceId: number | undefined;
|
let serviceId: number | undefined;
|
||||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
const services = await sdk.models.service.getAllByApplication(appSlug, {
|
||||||
|
$select: 'id',
|
||||||
$filter: { service_name: serviceName },
|
$filter: { service_name: serviceName },
|
||||||
});
|
});
|
||||||
if (services.length > 0) {
|
if (services.length > 0) {
|
||||||
@ -257,7 +271,7 @@ async function getServiceIdForApp(
|
|||||||
}
|
}
|
||||||
if (serviceId === undefined) {
|
if (serviceId === undefined) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Cannot find service ${serviceName} for fleet ${appName}`,
|
`Cannot find service ${serviceName} for fleet ${appSlug}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return serviceId;
|
return serviceId;
|
@ -14,28 +14,12 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import * as ec from '../../utils/env-common';
|
import * as ec from '../../utils/env-common';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
config: boolean;
|
|
||||||
device: boolean;
|
|
||||||
service: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
id: number;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EnvRenameCmd extends Command {
|
export default class EnvRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Change the value of a config or env var for a fleet, device or service.
|
Change the value of a config or env var for a fleet, device or service.
|
||||||
@ -54,34 +38,27 @@ export default class EnvRenameCmd extends Command {
|
|||||||
'$ balena env rename 678678 1 --device --config',
|
'$ balena env rename 678678 1 --device --config',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
id: Args.integer({
|
||||||
name: 'id',
|
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: (input) => parseAsInteger(input, 'id'),
|
parse: async (input) => parseAsInteger(input, 'id'),
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"variable value; if omitted, use value from this process' environment",
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'env rename <id> <value>';
|
public static flags = {
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
config: ec.booleanConfig,
|
config: ec.booleanConfig,
|
||||||
device: ec.booleanDevice,
|
device: ec.booleanDevice,
|
||||||
service: ec.booleanService,
|
service: ec.booleanService,
|
||||||
help: cf.help,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
|
||||||
EnvRenameCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Command.checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user