Skip to content

Commit a5e2ab7

Browse files
committed
Added Force option to publish command
Added more and coloured logs
1 parent f57886a commit a5e2ab7

3 files changed

Lines changed: 129 additions & 74 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@maxvandelaar/homey-community-store-cli",
3-
"version": "2.0.0",
3+
"version": "2.1.1",
44
"description": "CLI tool to upload apps to the Homey Community Store. Run 'hcs' to understand how to use it.",
55
"main": "src/index.js",
66
"bin": {
@@ -24,6 +24,7 @@
2424
"aws-sdk": "^2.698.0",
2525
"aws4": "^1.10.1",
2626
"axios": "^0.20.0",
27+
"chalk": "^4.1.0",
2728
"esm": "^3.2.25",
2829
"inquirer": "^7.1.0",
2930
"keytar": "^6.0.1",

src/cli.js

Lines changed: 123 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import yargs from 'yargs';
1111
import slash from 'slash';
1212
import aws4 from 'aws4';
1313
import axios from 'axios';
14+
import chalk from 'chalk';
15+
16+
const log = console.log;
17+
const error = console.error;
18+
const {blue, green, gray, red} = chalk;
1419

1520
function parseArgumentsIntoOptions() {
1621
return yargs
@@ -22,8 +27,11 @@ function parseArgumentsIntoOptions() {
2227
})
2328
}, build)
2429
.command('publish', 'Build the app and upload it to the Homey Community Store', (yargs) => {
25-
return yargs;
26-
}, publish)
30+
return yargs.option('force', {
31+
type: 'boolean',
32+
description: 'CAUTION: This will override the version if it already exists in the database!'
33+
});
34+
}, publish)
2735
.command('logout', 'Remove all credentials', (yargs) => {
2836
return yargs
2937
}, logout)
@@ -62,12 +70,14 @@ function determineCategory(appInfo) {
6270
}
6371

