import { Command, Flags } from '@oclif/core'; import { FailedFlagValidationError } from '@oclif/core/lib/parser/errors'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { isNil, uniqBy } from 'lodash'; import { ApiError, getOrganization, getSquid, listOrganizations, listSquids, listUserSquids, Squid, SquidRequest, } from './api'; import { getTTY } from './tty'; import { formatSquidReference, printSquid } from './utils'; export const SUCCESS_CHECK_MARK = chalk.green('✓'); export abstract class CliCommand extends Command { static baseFlags = { interactive: Flags.boolean({ description: 'Enable interactive mode. Use --no-interactive to disable prompts for CI/scripts.', required: false, default: true, allowNo: true, }), }; logSuccess(message: string) { this.log(SUCCESS_CHECK_MARK + message); } logQuestion(message: string) { this.log(chalk.green(`? `) + message); } logDimmed(message: string) { this.log(chalk.dim(message)); } validateSquidNameFlags(flags: { reference?: any; name?: any }) { if (flags.reference || flags.name) return; throw new FailedFlagValidationError({ failed: [ { name: 'squid name', validationFn: 'validateSquidName', reason: [ 'One of the following must be provided: --reference or --name', '', 'Examples:', ' sqd --reference my-squid@v1', ' sqd --name my-squid --slot ', ].join('\n'), status: 'failed', }, ], parse: {}, }); } async catch(error: any) { if (error instanceof ApiError) { const { request, body } = error; switch (request.status) { case 401: return this.error( `Authentication failure. Please obtain a new deployment key at https://app.subsquid.io and follow the instructions`, ); case 400: if (body?.invalidFields) { const messages = body.invalidFields.map(function (obj: any, index: number) { return `${index + 1}) ${chalk.bold('"' + obj.path.join('.') + '"')} — ${obj.message}`; }); return this.error(`Validation error:\n${messages.join('\n')}`); } return this.error(body?.error || body?.message || `Validation error ${body}`); case 404: const defaultErrorStart = `cannot ${request.method.toLowerCase()}`; if ( body.error.toLowerCase().startsWith(defaultErrorStart) || body.message?.toLowerCase().startsWith(defaultErrorStart) ) { const url = `${chalk.bold(request.method)} ${chalk.bold(request.url)}`; return this.error( `Unknown API endpoint ${url}. Check that your are using the latest version of the Squid CLI. If the problem persists, please contact support.`, ); } else { return this.error(body.error); } case 405: return this.error(body?.error || body?.message || 'Method not allowed'); case 502: case 503: case 504: return this.error('The API is currently unavailable. Please try again later'); default: return this.error( [ `Unknown network error occurred`, `==================`, `Status: ${request.status}`, `Body:\n${JSON.stringify(body)}`, ].join('\n'), ); } } throw error; } async findSquid(req: SquidRequest) { try { return await getSquid(req); } catch (e) { if (e instanceof ApiError && e.request.status === 404) { return null; } throw e; } } async findOrThrowSquid({ organization, squid }: SquidRequest) { const res = await this.findSquid({ organization, squid }); if (!res) { throw new Error( `The squid ${formatSquidReference(typeof squid === 'string' ? squid : squid, { colored: true })} is not found`, ); } return res; } async promptOrganization( code: string | null | undefined, { using, interactive }: { using?: string; interactive?: boolean } = {}, ) { if (code) { return await getOrganization({ organization: { code } }); } const organizations = await listOrganizations(); return await this.getOrganizationPrompt(organizations, { using, interactive }); } async promptSquidOrganization( code: string | null | undefined, name: string, { using, interactive, }: { using?: string; interactive?: boolean; } = {}, ) { if (code) { return await getOrganization({ organization: { code } }); } const squids = await listUserSquids({ name }); let organizations = squids.map((s) => s.organization).filter((o) => !isNil(o)); organizations = uniqBy(organizations, (o) => o.code); if (organizations.length === 0) { return this.error(`You have no organizations with squid "${name}".`); } return await this.getOrganizationPrompt(organizations, { using, interactive }); } async promptSquid(organization: { code: string }, { interactive }: { interactive?: boolean } = {}): Promise { const squids = await listSquids({ organization }); if (squids.length === 0) { return this.error(`No squids found in organization "${organization.code}".`); } if (squids.length === 1) { return squids[0]; } const { stdin, stdout } = getTTY(); if (!stdin || !stdout || !interactive) { return this.error( [ `Organization "${organization.code}" has ${squids.length} squids:`, ...squids.map((s) => ` - ${formatSquidReference({ name: s.name, slot: s.slot })}`), ``, `Please specify the squid using "--reference" or "--name" flag.`, `Example: sqd --reference ${squids[0].name}@${squids[0].slot}`, ].join('\n'), ); } const prompt = inquirer.createPromptModule({ input: stdin, output: stdout }); const { squid } = await prompt([ { name: 'squid', type: 'list', message: 'Please choose a squid:', choices: squids.map((s) => { const ref = formatSquidReference({ name: s.name, slot: s.slot }); const tags = s.tags.length ? ` (${s.tags.map((t) => t.name).join(', ')})` : ''; return { name: `${ref}${tags}`, value: s, }; }), }, ]); stdin.destroy(); stdout.destroy(); return squid; } private async getOrganizationPrompt( organizations: T[], { using = 'using "--org" flag', interactive, }: { using?: string; interactive?: boolean; }, ): Promise { if (organizations.length === 0) { return this.error(`You have no organizations. Please create organization first.`); } else if (organizations.length === 1) { return organizations[0]; } const { stdin, stdout } = getTTY(); if (!stdin || !stdout || !interactive) { return this.error( [ `You have ${organizations.length} organizations:`, ...organizations.map((o) => ` - ${o.code}`), ``, `Please specify one of them explicitly ${using}.`, `Example: sqd --org ${organizations[0].code}`, ].join('\n'), ); } const prompt = inquirer.createPromptModule({ input: stdin, output: stdout }); const { organization } = await prompt([ { name: 'organization', type: 'list', message: `Please choose an organization:`, choices: organizations.map((o) => { return { name: o.name ? `${o.name} (${o.code})` : o.code, value: o, }; }), }, ]); // Hack to prevent opened descriptors to block event loop before exit stdin.destroy(); stdout.destroy(); return organization; } } export * as SqdFlags from './flags';