From 8231e9a2990cc8952d739079fa8d31fe5b053f36 Mon Sep 17 00:00:00 2001 From: belopash Date: Tue, 10 Sep 2024 19:28:15 +0500 Subject: [PATCH] feat: fix --add-tag flag and improve ux --- package.json | 2 +- src/api/schema.d.ts | 3 +- src/api/squids.ts | 37 +++--- src/api/types.ts | 10 +- src/command.ts | 97 ++++++++------ src/commands/deploy.ts | 196 +++++++++++++--------------- src/commands/explorer.ts | 4 +- src/commands/logs.ts | 41 ++---- src/commands/ls.ts | 40 +++--- src/commands/restart.ts | 40 +----- src/commands/rm.ts | 46 ++----- src/commands/secrets/ls.ts | 4 +- src/commands/secrets/rm.ts | 4 +- src/commands/secrets/set.ts | 4 +- src/commands/tags/add.ts | 47 +++---- src/commands/tags/remove.ts | 22 +--- src/deploy-command.ts | 46 ++++++- src/flags/fullname.ts | 8 +- src/flags/name.ts | 11 +- src/flags/org.ts | 2 +- src/flags/slot.ts | 4 +- src/flags/tag.ts | 4 +- src/ui/components/VersionLogsTab.ts | 4 +- src/ui/components/types.ts | 4 +- src/utils.ts | 32 +++-- 25 files changed, 341 insertions(+), 371 deletions(-) diff --git a/package.json b/package.json index 65f2ec9..11b8f1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@subsquid/cli", "description": "squid cli tool", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "license": "GPL-3.0-or-later", "repository": "git@github.com:subsquid/squid-cli.git", "publishConfig": { diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index 0e60677..2890d75 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -2981,7 +2981,8 @@ export enum DeploymentResponseStatus { UNPACKING = "UNPACKING", IMAGE_BUILDING = "IMAGE_BUILDING", RESETTING = "RESETTING", - CONFIGURING_INGRESS = "CONFIGURING_INGRESS", + ADDING_INGRESS = "ADDING_INGRESS", + REMOVING_INGRESS = "REMOVING_INGRESS", SQUID_SYNCING = "SQUID_SYNCING", SQUID_DELETING = "SQUID_DELETING", ADDONS_SYNCING = "ADDONS_SYNCING", diff --git a/src/api/squids.ts b/src/api/squids.ts index 9df15b5..25f7d79 100644 --- a/src/api/squids.ts +++ b/src/api/squids.ts @@ -1,6 +1,7 @@ import split2 from 'split2'; import { pretty } from '../logs'; +import { formatSquidReference } from '../utils'; import { api, debugLog } from './api'; import { @@ -24,10 +25,10 @@ export async function listSquids({ organization, name }: OrganizationRequest & { return body.payload.sort((a, b) => a.name.localeCompare(b.name)); } -export async function getSquid({ organization, reference }: SquidRequest): Promise { +export async function getSquid({ organization, squid }: SquidRequest): Promise { const { body } = await api>({ method: 'get', - path: `/orgs/${organization.code}/squids/${reference}`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}`, }); return body.payload; @@ -35,7 +36,7 @@ export async function getSquid({ organization, reference }: SquidRequest): Promi export async function squidHistoryLogs({ organization, - reference, + squid, query, abortController, }: SquidRequest & { @@ -52,7 +53,7 @@ export async function squidHistoryLogs({ }): Promise { const { body } = await api>({ method: 'get', - path: `/orgs/${organization.code}/squids/${reference}/logs/history`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}/logs/history`, query: { ...query, from: query.from.toISOString(), @@ -68,7 +69,7 @@ export async function squidHistoryLogs({ export async function squidLogsFollow({ organization, - reference, + squid, query, abortController, }: SquidRequest & { @@ -77,7 +78,7 @@ export async function squidLogsFollow({ }) { const { body } = await api({ method: 'get', - path: `/orgs/${organization.code}/squids/${reference}/logs/follow`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}/logs/follow`, query, responseType: 'stream', abortController: abortController, @@ -88,7 +89,7 @@ export async function squidLogsFollow({ export async function streamSquidLogs({ organization, - reference, + squid, abortController, query = {}, onLog, @@ -106,7 +107,7 @@ export async function streamSquidLogs({ try { stream = await squidLogsFollow({ organization, - reference, + squid, query, abortController, }); @@ -198,32 +199,28 @@ export async function getUploadUrl({ organization }: OrganizationRequest): Promi return body.payload; } -export async function restartSquid({ organization, reference }: SquidRequest): Promise { +export async function restartSquid({ organization, squid }: SquidRequest): Promise { const { body } = await api>({ method: 'post', - path: `/orgs/${organization.code}/squids/${reference}/restart`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}/restart`, }); return body.payload; } -export async function deleteSquid({ organization, reference }: SquidRequest): Promise { +export async function deleteSquid({ organization, squid }: SquidRequest): Promise { const { body } = await api>({ method: 'delete', - path: `/orgs/${organization.code}/squids/${reference}`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}`, }); return body.payload; } -export async function addSquidTag({ - organization, - reference, - tag, -}: SquidRequest & { tag: string }): Promise { +export async function addSquidTag({ organization, squid, tag }: SquidRequest & { tag: string }): Promise { const { body } = await api>({ method: 'PUT', - path: `/orgs/${organization.code}/squids/${reference}/tags/${tag}`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}/tags/${tag}`, }); return body.payload; @@ -231,12 +228,12 @@ export async function addSquidTag({ export async function removeSquidTag({ organization, - reference, + squid, tag, }: SquidRequest & { tag: string }): Promise { const { body } = await api>({ method: 'DELETE', - path: `/orgs/${organization.code}/squids/${reference}/tags/${tag}`, + path: `/orgs/${organization.code}/squids/${formatSquidReference(squid)}/tags/${tag}`, }); return body.payload; diff --git a/src/api/types.ts b/src/api/types.ts index 2a09afc..d28e17d 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -51,6 +51,14 @@ export type LogsResponse = { export type OrganizationRequest = { organization: PickDeep }; -export type SquidRequest = OrganizationRequest & { reference: string }; +export type SquidRequest = OrganizationRequest & { + squid: + | ({ name: string } & ( + | { tag?: never; slot: string } + | { tag: string; slot?: never } + | { tag: string; slot: string } + )) + | string; +}; export type DeployRequest = OrganizationRequest & { deploy: PickDeep }; diff --git a/src/command.ts b/src/command.ts index 8bcf16a..c19b69b 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,4 +1,4 @@ -import { Args, Command } from '@oclif/core'; +import { Command, Flags } from '@oclif/core'; import { FailedFlagValidationError } from '@oclif/core/lib/parser/errors'; import chalk from 'chalk'; import inquirer from 'inquirer'; @@ -6,10 +6,19 @@ import { isNil, uniqBy } from 'lodash'; import { ApiError, getOrganization, getSquid, listOrganizations, listUserSquids, 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({ + required: false, + default: true, + allowNo: true, + }), + }; + logSuccess(message: string) { this.log(SUCCESS_CHECK_MARK + message); } @@ -92,9 +101,9 @@ export abstract class CliCommand extends Command { throw error; } - async findSquid({ organization, reference }: SquidRequest) { + async findSquid(req: SquidRequest) { try { - return await getSquid({ organization, reference }); + return await getSquid(req); } catch (e) { if (e instanceof ApiError && e.request.status === 404) { return null; @@ -104,31 +113,41 @@ export abstract class CliCommand extends Command { } } - async findOrThrowSquid({ organization, reference }: SquidRequest) { - const squid = await this.findSquid({ organization, reference }); - if (!squid) { - throw new Error(`The squid "${reference}" is not found`); + 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 squid; + return res; } - async promptOrganization(code: string | null | undefined, using?: string) { + async promptOrganization( + code: string | null | undefined, + { using, interactive }: { using?: string; interactive?: boolean } = {}, + ) { if (code) { return await getOrganization({ organization: { code } }); } const organizations = await listOrganizations(); - if (organizations.length === 0) { - return this.error(`You have no organizations. Please create organization first.`); - } else if (organizations.length === 1) { - return organizations[0]; - } - return await this.getOrganizationPrompt(organizations, using); + return await this.getOrganizationPrompt(organizations, { using, interactive }); } - async promptSquidOrganization({ code, name, using }: { code?: string | null; name: string; using?: string }) { + async promptSquidOrganization( + code: string, + name: string, + { + using, + interactive, + }: { + using?: string; + interactive?: boolean; + } = {}, + ) { if (code) { return await getOrganization({ organization: { code } }); } @@ -139,25 +158,37 @@ export abstract class CliCommand extends Command { organizations = uniqBy(organizations, (o) => o.code); if (organizations.length === 0) { - return this.error(`Squid "${name}" was not found.`); - } else if (organizations.length === 1) { - return organizations[0]; + return this.error(`You have no organizations with squid "${name}".`); } - return await this.getOrganizationPrompt(organizations, using); + return await this.getOrganizationPrompt(organizations, { using, interactive }); } private async getOrganizationPrompt( organizations: T[], - using: string = 'using "-o" flag', + { + 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) { - this.log(chalk.dim(`You have ${organizations.length} organizations:`)); - for (const organization of organizations) { - this.log(`${chalk.dim(' - ')}${chalk.dim(organization.code)}`); - } - return this.error(`Please specify one of them explicitly ${using}`); + if (!stdin || !stdout || !interactive) { + return this.error( + [ + `You have ${organizations.length} organizations:`, + ...organizations.map((o) => `${chalk.dim(' - ')}${chalk.dim(o.code)}`), + `Please specify one of them explicitly ${using}`, + ].join('\n'), + ); } const prompt = inquirer.createPromptModule({ input: stdin, output: stdout }); @@ -183,16 +214,4 @@ export abstract class CliCommand extends Command { } } -export const SquidReferenceArg = Args.string({ - // description: ` or `, - required: true, - parse: async (input) => { - input = input.toLowerCase(); - if (!/^[a-z0-9\-]+[:@][a-z0-9\-]+$/.test(input)) { - throw new Error(`Expected a squid reference but received: ${input}`); - } - return input; - }, -}); - export * as SqdFlags from './flags'; diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 91c56cf..f394522 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -13,11 +13,11 @@ import { defaults, get, isNil, keys, pick, pickBy } from 'lodash'; import prettyBytes from 'pretty-bytes'; import targz from 'targz'; -import { deploySquid, OrganizationRequest, Squid, uploadFile } from '../api'; +import { deploySquid, Organization, OrganizationRequest, Squid, uploadFile } from '../api'; import { SqdFlags, SUCCESS_CHECK_MARK } from '../command'; import { DeployCommand } from '../deploy-command'; import { loadManifestFile } from '../manifest'; -import { formatSquidFullname, ParsedSquidFullname, parseSquidFullname, printSquidFullname } from '../utils'; +import { formatSquidReference, ParsedSquidReference, parseSquidReference, printSquid } from '../utils'; const compressAsync = promisify(targz.compress); @@ -60,22 +60,24 @@ function example(command: string, description: string) { export default class Deploy extends DeployCommand { static description = 'Deploy new or update an existing squid in the Cloud'; static examples = [ - example('sqd deploy', 'Create a new squid with name provided in the manifest file'), + example('sqd deploy ./', 'Create a new squid with name provided in the manifest file'), example( - 'sqd deploy my-squid-override', + 'sqd deploy ./ -n my-squid-override', 'Create a new squid deployment and override it\'s name to "my-squid-override"', ), - example('sqd deploy my-squid#asmzf5', 'Update the "my-squid" squid with hash "asmzf5"'), + example('sqd deploy ./ -n my-squid -s asmzf5', 'Update the "my-squid" squid with slot "asmzf5"'), example( - 'sqd deploy -d ./path-to-the-squid -m squid.prod.yaml', + 'sqd deploy ./path-to-the-squid -m squid.prod.yaml', 'Use a manifest file located in ./path-to-the-squid/squid.prod.yaml', ), example( - 'sqd deploy -d /Users/dev/path-to-the-squid -m /Users/dev/path-to-the-squid/squid.prod.yaml', + 'sqd deploy /Users/dev/path-to-the-squid -m /Users/dev/path-to-the-squid/squid.prod.yaml', 'Full paths are also fine', ), ]; + static help = 'If squid flags are not specified, the they will be retrieved from the manifest or prompted.'; + static args = { // squid_name_or_reference: Args.string({ // description: [ @@ -88,7 +90,7 @@ export default class Deploy extends DeployCommand { // ].join('\n'), // required: false, // }), - source: Args.string({ + source: Args.directory({ description: [ `Squid source. Could be:`, ` - a relative or absolute path to a local folder (e.g. ".")`, @@ -106,58 +108,48 @@ export default class Deploy extends DeployCommand { }), name: SqdFlags.name({ required: false, + relationships: [], }), tag: SqdFlags.tag({ required: false, + dependsOn: [], }), slot: SqdFlags.slot({ required: false, + dependsOn: [], }), fullname: SqdFlags.fullname({ required: false, }), - manifest: Flags.string({ + manifest: Flags.file({ char: 'm', description: 'Relative local path to a squid manifest file in squid working directory', required: false, default: 'squid.yaml', helpValue: '', }), - // dir: Flags.string({ - // char: 'd', - // description: SQUID_WORKDIR_DESC.join('\n'), - // required: false, - // default: '.', - // helpValue: '', - // }), - // tag: Flags.string({ - // char: 't', - // description: [ - // 'Assign the tag to the squid deployment. ', - // 'The previous deployment API URL assigned with the same tag will be transitioned to the new deployment', - // 'Tag must contain only alphanumeric characters, dashes, and underscores', - // ].join('\n'), - // required: false, - // helpValue: '', - // }), force: Flags.boolean({ required: false, default: false, }), + override: Flags.boolean({ + required: false, + default: false, + }), 'hard-reset': Flags.boolean({ description: 'Do a hard reset before deploying. Drops and re-creates all the squid resources including the database. Will cause a short API downtime', required: false, default: false, }), - 'no-stream-logs': Flags.boolean({ - description: 'Do not attach and stream squid logs after the deploy', + 'stream-logs': Flags.boolean({ + description: 'Attach and stream squid logs after the deploy', required: false, - default: false, + default: true, + allowNo: true, }), - 'apply-tag': Flags.boolean({ + 'add-tag': Flags.string({ required: false, - default: false, }), }; @@ -165,12 +157,14 @@ export default class Deploy extends DeployCommand { const { args: { source }, flags: { + interactive, manifest: manifestPath, 'hard-reset': hardReset, - 'no-stream-logs': noStreamLogs, - 'apply-tag': applyTag, + 'stream-logs': streamLogs, + 'add-tag': addTag, force, fullname, + override, ...flags }, } = await this.parse(Deploy); @@ -190,21 +184,23 @@ export default class Deploy extends DeployCommand { const overrides = fullname ? fullname : flags; - // some hack to add slot name in case if version is used + // some hack to normalize slot name in case if version is used { manifest.slot = manifest.slotName(); delete manifest['version']; } - const override = await this.promptOverrideConflict(manifest, overrides); - if (!override) return; + if (!override) { + const confirm = await this.promptOverrideConflict(manifest, overrides, { interactive }); + if (!confirm) return; + } // eslint-disable-next-line prefer-const let { name, slot, org, tag } = defaults(overrides, manifest); - const organization = await this.promptOrganization(org); + const organization = await this.promptOrganization(org, { interactive }); - name = await this.promptSquidName(name); + name = await this.promptSquidName(name, { interactive }); this.log(chalk.dim(`Squid directory: ${squidDir}`)); this.log(chalk.dim(`Build directory: ${buildDir}`)); @@ -214,8 +210,7 @@ export default class Deploy extends DeployCommand { this.log(chalk.cyan(`Squid name: ${name}`)); if (slot) { this.log(chalk.cyan(`Squid slot: ${slot}`)); - } - if (tag) { + } else if (tag) { this.log(chalk.cyan(`Squid tag: ${tag}`)); } this.log(chalk.cyan(`-----------------------------`)); @@ -224,7 +219,7 @@ export default class Deploy extends DeployCommand { if (slot || tag) { target = await this.findSquid({ organization, - reference: formatSquidFullname(slot ? { name, slot } : { name, tag: tag! }), + squid: { name, slot, tag: tag! }, }); } @@ -247,9 +242,10 @@ export default class Deploy extends DeployCommand { /** * Squid exists we should check if tag belongs to another squid */ - if (target && slot && tag && !applyTag) { - const apply = await this.promptApplyTag(target, tag); - if (!apply) return; + const hasTag = !!target?.tags.find((t) => t.name === addTag) || tag === addTag; + if (addTag && !force && !hasTag) { + const add = await this.promptAddTag({ organization, name, tag: addTag }); + if (!add) return; } const archiveName = `${manifest.name}.tar.gz`; @@ -274,75 +270,48 @@ export default class Deploy extends DeployCommand { if (!deployment || !deployment.squid) return; if (target) { - this.logDeployResult( - UPDATE_COLOR, - `The squid ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} has been successfully updated`, - ); + this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(target)} has been successfully updated`); } else { this.logDeployResult( CREATE_COLOR, - `A new squid ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} has been successfully created`, + `A new squid ${printSquid({ ...deployment.squid, organization: deployment.organization })} has been successfully created`, ); } - if (!noStreamLogs) { - await this.streamLogs(organization, deployment.squid); + if (streamLogs) { + await this.streamLogs({ organization: deployment.organization, squid: deployment.squid }); } } - private async promptUpdateSquid(target: Squid) { - this.log( - `A squid "${printSquidFullname({ - org: target.organization.code, - name: target.name, - slot: target.slot, - })}" will be updated.`, - ); - const { confirm } = await inquirer.prompt([ - { - name: 'confirm', - type: 'confirm', - message: 'Are you sure you wish to proceed?', - }, - ]); - - return !!confirm; - } - - private async promptApplyTag(target: Squid, tag: string) { - if (!!target.tags.find((t) => t.name === tag)) return true; + private async promptUpdateSquid( + target: Squid, + { using = 'using "--force" flag', interactive }: { using?: string; interactive?: boolean } = {}, + ) { + const warning = `A squid ${printSquid(target)} already exists.`; - const oldSquid = await this.findSquid({ - organization: target.organization, - reference: formatSquidFullname({ name: target.name, tag }), - }); - if (!oldSquid) return true; + if (!interactive) { + this.error([warning, `Please do it explicitly ${using}`].join('\n')); + } - this.log( - `A squid tag "${tag}" has already been assigned to ${printSquidFullname({ name: oldSquid.name, slot: oldSquid.slot })}.`, - ); - this.log(`The tag will be assigned to the newly created squid.`); + this.warn(warning); const { confirm } = await inquirer.prompt([ { name: 'confirm', type: 'confirm', - message: 'Are you sure you wish to proceed?', + message: 'Are you sure?', + prefix: `A squid ${printSquid(target)} will be updated.`, }, ]); return !!confirm; } - private async promptOverrideConflict(manifest: Manifest, override: Record) { + private async promptOverrideConflict( + manifest: Manifest, + override: Record, + { using = 'using "--override" flag', interactive }: { using?: string; interactive?: boolean } = {}, + ) { const conflictKeys = keys(override).filter((k) => { const m = get(manifest, k); const o = get(override, k); @@ -351,18 +320,24 @@ export default class Deploy extends DeployCommand { if (!conflictKeys.length) return true; + const warning = [ + 'Conflict detected!', + `A manifest values do not match with specified ones.`, + ``, + diff( + { content: conflictKeys.map((k) => `${k}: ${get(manifest, k)}`).join('\n') + '\n' }, + { content: conflictKeys.map((k) => `${k}: ${get(override, k)}`).join('\n') + '\n' }, + ), + ``, + ].join('\n'); + + if (!interactive) { + this.error([warning, `Please do it explicitly ${using}`].join('\n')); + } + + this.warn(warning); this.log( - [ - chalk.bold('Conflict detected!'), - - `A manifest values do not match with specified ones.`, - `If it is intended and you'd like to override them, just skip this message and confirm, the manifest name will be overridden automatically in the Cloud during the deploy.`, - ``, - diff( - { content: conflictKeys.map((k) => `${k}: ${get(manifest, k)}`).join('\n') + '\n' }, - { content: conflictKeys.map((k) => `${k}: ${get(override, k)}`).join('\n') + '\n' }, - ), - ].join('\n'), + `If it is intended and you'd like to override them, just skip this message and confirm, the manifest name will be overridden automatically in the Cloud during the deploy.`, ); const { confirm } = await inquirer.prompt([ @@ -376,17 +351,24 @@ export default class Deploy extends DeployCommand { return !!confirm; } - private async promptSquidName(name?: string) { + private async promptSquidName( + name?: string | null | undefined, + { using = 'using "--name" flag', interactive }: { using?: string; interactive?: boolean } = {}, + ) { if (name) return name; + const warning = `The squid name is not defined either in the manifest or via CLI command.`; + + if (!interactive) { + this.error([warning, `Please specify it explicitly ${using}`].join('\n')); + } + + this.warn(warning); const { input } = await inquirer.prompt([ { name: 'input', type: 'input', - message: [ - chalk.reset(`The squid name is not defined either in the manifest or via CLI command.`), - chalk.reset(`Please enter the name of the squid:`), - ].join('\n'), + message: `Please enter the name of the squid:`, }, ]); diff --git a/src/commands/explorer.ts b/src/commands/explorer.ts index 01669f9..3dd7623 100644 --- a/src/commands/explorer.ts +++ b/src/commands/explorer.ts @@ -18,10 +18,10 @@ export default class Explorer extends CliCommand { async run(): Promise { const { - flags: { org }, + flags: { org, interactive }, } = await this.parse(Explorer); - const organization = await this.promptOrganization(org); + const organization = await this.promptOrganization(org, { interactive }); const screen = blessed.screen({ smartCSR: true, fastCSR: true, diff --git a/src/commands/logs.ts b/src/commands/logs.ts index 73058de..dd70cd0 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -3,9 +3,9 @@ import { isNil, omitBy } from 'lodash'; import ms from 'ms'; import { debugLog, squidHistoryLogs, SquidRequest, streamSquidLogs } from '../api'; -import { CliCommand, SqdFlags, SquidReferenceArg } from '../command'; +import { CliCommand, SqdFlags } from '../command'; import { pretty } from '../logs'; -import { formatSquidFullname } from '../utils'; +import { formatSquidReference } from '../utils'; type LogResult = { hasLogs: boolean; @@ -27,33 +27,15 @@ export default class Logs extends CliCommand { static flags = { org: SqdFlags.org({ required: false, - relationships: [ - { - type: 'all', - flags: ['name'], - }, - ], }), name: SqdFlags.name({ required: false, - relationships: [ - { - type: 'some', - flags: [ - { name: 'slot', when: async (flags) => !flags['tag'] }, - { name: 'tag', when: async (flags) => !flags['slot'] }, - ], - }, - ], }), slot: SqdFlags.slot({ required: false, - dependsOn: ['name'], }), tag: SqdFlags.tag({ required: false, - dependsOn: ['name'], - exclusive: ['slot'], }), fullname: SqdFlags.fullname({ required: false, @@ -98,16 +80,15 @@ export default class Logs extends CliCommand { async run(): Promise { const { - flags: { follow, pageSize, container, level, since, search, fullname, ...flags }, + flags: { follow, pageSize, container, level, since, search, fullname, interactive, ...flags }, } = await this.parse(Logs); this.validateSquidNameFlags({ fullname, ...flags }); - const { org, name, tag, slot } = fullname ? fullname : omitBy(flags, isNil); - const reference = formatSquidFullname({ name, slot, tag }); + const { org, name, tag, slot } = fullname ? fullname : (flags as any); - const organization = await this.promptSquidOrganization({ code: org, name }); - const squid = await this.findOrThrowSquid({ organization, reference }); + const organization = await this.promptSquidOrganization(org, name, { interactive }); + const squid = await this.findOrThrowSquid({ organization, squid: { name, slot, tag } }); if (!squid) return; const fromDate = parseDate(since); @@ -115,7 +96,7 @@ export default class Logs extends CliCommand { if (follow) { await this.fetchLogs({ organization, - reference, + squid, reverse: true, query: { limit: 30, @@ -127,7 +108,7 @@ export default class Logs extends CliCommand { }); await streamSquidLogs({ organization, - reference, + squid, onLog: (l) => this.log(l), query: { container, level, search }, }); @@ -138,7 +119,7 @@ export default class Logs extends CliCommand { do { const { hasLogs, nextPage }: LogResult = await this.fetchLogs({ organization, - reference, + squid, query: { limit: pageSize, from: fromDate, @@ -164,7 +145,7 @@ export default class Logs extends CliCommand { async fetchLogs({ organization, - reference, + squid, query, reverse, }: SquidRequest & { @@ -180,7 +161,7 @@ export default class Logs extends CliCommand { }; }): Promise { // eslint-disable-next-line prefer-const - let { logs, nextPage } = await squidHistoryLogs({ organization, reference, query }); + let { logs, nextPage } = await squidHistoryLogs({ organization, squid, query }); if (reverse) { logs = logs.reverse(); diff --git a/src/commands/ls.ts b/src/commands/ls.ts index c465167..7c0869b 100644 --- a/src/commands/ls.ts +++ b/src/commands/ls.ts @@ -1,9 +1,8 @@ import { ux as CliUx, Flags } from '@oclif/core'; -import chalk from 'chalk'; -import { isNil, omitBy } from 'lodash'; import { listSquids } from '../api'; import { CliCommand, SqdFlags } from '../command'; +import { printSquid } from '../utils'; export default class Ls extends CliCommand { static description = 'List squids deployed to the Cloud'; @@ -11,47 +10,52 @@ export default class Ls extends CliCommand { static flags = { org: SqdFlags.org({ required: false, - relationships: [ - { - type: 'all', - flags: ['name'], - }, - ], }), name: SqdFlags.name({ required: false, + relationships: [], + }), + tag: SqdFlags.tag({ + required: false, + dependsOn: [], + }), + slot: SqdFlags.slot({ + required: false, + dependsOn: [], }), fullname: SqdFlags.fullname({ required: false, }), truncate: Flags.boolean({ - char: 't', description: 'Truncate data in columns: false by default', required: false, default: false, + allowNo: true, }), }; async run(): Promise { const { - flags: { truncate, fullname, ...flags }, + flags: { truncate, fullname, interactive, ...flags }, } = await this.parse(Ls); - const noTruncate = !truncate; - const { org, name } = fullname ? fullname : omitBy(flags, isNil); + const { org, name, slot, tag } = fullname ? fullname : (flags as any); const organization = name - ? await this.promptSquidOrganization({ code: org, name }) - : await this.promptOrganization(org); + ? await this.promptSquidOrganization(org, name, { interactive }) + : await this.promptOrganization(org, { interactive }); - const squids = await listSquids({ organization, name }); - if (squids) { + let squids = await listSquids({ organization, name }); + if (tag || slot) { + squids = squids.filter((s) => s.slot === slot || s.tags.some((t) => t.name === tag)); + } + if (squids.length) { CliUx.ux.table( squids, { name: { header: 'Squid', - get: (s) => `${s.name}${chalk.dim(`@${s.slot}`)}`, + get: (s) => `${printSquid(s)}`, }, tags: { header: 'Tags', @@ -70,7 +74,7 @@ export default class Ls extends CliCommand { get: (s) => (s.deployedAt ? new Date(s.deployedAt).toUTCString() : `-`), }, }, - { 'no-truncate': noTruncate }, + { 'no-truncate': !truncate }, ); } } diff --git a/src/commands/restart.ts b/src/commands/restart.ts index d954853..3415ad5 100644 --- a/src/commands/restart.ts +++ b/src/commands/restart.ts @@ -4,7 +4,7 @@ import { isNil, omitBy } from 'lodash'; import { restartSquid } from '../api'; import { SqdFlags } from '../command'; import { DeployCommand } from '../deploy-command'; -import { formatSquidFullname, printSquidFullname } from '../utils'; +import { formatSquidReference as formatSquidReference, printSquid } from '../utils'; import { UPDATE_COLOR } from './deploy'; @@ -14,33 +14,15 @@ export default class Restart extends DeployCommand { static flags = { org: SqdFlags.org({ required: false, - relationships: [ - { - type: 'all', - flags: ['name'], - }, - ], }), name: SqdFlags.name({ required: false, - relationships: [ - { - type: 'some', - flags: [ - { name: 'slot', when: async (flags) => !flags['tag'] }, - { name: 'tag', when: async (flags) => !flags['slot'] }, - ], - }, - ], }), slot: SqdFlags.slot({ required: false, - dependsOn: ['name'], }), tag: SqdFlags.tag({ required: false, - dependsOn: ['name'], - exclusive: ['slot'], }), fullname: SqdFlags.fullname({ required: false, @@ -49,28 +31,20 @@ export default class Restart extends DeployCommand { async run(): Promise { const { - flags: { fullname, ...flags }, + flags: { fullname, interactive, ...flags }, } = await this.parse(Restart); this.validateSquidNameFlags({ fullname, ...flags }); - const { org, name, tag, slot } = fullname ? fullname : omitBy(flags, isNil); - const reference = formatSquidFullname({ name, slot, tag }); + const { org, name, tag, slot } = fullname ? fullname : (flags as any); - const organization = await this.promptSquidOrganization({ code: org, name }); - await this.findOrThrowSquid({ organization, reference }); + const organization = await this.promptSquidOrganization(org, name, { interactive }); + const squid = await this.findOrThrowSquid({ organization, squid: { name, tag, slot } }); - const deployment = await restartSquid({ organization, reference }); + const deployment = await restartSquid({ organization, squid }); await this.pollDeploy({ organization, deploy: deployment }); if (!deployment || !deployment.squid) return; - this.logDeployResult( - UPDATE_COLOR, - `The squid ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} has been successfully restarted`, - ); + this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(squid)} has been successfully restarted`); } } diff --git a/src/commands/rm.ts b/src/commands/rm.ts index 245cba9..8a45e8a 100644 --- a/src/commands/rm.ts +++ b/src/commands/rm.ts @@ -2,49 +2,27 @@ import { Flags } from '@oclif/core'; import inquirer from 'inquirer'; import { deleteSquid } from '../api'; -import { SqdFlags, SquidReferenceArg } from '../command'; +import { SqdFlags } from '../command'; import { DeployCommand } from '../deploy-command'; -import { formatSquidFullname, printSquidFullname } from '../utils'; +import { formatSquidReference, printSquid } from '../utils'; import { DELETE_COLOR } from './deploy'; export default class Rm extends DeployCommand { static description = 'Remove a squid deployed to the Cloud'; - static args = { - squid_reference: SquidReferenceArg, - }; - static flags = { org: SqdFlags.org({ required: false, - relationships: [ - { - type: 'all', - flags: ['name'], - }, - ], }), name: SqdFlags.name({ required: false, - relationships: [ - { - type: 'some', - flags: [ - { name: 'slot', when: async (flags) => !flags['tag'] }, - { name: 'tag', when: async (flags) => !flags['slot'] }, - ], - }, - ], }), slot: SqdFlags.slot({ required: false, - dependsOn: ['name'], }), tag: SqdFlags.tag({ required: false, - dependsOn: ['name'], - exclusive: ['slot'], }), fullname: SqdFlags.fullname({ required: false, @@ -58,39 +36,31 @@ export default class Rm extends DeployCommand { async run(): Promise { const { - flags: { force, fullname, ...flags }, + flags: { interactive, force, fullname, ...flags }, } = await this.parse(Rm); this.validateSquidNameFlags({ fullname, ...flags }); const { org, name, tag, slot } = fullname ? fullname : (flags as any); - const reference = formatSquidFullname({ name, slot, tag }); - const organization = await this.promptSquidOrganization({ code: org, name }); - const squid = await this.findOrThrowSquid({ organization, reference }); + const organization = await this.promptSquidOrganization(org, name, { interactive }); + const squid = await this.findOrThrowSquid({ organization, squid: { name, slot, tag } }); if (!force) { const { confirm } = await inquirer.prompt([ { name: 'confirm', type: 'confirm', - message: `Your squid ${printSquidFullname({ org, name, slot: squid.slot })} will be completely removed. This action can not be undone. Are you sure?`, + message: `Your squid ${printSquid(squid)} will be completely removed. This action can not be undone. Are you sure?`, }, ]); if (!confirm) return; } - const deployment = await deleteSquid({ organization, reference }); + const deployment = await deleteSquid({ organization, squid }); await this.pollDeploy({ organization, deploy: deployment }); if (!deployment || !deployment.squid) return; - this.logDeployResult( - DELETE_COLOR, - `A squid deployment ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} was successfully deleted`, - ); + this.logDeployResult(DELETE_COLOR, `A squid deployment ${printSquid(squid)} was successfully deleted`); } } diff --git a/src/commands/secrets/ls.ts b/src/commands/secrets/ls.ts index 397b753..e81794d 100644 --- a/src/commands/secrets/ls.ts +++ b/src/commands/secrets/ls.ts @@ -16,11 +16,11 @@ export default class Ls extends CliCommand { async run(): Promise { const { - flags: { org }, + flags: { org, interactive }, args: {}, } = await this.parse(Ls); - const organization = await this.promptOrganization(org); + const organization = await this.promptOrganization(org, { interactive }); const response = await listSecrets({ organization }); if (!Object.keys(response.secrets).length) { diff --git a/src/commands/secrets/rm.ts b/src/commands/secrets/rm.ts index 582a376..0fad46c 100644 --- a/src/commands/secrets/rm.ts +++ b/src/commands/secrets/rm.ts @@ -22,11 +22,11 @@ export default class Rm extends CliCommand { async run(): Promise { const { - flags: { org }, + flags: { org, interactive }, args: { name }, } = await this.parse(Rm); - const organization = await this.promptOrganization(org); + const organization = await this.promptOrganization(org, { interactive }); await removeSecret({ organization, name }); this.log(`Secret '${name}' removed`); diff --git a/src/commands/secrets/set.ts b/src/commands/secrets/set.ts index 05f2395..704c3b1 100644 --- a/src/commands/secrets/set.ts +++ b/src/commands/secrets/set.ts @@ -33,11 +33,11 @@ export default class Set extends CliCommand { async run(): Promise { const { - flags: { org }, + flags: { org, interactive }, args: { name, value }, } = await this.parse(Set); - const organization = await this.promptOrganization(org); + const organization = await this.promptOrganization(org, { interactive }); let secretValue = value; if (!secretValue) { diff --git a/src/commands/tags/add.ts b/src/commands/tags/add.ts index 4e50d42..8431997 100644 --- a/src/commands/tags/add.ts +++ b/src/commands/tags/add.ts @@ -5,7 +5,7 @@ import inquirer from 'inquirer'; import { addSquidTag } from '../../api'; import { SqdFlags } from '../../command'; import { DeployCommand } from '../../deploy-command'; -import { formatSquidFullname, printSquidFullname } from '../../utils'; +import { formatSquidReference, printSquid } from '../../utils'; import { UPDATE_COLOR } from '../deploy'; export default class Add extends DeployCommand { @@ -52,60 +52,49 @@ export default class Add extends DeployCommand { fullname: SqdFlags.fullname({ required: false, }), + force: Flags.boolean({ + required: false, + default: false, + }), }; async run(): Promise { const { args: { tag: tagName }, - flags: { fullname, ...flags }, + flags: { fullname, interactive, force, ...flags }, } = await this.parse(Add); this.validateSquidNameFlags({ fullname, ...flags }); const { org, name, tag, slot } = fullname ? fullname : (flags as any); - const reference = formatSquidFullname({ name, slot, tag }); - const organization = await this.promptSquidOrganization({ code: org, name }); - const squid = await this.findOrThrowSquid({ organization, reference }); + const organization = await this.promptSquidOrganization(org, name, { interactive }); + const squid = await this.findOrThrowSquid({ organization, squid: { name, slot, tag } }); if (squid.tags.find((t) => t.name === tagName)) { - return this.log( - `Tag "${tagName}" is already assigned to the squid ${printSquidFullname({ org, name, tag, slot })}`, - ); + return this.log(`Tag "${tagName}" is already assigned to the squid ${printSquid(squid)}`); } - const oldSquid = await this.findSquid({ organization, reference: formatSquidFullname({ name, tag: tagName }) }); - if (oldSquid) { - const { confirm } = await inquirer.prompt([ + if (!force) { + const confirm = await this.promptAddTag( { - name: 'confirm', - type: 'confirm', - message: [ - chalk.reset( - `A squid tag "${tagName}" has already been assigned to the previous squid deployment ${printSquidFullname({ org, name, slot: oldSquid.slot })}.`, - ), - chalk.reset(`The tag URL will be assigned to the newly created deployment. ${chalk.bold(`Are you sure?`)}`), - ].join('\n'), + organization, + name, + tag: tagName, }, - ]); + { interactive }, + ); if (!confirm) return; } const deployment = await addSquidTag({ organization, - reference, + squid, tag: tagName, }); await this.pollDeploy({ organization, deploy: deployment }); if (!deployment || !deployment.squid) return; - this.logDeployResult( - UPDATE_COLOR, - `The squid ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} has been successfully updated`, - ); + this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(squid)} has been successfully updated`); } } diff --git a/src/commands/tags/remove.ts b/src/commands/tags/remove.ts index a267985..3fb36a3 100644 --- a/src/commands/tags/remove.ts +++ b/src/commands/tags/remove.ts @@ -3,7 +3,7 @@ import { Args } from '@oclif/core'; import { removeSquidTag } from '../../api'; import { SqdFlags } from '../../command'; import { DeployCommand } from '../../deploy-command'; -import { formatSquidFullname, printSquidFullname } from '../../utils'; +import { formatSquidReference, printSquid } from '../../utils'; import { UPDATE_COLOR } from '../deploy'; export default class Remove extends DeployCommand { @@ -62,36 +62,28 @@ export default class Remove extends DeployCommand { async run(): Promise { const { args: { tag: tagName }, - flags: { fullname, ...flags }, + flags: { fullname, interactive, ...flags }, } = await this.parse(Remove); this.validateSquidNameFlags({ fullname, ...flags }); const { org, name, tag, slot } = fullname ? fullname : (flags as any); - const reference = formatSquidFullname({ name, slot, tag }); - const organization = await this.promptSquidOrganization({ code: org, name }); - const squid = await this.findOrThrowSquid({ organization, reference }); + const organization = await this.promptSquidOrganization(org, name, { interactive }); + const squid = await this.findOrThrowSquid({ organization, squid: { name, tag, slot } }); if (!squid.tags.some((t) => t.name === tagName)) { - return this.log(`Tag "${tagName}" is not assigned to the squid ${printSquidFullname({ org, name, tag, slot })}`); + return this.log(`Tag "${tagName}" is not assigned to the squid ${printSquid(squid)}`); } const deployment = await removeSquidTag({ organization, - reference, + squid, tag: tagName, }); await this.pollDeploy({ organization, deploy: deployment }); if (!deployment || !deployment.squid) return; - this.logDeployResult( - UPDATE_COLOR, - `The squid ${printSquidFullname({ - org: deployment.organization.code, - name: deployment.squid.name, - slot: deployment.squid.slot, - })} has been successfully updated`, - ); + this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(squid)} has been successfully updated`); } } diff --git a/src/deploy-command.ts b/src/deploy-command.ts index f3ccf59..2179a6c 100644 --- a/src/deploy-command.ts +++ b/src/deploy-command.ts @@ -2,9 +2,9 @@ import { ux as CliUx } from '@oclif/core'; import chalk, { ForegroundColor } from 'chalk'; import inquirer from 'inquirer'; -import { Deployment, DeployRequest, getDeploy, Organization, Squid, streamSquidLogs } from './api'; +import { Deployment, DeployRequest, getDeploy, Organization, Squid, SquidRequest, streamSquidLogs } from './api'; import { CliCommand, SUCCESS_CHECK_MARK } from './command'; -import { doUntil, formatSquidFullname } from './utils'; +import { doUntil, formatSquidReference, printSquid } from './utils'; export abstract class DeployCommand extends CliCommand { deploy: Deployment | undefined; @@ -21,7 +21,7 @@ export abstract class DeployCommand extends CliCommand { { name: 'confirm', type: 'confirm', - message: `Squid "${formatSquidFullname(squid)}" is being deploying. + message: `Squid "${formatSquidReference(squid)}" is being deploying. You can not run deploys on the same squid in parallel. Do you want to attach to the running deploy process?`, }, @@ -40,6 +40,36 @@ Do you want to attach to the running deploy process?`, } } + async promptAddTag( + { organization, name, tag }: { organization: Pick; name: string; tag: string }, + { using = 'using "--force" flag', interactive }: { using?: string; interactive?: boolean } = {}, + ) { + const oldSquid = await this.findSquid({ + organization, + squid: { name, tag }, + }); + if (!oldSquid) return true; + + const warning = `A tag "${tag}" has already been assigned to ${printSquid(oldSquid)}.`; + + if (!interactive) { + this.error([warning, `Please do it explicitly ${using}`].join('\n')); + } + + this.warn(warning); + + const { confirm } = await inquirer.prompt([ + { + name: 'confirm', + type: 'confirm', + message: 'Are you sure?', + prefix: `The tag will be assigned to the newly created squid.`, + }, + ]); + + return !!confirm; + } + async pollDeploy({ deploy, organization, @@ -98,7 +128,8 @@ Do you want to attach to the running deploy process?`, CliUx.ux.action.start('◷ Syncing the squid addons'); return false; - case 'CONFIGURING_INGRESS': + case 'ADDING_INGRESS': + case 'REMOVING_INGRESS': CliUx.ux.action.start('◷ Configuring ingress'); return false; @@ -120,12 +151,12 @@ Do you want to attach to the running deploy process?`, return this.deploy; } - async streamLogs(organization: Organization, squid: Pick) { + async streamLogs({ organization, squid }: SquidRequest) { CliUx.ux.action.start(`Streaming logs from the squid`); await streamSquidLogs({ organization, - reference: squid.reference, + squid, onLog: (l) => this.log(l), }); } @@ -170,7 +201,7 @@ Do you want to attach to the running deploy process?`, ); if (this.deploy?.squid) { - errors.push(`${chalk.dim('Squid:')} ${formatSquidFullname(this.deploy.squid)}`); + errors.push(`${chalk.dim('Squid:')} ${formatSquidReference(this.deploy.squid)}`); } } @@ -191,6 +222,7 @@ Do you want to attach to the running deploy process?`, chalk[color](`=================================================`), message, chalk[color](`=================================================`), + '', ].join('\n'), ); } diff --git a/src/flags/fullname.ts b/src/flags/fullname.ts index a64721f..094a7f4 100644 --- a/src/flags/fullname.ts +++ b/src/flags/fullname.ts @@ -1,9 +1,9 @@ import { Flags } from '@oclif/core'; -import { ParsedSquidFullname, parseSquidFullname, SQUID_FULLNAME_REGEXP } from '../utils'; +import { ParsedSquidReference, parseSquidReference, SQUID_FULLNAME_REGEXP } from '../utils'; -export const fullname = Flags.custom({ - helpGroup: 'COMMON', +export const fullname = Flags.custom({ + helpGroup: 'SQUID', name: 'fullname', aliases: ['ref'], description: `Reference of a squid`, @@ -16,6 +16,6 @@ export const fullname = Flags.custom({ throw new Error(`Expected a squid reference name but received: ${input}`); } - return parseSquidFullname(input); + return parseSquidReference(input); }, }); diff --git a/src/flags/name.ts b/src/flags/name.ts index 7fa4c4c..fee828b 100644 --- a/src/flags/name.ts +++ b/src/flags/name.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; export const name = Flags.custom({ - helpGroup: 'COMMON', + helpGroup: 'SQUID', char: 'n', name: 'name', description: 'Squid name', @@ -10,4 +10,13 @@ export const name = Flags.custom({ parse: async (input) => { return input.toLowerCase(); }, + relationships: [ + { + type: 'some', + flags: [ + { name: 'slot', when: async (flags) => !flags['tag'] }, + { name: 'tag', when: async (flags) => !flags['slot'] }, + ], + }, + ], }); diff --git a/src/flags/org.ts b/src/flags/org.ts index a30c81b..53bcfd2 100644 --- a/src/flags/org.ts +++ b/src/flags/org.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; export const org = Flags.custom({ - helpGroup: 'COMMON', + helpGroup: 'ORG', char: 'o', name: 'org', description: 'Organization code', diff --git a/src/flags/slot.ts b/src/flags/slot.ts index a193ba3..1d45e5f 100644 --- a/src/flags/slot.ts +++ b/src/flags/slot.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; export const slot = Flags.custom({ - helpGroup: 'COMMON', + helpGroup: 'SQUID', char: 's', name: 'slot', description: 'Squid slot', @@ -10,4 +10,6 @@ export const slot = Flags.custom({ return input.toLowerCase(); }, required: false, + dependsOn: ['name'], + exclusive: ['tag'], }); diff --git a/src/flags/tag.ts b/src/flags/tag.ts index 05de0be..0285ecc 100644 --- a/src/flags/tag.ts +++ b/src/flags/tag.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; export const tag = Flags.custom({ - helpGroup: 'COMMON', + helpGroup: 'SQUID', char: 't', name: 'tag', description: 'Squid tag', @@ -10,4 +10,6 @@ export const tag = Flags.custom({ parse: async (input) => { return input.toLowerCase(); }, + dependsOn: ['name'], + exclusive: ['slot'], }); diff --git a/src/ui/components/VersionLogsTab.ts b/src/ui/components/VersionLogsTab.ts index 7983392..627710d 100644 --- a/src/ui/components/VersionLogsTab.ts +++ b/src/ui/components/VersionLogsTab.ts @@ -39,7 +39,7 @@ export class VersionLogTab implements VersionTab { try { const { logs } = await squidHistoryLogs({ organization: squid.organization, - reference: squid.reference, + squid, query: { limit: 100, from: addMinutes(new Date(), -30), @@ -61,7 +61,7 @@ export class VersionLogTab implements VersionTab { streamSquidLogs({ organization: squid.organization, - reference: squid.reference, + squid, onLog: (line) => { logsBox.add(line); }, diff --git a/src/ui/components/types.ts b/src/ui/components/types.ts index c862f2f..b5e529c 100644 --- a/src/ui/components/types.ts +++ b/src/ui/components/types.ts @@ -1,5 +1,5 @@ import { Squid as ApiSquid } from '../../api'; -import { formatSquidFullname } from '../../utils'; +import { formatSquidReference } from '../../utils'; export interface Squid extends ApiSquid {} export class Squid { @@ -8,7 +8,7 @@ export class Squid { constructor(squid: ApiSquid) { Object.assign(this, squid); - this.displayName = formatSquidFullname({ name: this.name, slot: this.slot }); + this.displayName = formatSquidReference({ name: this.name, slot: this.slot }); if (this.tags.length) { this.displayName += ` (${this.tags.map((a) => a.name).join(', ')})`; diff --git a/src/utils.ts b/src/utils.ts index daf2805..975e09e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ import { ConfigNotFound, getConfig } from '@subsquid/commands'; import chalk from 'chalk'; +import { PickDeep } from 'type-fest'; + +import { Squid } from './api'; export async function getSquidCommands() { try { @@ -23,32 +26,37 @@ export async function doUntil(fn: () => Promise, { pause }: { pause: nu } } -export type ParsedSquidFullname = { org?: string; name: string } & ( +export type ParsedSquidReference = { org?: string; name: string } & ( | { slot: string; tag?: never } | { slot?: never; tag: string } + | { slot: string; tag: string } ); -export function formatSquidFullname({ org, name, slot, tag }: ParsedSquidFullname) { - let res = org ? `${org}/` : ''; - res += name; - res += slot ? `@${slot}` : `:${tag}`; +export function formatSquidReference( + reference: ParsedSquidReference | string, + { colored }: { colored?: boolean } = {}, +) { + const { org, name, slot, tag } = typeof reference === 'string' ? parseSquidReference(reference) : reference; - return res; -} + const prefix = org ? `${org}/` : ``; + const suffix = slot ? `@${slot}` : `:${tag}`; -export function printSquidFullname(args: ParsedSquidFullname) { - return chalk.bold(formatSquidFullname(args)); + return colored ? chalk`{bold {green ${prefix}}{green ${name}}{blue ${suffix}}}` : `${prefix}${name}${suffix}`; } export const SQUID_FULLNAME_REGEXP = /^(([a-z0-9\-]+)\/)?([a-z0-9\-]+)([:@])([a-z0-9\-]+)$/; -export function parseSquidFullname(fullname: string): ParsedSquidFullname { - const parsed = SQUID_FULLNAME_REGEXP.exec(fullname); +export function parseSquidReference(reference: string): ParsedSquidReference { + const parsed = SQUID_FULLNAME_REGEXP.exec(reference); if (!parsed) { - throw new Error(`Invalid squid full name: "${fullname}"`); + throw new Error(`Invalid squid full name: "${reference}"`); } const [, , org, name, type, tagOrSlot] = parsed; return { org, name, ...(type === ':' ? { tag: tagOrSlot } : { slot: tagOrSlot }) }; } + +export function printSquid(squid: PickDeep) { + return formatSquidReference({ org: squid.organization.code, name: squid.name, slot: squid.slot }, { colored: true }); +}