| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- "use strict";
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- var desc = Object.getOwnPropertyDescriptor(m, k);
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
- desc = { enumerable: true, get: function() { return m[k]; } };
- }
- Object.defineProperty(o, k2, desc);
- }) : (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- o[k2] = m[k];
- }));
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
- Object.defineProperty(o, "default", { enumerable: true, value: v });
- }) : function(o, v) {
- o["default"] = v;
- });
- var __importStar = (this && this.__importStar) || function (mod) {
- if (mod && mod.__esModule) return mod;
- var result = {};
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
- __setModuleDefault(result, mod);
- return result;
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.LinkCommand = void 0;
- const cli_framework_1 = require("@ionic/cli-framework");
- const cli_framework_prompts_1 = require("@ionic/cli-framework-prompts");
- const utils_terminal_1 = require("@ionic/utils-terminal");
- const debug_1 = require("debug");
- const constants_1 = require("../constants");
- const guards_1 = require("../guards");
- const color_1 = require("../lib/color");
- const command_1 = require("../lib/command");
- const errors_1 = require("../lib/errors");
- const executor_1 = require("../lib/executor");
- const open_1 = require("../lib/open");
- const debug = (0, debug_1.debug)('ionic:commands:link');
- const CHOICE_CREATE_NEW_APP = 'createNewApp';
- const CHOICE_NEVERMIND = 'nevermind';
- const CHOICE_RELINK = 'relink';
- const CHOICE_LINK_EXISTING_APP = 'linkExistingApp';
- const CHOICE_IONIC = 'ionic';
- const CHOICE_GITHUB = 'github';
- const CHOICE_SKIP = 'skip';
- const CHOICE_MASTER_ONLY = 'master';
- const CHOICE_SPECIFIC_BRANCHES = 'specific';
- class LinkCommand extends command_1.Command {
- async getMetadata() {
- const projectFile = this.project ? (0, utils_terminal_1.prettyPath)(this.project.filePath) : constants_1.PROJECT_FILE;
- return {
- name: 'link',
- type: 'project',
- groups: ["paid" /* MetadataGroup.PAID */],
- summary: 'Connect local apps to Ionic',
- description: `
- Link apps on Appflow to local Ionic projects with this command.
- If the ${(0, color_1.input)('id')} argument is excluded, this command will prompt you to select an app from Appflow.
- Appflow uses a git-based workflow to manage app updates. During the linking process, select ${(0, color_1.strong)('GitHub')} (recommended) or ${(0, color_1.strong)('Appflow')} as a git host. See our documentation[^appflow-git-basics] for more information.
- Ultimately, this command sets the ${(0, color_1.strong)('id')} property in ${(0, color_1.strong)((0, utils_terminal_1.prettyPath)(projectFile))}, which marks this app as linked.
- If you are having issues linking, please get in touch with our Support[^support-request].
- `,
- footnotes: [
- {
- id: 'appflow-git-basics',
- url: 'https://ionicframework.com/docs/appflow/basics/git',
- shortUrl: 'https://ion.link/appflow-git-basics',
- },
- {
- id: 'support-request',
- url: 'https://ion.link/support-request',
- },
- ],
- exampleCommands: ['', 'a1b2c3d4'],
- inputs: [
- {
- name: 'id',
- summary: `The Appflow ID of the app to link (e.g. ${(0, color_1.input)('a1b2c3d4')})`,
- },
- ],
- options: [
- {
- name: 'name',
- summary: 'The app name to use during the linking of a new app',
- groups: ["hidden" /* MetadataGroup.HIDDEN */],
- },
- {
- name: 'create',
- summary: 'Create a new app on Ionic Appflow and link it with this local Ionic project',
- type: Boolean,
- groups: ["hidden" /* MetadataGroup.HIDDEN */],
- },
- {
- name: 'pro-id',
- summary: 'Specify an app ID from the Ionic Appflow to link',
- groups: ["deprecated" /* MetadataGroup.DEPRECATED */, "hidden" /* MetadataGroup.HIDDEN */],
- spec: { value: 'id' },
- },
- ],
- };
- }
- async preRun(inputs, options) {
- const { create } = options;
- if (inputs[0] && create) {
- throw new errors_1.FatalException(`Sorry--cannot use both ${(0, color_1.input)('id')} and ${(0, color_1.input)('--create')}. You must either link an existing app or create a new one.`);
- }
- const id = options['pro-id'] ? String(options['pro-id']) : undefined;
- if (id) {
- inputs[0] = id;
- }
- }
- async run(inputs, options, runinfo) {
- const { promptToLogin } = await Promise.resolve().then(() => __importStar(require('../lib/session')));
- if (!this.project) {
- throw new errors_1.FatalException(`Cannot run ${(0, color_1.input)('ionic link')} outside a project directory.`);
- }
- let id = inputs[0];
- let { create } = options;
- const idFromConfig = this.project.config.get('id');
- if (idFromConfig) {
- if (id && idFromConfig === id) {
- this.env.log.msg(`Already linked with app ${(0, color_1.input)(id)}.`);
- return;
- }
- const msg = id ?
- `Are you sure you want to link it to ${(0, color_1.input)(id)} instead?` :
- `Would you like to run link again?`;
- const confirm = await this.env.prompt({
- type: 'confirm',
- name: 'confirm',
- message: `App ID ${(0, color_1.input)(idFromConfig)} is already set up with this app. ${msg}`,
- });
- if (!confirm) {
- this.env.log.msg('Not linking.');
- return;
- }
- }
- if (!this.env.session.isLoggedIn()) {
- await promptToLogin(this.env);
- }
- if (!id && !create) {
- const choices = [
- {
- name: `Link ${idFromConfig ? 'a different' : 'an existing'} app on Ionic Appflow`,
- value: CHOICE_LINK_EXISTING_APP,
- },
- {
- name: 'Create a new app on Ionic Appflow',
- value: CHOICE_CREATE_NEW_APP,
- },
- ];
- if (idFromConfig) {
- choices.unshift({
- name: `Relink ${(0, color_1.input)(idFromConfig)}`,
- value: CHOICE_RELINK,
- });
- }
- const result = await this.env.prompt({
- type: 'list',
- name: 'whatToDo',
- message: 'What would you like to do?',
- choices,
- });
- if (result === CHOICE_CREATE_NEW_APP) {
- create = true;
- id = undefined;
- }
- else if (result === CHOICE_LINK_EXISTING_APP) {
- const tasks = this.createTaskChain();
- tasks.next(`Looking up your apps`);
- const apps = [];
- const appClient = await this.getAppClient();
- const paginator = appClient.paginate();
- for (const r of paginator) {
- const res = await r;
- apps.push(...res.data);
- }
- tasks.end();
- if (apps.length === 0) {
- const confirm = await this.env.prompt({
- type: 'confirm',
- name: 'confirm',
- message: `No apps found. Would you like to create a new app on Ionic Appflow?`,
- });
- if (!confirm) {
- throw new errors_1.FatalException(`Cannot link without an app selected.`);
- }
- create = true;
- id = undefined;
- }
- else {
- const choice = await this.chooseApp(apps);
- if (choice === CHOICE_NEVERMIND) {
- this.env.log.info('Not linking app.');
- id = undefined;
- }
- else {
- id = choice;
- }
- }
- }
- else if (result === CHOICE_RELINK) {
- id = idFromConfig;
- }
- }
- if (create) {
- let name = options['name'] ? String(options['name']) : undefined;
- if (!name) {
- name = await this.env.prompt({
- type: 'input',
- name: 'name',
- message: 'Please enter a name for your new app:',
- validate: v => cli_framework_1.validators.required(v),
- });
- }
- id = await this.createApp({ name }, runinfo);
- }
- else if (id) {
- const app = await this.lookUpApp(id);
- await this.linkApp(app, runinfo);
- }
- }
- async getAppClient() {
- const { AppClient } = await Promise.resolve().then(() => __importStar(require('../lib/app')));
- const token = await this.env.session.getUserToken();
- return new AppClient(token, this.env);
- }
- async getUserClient() {
- const { UserClient } = await Promise.resolve().then(() => __importStar(require('../lib/user')));
- const token = await this.env.session.getUserToken();
- return new UserClient(token, this.env);
- }
- async lookUpApp(id) {
- const tasks = this.createTaskChain();
- tasks.next(`Looking up app ${(0, color_1.input)(id)}`);
- const appClient = await this.getAppClient();
- const app = await appClient.load(id); // Make sure the user has access to the app
- tasks.end();
- return app;
- }
- async createApp({ name }, runinfo) {
- const appClient = await this.getAppClient();
- const org_id = this.env.config.get('org.id');
- const app = await appClient.create({ name, org_id });
- await this.linkApp(app, runinfo);
- return app.id;
- }
- async linkApp(app, runinfo) {
- // TODO: load connections
- // TODO: check for git availability before this
- this.env.log.nl();
- this.env.log.info(`Ionic Appflow uses a git-based workflow to manage app updates.\n` +
- `You will be prompted to set up the git host and repository for this new app. See the docs${(0, color_1.ancillary)('[1]')} for more information.\n\n` +
- `${(0, color_1.ancillary)('[1]')}: ${(0, color_1.strong)('https://ion.link/appflow-git-basics')}`);
- this.env.log.nl();
- const service = await this.env.prompt({
- type: 'list',
- name: 'gitService',
- message: 'Which git host would you like to use?',
- choices: [
- {
- name: 'GitHub',
- value: CHOICE_GITHUB,
- },
- {
- name: 'Ionic Appflow',
- value: CHOICE_IONIC,
- },
- {
- name: 'Don\'t use a git host',
- value: CHOICE_SKIP,
- },
- ],
- });
- let githubUrl;
- if (service === CHOICE_IONIC) {
- if (!this.env.config.get('git.setup')) {
- await (0, executor_1.runCommand)(runinfo, ['ssh', 'setup']);
- }
- await (0, executor_1.runCommand)(runinfo, ['config', 'set', 'id', `"${app.id}"`, '--json']);
- await (0, executor_1.runCommand)(runinfo, ['git', 'remote']);
- }
- else {
- if (service === CHOICE_GITHUB) {
- githubUrl = await this.linkGithub(app);
- }
- await (0, executor_1.runCommand)(runinfo, ['config', 'set', 'id', `"${app.id}"`, '--json']);
- }
- this.env.log.ok(`Project linked with app ${(0, color_1.input)(app.id)}!`);
- if (service === CHOICE_GITHUB) {
- this.env.log.info(`Here are some additional links that can help you with you first push to GitHub:\n` +
- `${(0, color_1.strong)('Adding GitHub as a remote')}:\n\t${(0, color_1.strong)('https://help.github.com/articles/adding-a-remote/')}\n\n` +
- `${(0, color_1.strong)('Pushing to a remote')}:\n\t${(0, color_1.strong)('https://help.github.com/articles/pushing-to-a-remote/')}\n\n` +
- `${(0, color_1.strong)('Working with branches')}:\n\t${(0, color_1.strong)('https://guides.github.com/introduction/flow/')}\n\n` +
- `${(0, color_1.strong)('More comfortable with a GUI? Try GitHub Desktop!')}\n\t${(0, color_1.strong)('https://desktop.github.com/')}`);
- if (githubUrl) {
- this.env.log.info(`You can now push to one of your branches on GitHub to trigger a build in Ionic Appflow!\n` +
- `If you haven't added GitHub as your origin you can do so by running:\n\n` +
- `${(0, color_1.input)('git remote add origin ' + githubUrl)}\n\n` +
- `You can find additional links above to help if you're having issues.`);
- }
- }
- }
- async linkGithub(app) {
- const { id } = this.env.session.getUser();
- const userClient = await this.getUserClient();
- const user = await userClient.load(id, { fields: ['oauth_identities'] });
- if (!user.oauth_identities || !user.oauth_identities.github) {
- await this.oAuthProcess(id);
- }
- if (await this.needsAssociation(app, user.id)) {
- await this.confirmGithubRepoExists();
- const repoId = await this.selectGithubRepo();
- const branches = await this.selectGithubBranches(repoId);
- return this.connectGithub(app, repoId, branches);
- }
- }
- async confirmGithubRepoExists() {
- let confirm = false;
- this.env.log.nl();
- this.env.log.info((0, color_1.strong)(`In order to link to a GitHub repository the repository must already exist on GitHub.`));
- this.env.log.info(`${(0, color_1.strong)('If the repository does not exist please create one now before continuing.')}\n` +
- `If you're not familiar with Git you can learn how to set it up with GitHub here:\n\n` +
- (0, color_1.strong)(`https://help.github.com/articles/set-up-git/ \n\n`) +
- `You can find documentation on how to create a repository on GitHub and push to it here:\n\n` +
- (0, color_1.strong)(`https://help.github.com/articles/create-a-repo/`));
- confirm = await this.env.prompt({
- type: 'confirm',
- name: 'confirm',
- message: 'Does the repository exist on GitHub?',
- });
- if (!confirm) {
- throw new errors_1.FatalException(`Repo must exist on GitHub in order to link. Please create the repo and run ${(0, color_1.input)('ionic link')} again.`);
- }
- }
- async oAuthProcess(userId) {
- const userClient = await this.getUserClient();
- let confirm = false;
- this.env.log.nl();
- this.env.log.info(`GitHub OAuth setup required.\n` +
- `To continue, we need you to authorize Ionic Appflow with your GitHub account. ` +
- `A browser will open and prompt you to complete the authorization request. ` +
- `When finished, please return to the CLI to continue linking your app.`);
- confirm = await this.env.prompt({
- type: 'confirm',
- name: 'ready',
- message: 'Open browser:',
- });
- if (!confirm) {
- throw new errors_1.FatalException(`GitHub OAuth setup is required to link to GitHub repository. Please run ${(0, color_1.input)('ionic link')} again when ready.`);
- }
- const url = await userClient.oAuthGithubLogin(userId);
- await (0, open_1.openUrl)(url);
- confirm = await this.env.prompt({
- type: 'confirm',
- name: 'ready',
- message: 'Authorized and ready to continue:',
- });
- if (!confirm) {
- throw new errors_1.FatalException(`GitHub OAuth setup is required to link to GitHub repository. Please run ${(0, color_1.input)('ionic link')} again when ready.`);
- }
- }
- async needsAssociation(app, userId) {
- const appClient = await this.getAppClient();
- if (app.association && app.association.repository.html_url) {
- this.env.log.msg(`App ${(0, color_1.input)(app.id)} already connected to ${(0, color_1.strong)(app.association.repository.html_url)}`);
- const confirm = await this.env.prompt({
- type: 'confirm',
- name: 'confirm',
- message: 'Would you like to connect a different repo?',
- });
- if (!confirm) {
- return false;
- }
- try {
- // TODO: maybe we can use a PUT instead of DELETE now + POST later?
- await appClient.deleteAssociation(app.id);
- }
- catch (e) {
- if ((0, guards_1.isSuperAgentError)(e)) {
- if (e.response.status === 401) {
- await this.oAuthProcess(userId);
- await appClient.deleteAssociation(app.id);
- return true;
- }
- else if (e.response.status === 404) {
- debug(`DELETE ${app.id} GitHub association not found`);
- return true;
- }
- }
- throw e;
- }
- }
- return true;
- }
- async connectGithub(app, repoId, branches) {
- const appClient = await this.getAppClient();
- try {
- const association = await appClient.createAssociation(app.id, { repoId, type: 'github', branches });
- this.env.log.ok(`App ${(0, color_1.input)(app.id)} connected to ${(0, color_1.strong)(association.repository.html_url)}`);
- return association.repository.html_url;
- }
- catch (e) {
- if ((0, guards_1.isSuperAgentError)(e) && e.response.status === 403) {
- throw new errors_1.FatalException(e.response.body.error.message);
- }
- }
- }
- formatRepoName(fullName) {
- const [org, name] = fullName.split('/');
- return `${(0, color_1.weak)(`${org} /`)} ${name}`;
- }
- async chooseApp(apps) {
- const { formatName } = await Promise.resolve().then(() => __importStar(require('../lib/app')));
- const neverMindChoice = {
- name: (0, color_1.strong)('Nevermind'),
- id: CHOICE_NEVERMIND,
- value: CHOICE_NEVERMIND,
- org: null,
- };
- const linkedApp = await this.env.prompt({
- type: 'list',
- name: 'linkedApp',
- message: 'Which app would you like to link',
- choices: [
- ...apps.map(app => ({
- name: `${formatName(app)} ${(0, color_1.weak)(`(${app.id})`)}`,
- value: app.id,
- })),
- (0, cli_framework_prompts_1.createPromptChoiceSeparator)(),
- neverMindChoice,
- (0, cli_framework_prompts_1.createPromptChoiceSeparator)(),
- ],
- });
- return linkedApp;
- }
- async selectGithubRepo() {
- const user = this.env.session.getUser();
- const userClient = await this.getUserClient();
- const tasks = this.createTaskChain();
- const task = tasks.next('Looking up your GitHub repositories');
- const paginator = userClient.paginateGithubRepositories(user.id);
- const repos = [];
- try {
- for (const r of paginator) {
- const res = await r;
- repos.push(...res.data);
- task.msg = `Looking up your GitHub repositories: ${(0, color_1.strong)(String(repos.length))} found`;
- }
- }
- catch (e) {
- tasks.fail();
- if ((0, guards_1.isSuperAgentError)(e) && e.response.status === 401) {
- await this.oAuthProcess(user.id);
- return this.selectGithubRepo();
- }
- throw e;
- }
- tasks.end();
- const repoId = await this.env.prompt({
- type: 'list',
- name: 'githubRepo',
- message: 'Which GitHub repository would you like to link?',
- choices: repos.map(repo => ({
- name: this.formatRepoName(repo.full_name),
- value: String(repo.id),
- })),
- });
- return Number(repoId);
- }
- async selectGithubBranches(repoId) {
- this.env.log.nl();
- this.env.log.info((0, color_1.strong)(`By default Ionic Appflow links only to the ${(0, color_1.input)('master')} branch.`));
- this.env.log.info(`${(0, color_1.strong)('If you\'d like to link to another branch or multiple branches you\'ll need to select each branch to connect to.')}\n` +
- `If you're not familiar with on working with branches in GitHub you can read about them here:\n\n` +
- (0, color_1.strong)(`https://guides.github.com/introduction/flow/ \n\n`));
- const choice = await this.env.prompt({
- type: 'list',
- name: 'githubMultipleBranches',
- message: 'Which would you like to do?',
- choices: [
- {
- name: `Link to master branch only`,
- value: CHOICE_MASTER_ONLY,
- },
- {
- name: `Link to specific branches`,
- value: CHOICE_SPECIFIC_BRANCHES,
- },
- ],
- });
- switch (choice) {
- case CHOICE_MASTER_ONLY:
- return ['master'];
- case CHOICE_SPECIFIC_BRANCHES:
- // fall through and begin prompting to choose branches
- break;
- default:
- throw new errors_1.FatalException('Aborting. No branch choice specified.');
- }
- const user = this.env.session.getUser();
- const userClient = await this.getUserClient();
- const paginator = userClient.paginateGithubBranches(user.id, repoId);
- const tasks = this.createTaskChain();
- const task = tasks.next('Looking for available branches');
- const availableBranches = [];
- try {
- for (const r of paginator) {
- const res = await r;
- availableBranches.push(...res.data);
- task.msg = `Looking up the available branches on your GitHub repository: ${(0, color_1.strong)(String(availableBranches.length))} found`;
- }
- }
- catch (e) {
- tasks.fail();
- throw e;
- }
- tasks.end();
- const choices = availableBranches.map(branch => ({
- name: branch.name,
- value: branch.name,
- checked: branch.name === 'master',
- }));
- if (choices.length === 0) {
- this.env.log.warn(`No branches found for the repository. Linking to ${(0, color_1.input)('master')} branch.`);
- return ['master'];
- }
- const selectedBranches = await this.env.prompt({
- type: 'checkbox',
- name: 'githubBranches',
- message: 'Which branch would you like to link?',
- choices,
- default: ['master'],
- });
- return selectedBranches;
- }
- }
- exports.LinkCommand = LinkCommand;
|