deploy.ts 5.7 KB
Newer Older
Bradley Hilton's avatar
Bradley Hilton committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import cli from 'cli-ux';
import * as FormData from 'form-data';
import * as fs from 'fs';
import fetch from 'node-fetch';
import { Response } from 'node-fetch';

import { AppCompiler, AppPackager, FolderDetails } from '../misc';

export default class Deploy extends Command {
    public static description = 'allows deploying an App to a server';

    public static flags = {
        help: flags.help({ char: 'h' }),
        // flag with no value (-f, --force)
        force: flags.boolean({ char: 'f', description: 'forcefully deploy the App, ignores lint & TypeScript errors' }),
18
        update: flags.boolean({ description: 'updates the app, instead of creating' }),
Bradley Hilton's avatar
Bradley Hilton committed
19 20 21
        url: flags.string({ description: 'where the App should be deployed to' }),
        username: flags.string({ char: 'u', dependsOn: ['url'], description: 'username to authenticate with' }),
        password: flags.string({ char: 'p', dependsOn: ['username'], description: 'password of the user' }),
22 23 24 25 26 27 28 29 30 31
        code: flags.string({ char: 'c', dependsOn: ['username'], description: '2FA code of the user' }),
        i2fa: flags.boolean({ description: 'interactively ask for 2FA code' }),
        token: flags.string({
            char: 't', dependsOn: ['userid'],
            description: 'API token to use with UserID (instead of username & password)',
        }),
        userid: flags.string({
            char: 'i', dependsOn: ['token'],
            description: 'UserID to use with API token (instead of username & password)',
        }),
Bradley Hilton's avatar
Bradley Hilton committed
32 33 34 35 36 37 38 39 40 41 42 43
    };

    public async run() {
        const { flags } = this.parse(Deploy);

        cli.action.start(`${ chalk.green('packaging') } your app`);

        const fd = new FolderDetails(this);

        try {
            await fd.readInfoFile();
        } catch (e) {
44
            this.error(e && e.message ? e.message : e);
Bradley Hilton's avatar
Bradley Hilton committed
45 46 47 48 49 50 51
            return;
        }

        const compiler = new AppCompiler(this, fd);
        const report = compiler.logDiagnostics();

        if (!report.isValid && !flags.force) {
52
            this.error('TypeScript compiler error(s) occurred');
Bradley Hilton's avatar
Bradley Hilton committed
53 54 55 56 57 58 59 60 61 62 63 64 65
            this.exit(1);
            return;
        }

        const packager = new AppPackager(this, fd);
        const zipName = await packager.zipItUp();

        cli.action.stop('packaged!');

        if (!flags.url) {
            flags.url = await cli.prompt('What is the server\'s url (include https)?');
        }

66
        if (!flags.username && !flags.token) {
Bradley Hilton's avatar
Bradley Hilton committed
67 68 69
            flags.username = await cli.prompt('What is the username?');
        }

70
        if (!flags.password && !flags.token) {
Bradley Hilton's avatar
Bradley Hilton committed
71 72 73
            flags.password = await cli.prompt('And, what is the password?', { type: 'hide' });
        }

74 75 76 77
        if (flags.i2fa) {
            flags.code = await cli.prompt('2FA code', { type: 'hide' });
        }

Bradley Hilton's avatar
Bradley Hilton committed
78 79 80 81 82
        cli.action.start(`${ chalk.green('deploying') } your app`);

        const data = new FormData();
        data.append('app', fs.createReadStream(fd.mergeWithFolder(zipName)));

83
        await this.asyncSubmitData(data, flags, fd);
Bradley Hilton's avatar
Bradley Hilton committed
84 85 86 87 88

        cli.action.stop('deployed!');
    }

    // tslint:disable:promise-function-async
89
    private async asyncSubmitData(data: FormData, flags: { [key: string]: any }, fd: FolderDetails): Promise<void> {
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
        let authResult;

        if (!flags.token) {
            let credentials: { username: string, password: string, code?: string };
            credentials = { username: flags.username, password: flags.password };
            if (flags.code) {
                credentials.code = flags.code;
            }

            authResult = await fetch(this.normalizeUrl(flags.url, '/api/v1/login'), {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(credentials),
            }).then((res: Response) => res.json());

            if (authResult.status === 'error' || !authResult.data) {
                throw new Error('Invalid username and password or missing 2FA code (if active)');
            }
        } else {
            const verificationResult = await fetch(this.normalizeUrl(flags.url, '/api/v1/me'), {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Auth-Token': flags.token,
                    'X-User-Id': flags.userid,
                },
            }).then((res: Response) => res.json());

            if (!verificationResult.success) {
                throw new Error('Invalid API token');
            }

            authResult = { data: { authToken: flags.token, userId: flags.userid } };
Bradley Hilton's avatar
Bradley Hilton committed
125 126
        }

127 128 129 130 131 132
        let endpoint = '/api/apps';
        if (flags.update) {
            endpoint += `/${fd.info.id}`;
        }

        const deployResult = await fetch(this.normalizeUrl(flags.url, endpoint), {
Bradley Hilton's avatar
Bradley Hilton committed
133 134 135 136 137 138 139 140 141
            method: 'POST',
            headers: {
                'X-Auth-Token': authResult.data.authToken,
                'X-User-Id': authResult.data.userId,
            },
            body: data,
        }).then((res: Response) => res.json());

        if (deployResult.status === 'error') {
142
            throw new Error(`Unknown error occurred while deploying ${JSON.stringify(deployResult)}`);
Bradley Hilton's avatar
Bradley Hilton committed
143
        } else if (!deployResult.success) {
144 145 146 147
            if (deployResult.status === 'compiler_error') {
                throw new Error(`Deployment compiler errors: \n${ JSON.stringify(deployResult.messages, null, 2) }`);
            }
            throw new Error(`Deployment error: ${ deployResult }`);
148
        }
Bradley Hilton's avatar
Bradley Hilton committed
149 150 151 152 153 154 155
    }

    // expects the `path` to start with the /
    private normalizeUrl(url: string, path: string): string {
        return url.replace(/\/$/, '') + path;
    }
}