6472
function createTar(appInfo, argv) {
73+
log(gray('Process for creating the tar.gz file'));
6574
return new Promise((resolve, reject) => {
6675
let version = `v${appInfo.version}`;
6776
if (argv.latest) {
6877
version = 'latest';
6978
}
7079
const tarFile = `${appInfo.id}-${version}.tar.gz`;
80+
log(gray(`Filename determined: '${tarFile}'`));
7181
tar.c({
7282
gzip: true,
7383
file: tarFile,
@@ -122,56 +132,70 @@ export async function cli(args) {
122132
parseArgumentsIntoOptions(args);
123133
}
124134

125-
function uploadToS3(s3Path, bucketName, root) {
126-
let s3 = new AWS.S3();
127-
128-
function walkSync(currentDirPath, callback) {
129-
fs.readdirSync(currentDirPath).forEach((name) => {
130-
const filePath = path.join(currentDirPath, name);
131-
const stat = fs.statSync(filePath);
132-
if (stat.isFile()) {
133-
callback(filePath, stat);
134-
} else if (stat.isDirectory()) {
135-
walkSync(filePath, callback);
136-
}
137-
});
138-
}
139-
140-
walkSync(s3Path, (filePath, _stat) => {
141-
const bucketPath = filePath;
142-
const key = slash(root + bucketPath.split(s3Path)[1]).replace(/\\/g, '/');
143-
if (!['.svg', '.png', '.jpeg', '.jpg', '.gz'].includes(path.extname(filePath)) || filePath.includes('node_modules') || filePath.includes('.github')) {
144-
return;
135+
async function uploadToS3(s3Path, bucketName, root) {
136+
return new Promise(async resolve => {
137+
log(gray('Upload assets to S3'));
138+
let s3 = new AWS.S3();
139+
140+
const overall = [];
141+
async function walkSync(currentDirPath, callback) {
142+
const promises = fs.readdirSync(currentDirPath).map((name) => {
143+
return new Promise(async (resolveMap) => {
144+
const filePath = path.join(currentDirPath, name);
145+
const stat = fs.statSync(filePath);
146+
if (stat.isFile()) {
147+
await callback(filePath, stat);
148+
resolveMap();
149+
} else if (stat.isDirectory()) {
150+
await walkSync(filePath, callback);
151+
resolveMap();
152+
}
153+
});
154+
});
155+
overall.push(...promises);
145156
}
146-
const contentType = mime.contentType(path.extname(bucketPath));
147-
let params = {
148-
Bucket: bucketName,
149-
ACL: 'public-read',
150-
ContentType: contentType,
151-
Key: key,
152-
Body: fs.readFileSync(filePath)
153-
};
154-
s3.putObject(params, function (err, _data) {
155-
if (err) {
156-
console.log(err)
157-
} else {
158-
console.log('Successfully uploaded ' + bucketPath + ' to ' + bucketName + ' as ' + key);
159-
}
157+
158+
await walkSync(s3Path, (filePath, _stat) => {
159+
return new Promise(async (resolveWalk, rejectWalk) => {
160+
const bucketPath = filePath;
161+
const key = slash(root + bucketPath.split(s3Path)[1]).replace(/\\/g, '/');
162+
if (!['.svg', '.png', '.jpeg', '.jpg', '.gz'].includes(path.extname(filePath)) || filePath.includes('node_modules') || filePath.includes('.github')) {
163+
return resolveWalk();
164+
}
165+
const contentType = mime.contentType(path.extname(bucketPath));
166+
const params = {
167+
Bucket: bucketName,
168+
ACL: 'public-read',
169+
ContentType: contentType,
170+
Key: key,
171+
Body: fs.readFileSync(filePath)
172+
};
173+
const success = await s3.putObject(params).promise().catch(rejectWalk);
174+
if (!success) {
175+
return error(red(`Could not upload ${key}`));
176+
}
177+
log(gray('Successfully uploaded ' + bucketPath + ' to ' + bucketName + ' as ' + key));
178+
resolveWalk();
179+
});
160180
});
181+
182+
resolve(overall);
161183
});
162184
}
163185

164186
async function build(argv) {
165-
console.log('Building the app');
187+
log(blue('Building the app'));
166188
let tar = {};
189+
let appInfo = {};
167190
try {
168-
const appInfo = require(`${cwd()}/app.json`);
191+
log(gray(`Loading '${cwd()}/app.json'`));
192+
appInfo = require(`${cwd()}/app.json`);
169193
tar = await createTar(appInfo, argv);
194+
log(gray(`${tar.filename} created successfully`));
170195
} catch (e) {
171-
console.error(e);
172-
return;
196+
error(red(e));
173197
}
174-
console.log(`Build finished: ${cwd()}/${tar.filename}`)
198+
log(green(`Build finished: ${cwd()}/${tar.filename}`));
175199
}
176200

177201
function getCredentials(account) {
@@ -196,31 +220,37 @@ async function logout(_argv) {
196220
await keytar.deletePassword('hcs-cli', creds.account);
197221
});
198222
await Promise.allSettled(promises);
199-
console.log('You have been signed out');
223+
log(green('You have been signed out'));
200224
}
201225

202226
async function publish(argv) {
227+
log(blue('Publishing the app'));
203228
let appInfo = {};
204229
let tar = {};
230+
const force = !!argv.force;
205231
try {
232+
log(gray(`Loading '${cwd()}/app.json'`));
206233
appInfo = require(`${cwd()}/app.json`);
207234
tar = await createTar(appInfo, argv);
235+
log(gray(`${tar.filename} created successfully`));
208236
} catch (e) {
209-
console.error(e);
237+
error(red(e));
210238
return;
211239
}
212240

241+
log(gray('Process the app.json'));
242+
const timestamp = Date.now();
213243
let app = {
214244
id: appInfo.id,
215-
added: Date.now(),
216-
modified: Date.now(),
245+
added: timestamp,
246+
modified: timestamp,
217247
versions: [{
218248
id: appInfo.id,
219249
summary: appInfo.description,
220250
hash: tar.hash,
221251
filename: tar.filename,
222-
added: Date.now(),
223-
modified: Date.now(),
252+
added: timestamp,
253+
modified: timestamp,
224254
sdk: appInfo.sdk,
225255
version: appInfo.version,
226256
compatibility: appInfo.compatibility,
@@ -251,20 +281,25 @@ async function publish(argv) {
251281
}]
252282
};
253283

284+
log(gray('Look for ./homeychangelog.json'));
254285
if (fs.existsSync(`${cwd()}/.homeychangelog.json`)) {
286+
log(gray('Changelog found, adding it to the app'))
255287
app.changelog = require(`${cwd()}/.homeychangelog.json`);
256288
app.versions[0].changelog = require(`${cwd()}/.homeychangelog.json`);
257289
}
258290

291+
log(gray(`Processing locales`));
259292
const locales = {};
260293
const appVersion = app.versions[0];
261294
if (appVersion.name) {
295+
log(gray(`Processing locales from the name: ${Object.keys(appVersion.name).join(', ')}`));
262296
Object.keys(appVersion.name).forEach(lang => {
263297
locales[lang] = {name: appVersion.name[lang]}
264298
});
265299
}
266300

267301
if (appVersion.summary) {
302+
log(gray(`Processing locales from the summary for the description: ${Object.keys(appVersion.name).join(', ')}`));
268303
Object.keys(appVersion.summary).forEach(lang => {
269304
locales[lang] = {
270305
...locales[lang],
@@ -274,6 +309,7 @@ async function publish(argv) {
274309
}
275310

276311
if (appVersion.description) {
312+
log(gray(`Processing locales from the description for the description: ${Object.keys(appVersion.name).join(', ')}`));
277313
Object.keys(appVersion.description).forEach(lang => {
278314
locales[lang] = {
279315
...locales[lang],
@@ -283,16 +319,18 @@ async function publish(argv) {
283319
}
284320

285321
if (appVersion.tags) {
322+
log(gray(`Processing locales from the tags: ${Object.keys(appVersion.name).join(', ')}`));
286323
Object.keys(appVersion.tags).forEach(lang => {
287324
locales[lang] = {
288325
...locales[lang],
289326
tags: appVersion.tags[lang]
290327
}
291328
});
292329
}
293-
if (appVersion.changelog) {
294330

331+
if (appVersion.changelog) {
295332
Object.keys(appVersion.changelog).forEach(version => {
333+
log(gray(`Processing locales from the changelog ${version}: ${Object.keys(appVersion.changelog[version]).join(', ')}`));
296334
Object.keys(appVersion.changelog[version]).forEach(lang => {
297335
if (!locales[lang]) {
298336
locales[lang] = {};
@@ -307,24 +345,27 @@ async function publish(argv) {
307345

308346
app.versions[0].locales = locales;
309347

310-
const creds = await keytar.findCredentials('hcs-cli').catch(console.error);
348+
log(gray('Looking for credentials'));
349+
const creds = await keytar.findCredentials('hcs-cli').catch(err => error(red(err)));
311350
let accessKeyId;
312351
let accessKeySecure;
313352
if (creds && creds.length === 1) {
314353
accessKeyId = creds[0].account;
315354
accessKeySecure = creds[0].password;
316355
} else {
356+
log(blue('Credentials not found, please sign in'));
317357
accessKeyId = await promptForAccessKeyId();
318-
accessKeySecure = await getCredentials(accessKeyId).catch(console.error);
358+
accessKeySecure = await getCredentials(accessKeyId).catch(err => error(red(err)));
319359
}
320360

321361
if (accessKeySecure === false) {
322362
//ask for credentials;
363+
log(blue('Password not found, please sign in'));
323364
const accessKeySecret = await promptForAccessKeySecret();
324365
if (accessKeySecret) {
325-
const success = await setCredentials(accessKeyId, accessKeySecret).catch(console.error);
366+
const success = await setCredentials(accessKeyId, accessKeySecret).catch(err => error(red(err)));
326367
if (!success) {
327-
console.log('Something went wrong storing your credentials');
368+
error(red('Something went wrong storing your credentials'));
328369
return;
329370
}
330371
} else {
@@ -333,6 +374,7 @@ async function publish(argv) {
333374
accessKeySecure = accessKeySecret;
334375
}
335376

377+
log(gray('Creating the AWS Config'));
336378
AWS.config = new AWS.Config({
337379
region: 'eu-central-1',
338380
accessKeyId: accessKeyId,
@@ -342,40 +384,52 @@ async function publish(argv) {
342384
const request = {
343385
host: '4c23v5xwtc.execute-api.eu-central-1.amazonaws.com',
344386
method: 'POST',
345-
url: `https://4c23v5xwtc.execute-api.eu-central-1.amazonaws.com/staging/apps/publish`,
346-
data: app, // object describing the foo
347-
body: JSON.stringify(app), // aws4 looks for body; axios for data
348-
path: `/staging/apps/publish`,
387+
url: `https://4c23v5xwtc.execute-api.eu-central-1.amazonaws.com/production/apps/publish`,
388+
data: {app, force}, // object describing the foo
389+
body: JSON.stringify({app, force}), // aws4 looks for body; axios for data
390+
path: `/production/apps/publish`,
349391
headers: {
350392
'content-type': 'application/json'
351393
}
352394
}
395+
log(gray(`Preparing request to the API ${request.url}`));
353396

354397
const signedRequest = aws4.sign(request,
355398
{
356-
// assumes user has authenticated and we have called
357-
// AWS.config.credentials.get to retrieve keys and
358-
// session tokens
359399
secretAccessKey: AWS.config.credentials.secretAccessKey,
360400
accessKeyId: AWS.config.credentials.accessKeyId
361401
})
362402

363-
delete signedRequest.headers['Host']
364-
delete signedRequest.headers['Content-Length']
365-
366-
const response = await axios(signedRequest).catch(console.error);
403+
delete signedRequest.headers['Host'];
404+
delete signedRequest.headers['Content-Length'];
367405

406+
log(gray(`Send request to the API ${request.url}`));
407+
const response = await axios(signedRequest).catch(err => error(red(err)));
368408
if (response && response.data && response.data.body) {
369409
const {success, msg} = response.data.body;
370410
if (!success) {
371-
console.error(msg);
411+
error(red(msg));
372412
return;
373413
}
374-
console.log(msg);
375-
//PUSH TAR FILE AND IMAGES TO S3!
376-
uploadToS3(cwd(), 'homey-community-store', `${app.id}/${appInfo.version}`);
414+
log(gray(msg));
415+
416+
const uploadPromise = uploadToS3(cwd(), 'homey-community-store', `${app.id}/${appInfo.version}`);
417+
const filePromises = await uploadPromise;
418+
if (filePromises){
419+
let errors;
420+
await Promise.allSettled(filePromises).catch(err => errors = err);
421+
if (errors) {
422+
log(red('Failed to push an asset to the S3 storage. Failed to publish the app. Please contact the HCS admin'));
423+
} else {
424+
log(green('Successfully published the app to the Homey Community Store.'));
425+
}
426+
} else {
427+
error(red('FAILED TO PUBLISH'));
428+
}
429+
430+
377431
} else {
378-
console.error('Failed pushing to the DB');
432+
error(red('Failed pushing to the DB'));
433+
error(red(response.statusText));
379434
}
380-
381435
}

0 commit comments

Comments
 (0)