diff --git a/src/command.ts b/src/command.ts index 740a748..48e7eab 100644 --- a/src/command.ts +++ b/src/command.ts @@ -4,7 +4,16 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { isNil, uniqBy } from 'lodash'; -import { ApiError, getOrganization, getSquid, listOrganizations, listUserSquids, SquidRequest } from './api'; +import { + ApiError, + getOrganization, + getSquid, + listOrganizations, + listSquids, + listUserSquids, + Squid, + SquidRequest, +} from './api'; import { getTTY } from './tty'; import { formatSquidReference, printSquid } from './utils'; @@ -13,7 +22,7 @@ export const SUCCESS_CHECK_MARK = chalk.green('✓'); export abstract class CliCommand extends Command { static baseFlags = { interactive: Flags.boolean({ - description: 'Disable interactive mode', + description: 'Enable interactive mode. Use --no-interactive to disable prompts for CI/scripts.', required: false, default: true, allowNo: true, @@ -32,7 +41,6 @@ export abstract class CliCommand extends Command { this.log(chalk.dim(message)); } - // Haven't find a way to do it with native settings validateSquidNameFlags(flags: { reference?: any; name?: any }) { if (flags.reference || flags.name) return; @@ -41,7 +49,13 @@ export abstract class CliCommand extends Command { { name: 'squid name', validationFn: 'validateSquidName', - reason: 'One of the following must be provided: --reference, --name', + 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', }, ], @@ -165,6 +179,53 @@ export abstract class CliCommand extends Command { 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[], { @@ -186,8 +247,10 @@ export abstract class CliCommand extends Command { return this.error( [ `You have ${organizations.length} organizations:`, - ...organizations.map((o) => `${chalk.dim(' - ')}${chalk.dim(o.code)}`), - `Please specify one of them explicitly ${using}`, + ...organizations.map((o) => ` - ${o.code}`), + ``, + `Please specify one of them explicitly ${using}.`, + `Example: sqd --org ${organizations[0].code}`, ].join('\n'), ); } diff --git a/src/commands/auth.ts b/src/commands/auth.ts index d0dabed..8210fb8 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -7,10 +7,12 @@ import { DEFAULT_API_URL, setConfig } from '../config'; export default class Auth extends CliCommand { static description = `Log in to the Cloud`; + static examples = ['sqd auth -k sqd_xyz123...']; + static flags = { key: Flags.string({ char: 'k', - description: 'Cloud auth key. Log in to https://app.subsquid.io to create or update your key.', + description: 'Cloud auth key. Log in to https://cloud.sqd.dev to create or update your key.', required: true, }), host: Flags.string({ diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index c12f3b2..80ec673 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -54,7 +54,6 @@ export function resolveManifest( } function example(command: string, description: string) { - // return [chalk.dim(`// ${description}`), command].join('\r\n'); return `${command} ${chalk.dim(`// ${description}`)}`; } @@ -77,7 +76,7 @@ export default class Deploy extends DeployCommand { ), ]; - static help = 'If squid flags are not specified, the they will be retrieved from the manifest or prompted.'; + static help = 'If squid flags are not specified, they will be retrieved from the manifest or prompted.'; static args = { source: Args.directory({ @@ -149,6 +148,11 @@ export default class Deploy extends DeployCommand { required: false, default: false, }), + 'allow-postgres-deletion': Flags.boolean({ + description: 'Allow deleting an existing Postgres addon when deploying a manifest without one', + required: false, + default: false, + }), }; async run(): Promise { @@ -160,17 +164,12 @@ export default class Deploy extends DeployCommand { 'hard-reset': hardReset, 'stream-logs': streamLogs, 'add-tag': addTag, + 'allow-postgres-deletion': allowPostgresDeletion, reference, ...flags }, } = await this.parse(Deploy); - const isUrl = source.startsWith('http://') || source.startsWith('https://'); - if (isUrl) { - this.log(`🦑 Releasing the squid from remote`); - return this.error('Not implemented yet'); - } - if (interactive && hardReset) { const { confirm } = await inquirer.prompt([ { @@ -193,7 +192,6 @@ export default class Deploy extends DeployCommand { const overrides = reference || (pick(flags, 'slot', 'name', 'tag', 'org') as Partial); let manifest = res.manifest; - // FIXME: it is not possible to override org atm if (entries(overrides).some(([k, v]) => k !== 'org' && get(manifest, k) !== v)) { // we need to do it to keep formatting the same const manifestRaw = Manifest.replace(res.manifestRaw, {}); @@ -272,7 +270,7 @@ export default class Deploy extends DeployCommand { /** * Warn if the existing squid has a Postgres addon but the new manifest removes it */ - if (!hardReset && target?.addons?.postgres && !manifest.deploy?.addons?.postgres) { + if (!hardReset && target?.addons?.postgres && !manifest.deploy?.addons?.postgres && !allowPostgresDeletion) { const confirmed = await this.promptPostgresDeletion(target, { interactive }); if (!confirmed) return; } @@ -307,6 +305,12 @@ export default class Deploy extends DeployCommand { const deployment = await this.pollDeploy({ organization, deploy }); if (!deployment || !deployment.squid) return; + const squidRef = formatSquidReference({ + org: deployment.organization.code, + name: deployment.squid.name, + slot: deployment.squid.slot, + }); + if (target) { this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(target)} has been successfully updated`); } else { @@ -344,7 +348,13 @@ export default class Deploy extends DeployCommand { if (interactive) { this.warn(warning.join('\n')); } else { - this.error([...warning, `Please do it explicitly ${using}`].join('\n')); + this.error( + [ + ...warning, + `Please do it explicitly ${using}.`, + `Example: sqd deploy . --name ${squid.name} --allow-update`, + ].join('\n'), + ); } const { confirm } = await inquirer.prompt([ @@ -362,7 +372,7 @@ export default class Deploy extends DeployCommand { private async promptOverrideConflict( dest: string, src: string, - { using = 'using "--allow--manifest-override" flag', interactive }: { using?: string; interactive?: boolean } = {}, + { using = 'using "--allow-manifest-override" flag', interactive }: { using?: string; interactive?: boolean } = {}, ) { const warning = [ 'Conflict detected!', @@ -375,7 +385,9 @@ export default class Deploy extends DeployCommand { if (interactive) { this.warn(warning); } else { - this.error([warning, `Please do it explicitly ${using}`].join('\n')); + this.error( + [warning, `Please do it explicitly ${using}.`, `Example: sqd deploy . --allow-manifest-override`].join('\n'), + ); } this.log( @@ -400,7 +412,13 @@ export default class Deploy extends DeployCommand { const warning = `The new manifest does not include "addons.postgres", but the squid ${printSquid(squid)} currently has a Postgres database. Deploying will permanently delete the database and all its data.`; if (!interactive) { - this.error([warning, `Please do it explicitly ${using}`].join('\n')); + this.error( + [ + warning, + `Please do it explicitly using "--allow-postgres-deletion" flag.`, + `Example: sqd deploy . --allow-postgres-deletion`, + ].join('\n'), + ); } this.warn(warning); @@ -428,7 +446,9 @@ export default class Deploy extends DeployCommand { if (interactive) { this.warn(warning); } else { - this.error([warning, `Please specify it explicitly ${using}`].join('\n')); + this.error( + [warning, `Please specify it explicitly ${using}.`, `Example: sqd deploy . --name my-squid`].join('\n'), + ); } const { input } = await inquirer.prompt([ diff --git a/src/commands/deploy.unit.spec.ts b/src/commands/deploy.unit.spec.ts index 3363c11..b9b60e4 100644 --- a/src/commands/deploy.unit.spec.ts +++ b/src/commands/deploy.unit.spec.ts @@ -33,9 +33,7 @@ describe('Deploy', () => { ? { postgres: { connections: [], disk: { usageStatus: 'NORMAL', usedBytes: 0, totalBytes: 0 } } } : undefined, manifest: { - current: pgVersion - ? { deploy: { addons: { postgres: { version: pgVersion } } } } - : { deploy: {} }, + current: pgVersion ? { deploy: { addons: { postgres: { version: pgVersion } } } } : { deploy: {} }, raw: '', }, } as any; diff --git a/src/commands/explorer.ts b/src/commands/explorer.ts index 9701be6..7f9c2cd 100644 --- a/src/commands/explorer.ts +++ b/src/commands/explorer.ts @@ -9,7 +9,7 @@ export default class Explorer extends CliCommand { static hidden = true; static description = 'Open a visual explorer for the Cloud deployments'; - // static hidden = true; + static flags = { org: Flags.string({ char: 'o', @@ -27,9 +27,6 @@ export default class Explorer extends CliCommand { const screen = blessed.screen({ smartCSR: true, fastCSR: true, - // dockBorders: true, - debug: true, - // autoPadding: true, fullUnicode: true, }); @@ -59,8 +56,6 @@ export default class Explorer extends CliCommand { return process.exit(0); }); - // screen.program.disableMouse(); - manager.focus(); screen.render(); } diff --git a/src/commands/gateways/list.ts b/src/commands/gateways/list.ts index 503797f..20006d7 100644 --- a/src/commands/gateways/list.ts +++ b/src/commands/gateways/list.ts @@ -11,6 +11,12 @@ export default class Ls extends CliCommand { static description = 'List available gateways'; + static examples = [ + 'sqd gateways list', + 'sqd gateways list --type evm', + 'sqd gateways list --type evm --name ethereum --chain 1', + ]; + static flags = { type: Flags.string({ char: 't', @@ -97,7 +103,7 @@ export default class Ls extends CliCommand { }, }); - gateways.map(({ chainName, chainId, chainSS58Prefix, providers }) => { + gateways.forEach(({ chainName, chainId, chainSS58Prefix, providers }) => { const row = [chainName, chalk.dim(chainId || chainSS58Prefix || '-'), providers[0].dataSourceUrl]; table.push(row); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index a719b9d..098b02a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -80,6 +80,13 @@ const SQUID_TEMPLATE_DESC = [ export default class Init extends CliCommand { static description = 'Setup a new squid project from a template or github repo'; + static examples = [ + 'sqd init my-squid --template evm', + 'sqd init my-squid -t substrate', + 'sqd init my-squid -t https://github.com/user/repo -d ./target-dir', + 'sqd init my-squid -t evm -r', + ]; + static args = { name: Args.string({ description: SQUID_NAME_DESC.join('\n'), required: true }), }; @@ -105,7 +112,7 @@ export default class Init extends CliCommand { async run() { const { args: { name }, - flags: { template, dir, remove }, + flags: { template, dir, remove, interactive }, } = await this.parse(Init); const localDir = path.resolve(dir || name); @@ -122,6 +129,18 @@ export default class Init extends CliCommand { let resolvedTemplate = template || ''; if (!template) { + if (!interactive) { + const templateNames = Object.keys(TEMPLATE_ALIASES).join(', '); + return this.error( + [ + `No template specified.`, + `Available templates: ${templateNames}`, + ``, + `Example: sqd init ${name} --template evm`, + ].join('\n'), + ); + } + const { alias } = await inquirer.prompt({ name: 'alias', message: `Please select one of the templates for your "${name}" squid:`, @@ -157,7 +176,9 @@ export default class Init extends CliCommand { /** Remove deprecated files from repositories **/ try { await asyncFs.rm(path.resolve(localDir, 'Dockerfile')); - } catch (e) {} + } catch (e: any) { + if (e.code !== 'ENOENT') throw e; + } const manifestPath = path.resolve(localDir, 'squid.yaml'); try { diff --git a/src/commands/list.ts b/src/commands/list.ts index fae721e..d3df385 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -3,12 +3,19 @@ import { ux as CliUx, Flags } from '@oclif/core'; import { listSquids } from '../api'; import { CliCommand, SqdFlags } from '../command'; import { printSquid } from '../utils'; +import { formatSquidJson } from './view'; export default class List extends CliCommand { static aliases = ['ls']; static description = 'List squids deployed to the Cloud'; + static examples = [ + 'sqd list --org my-org', + 'sqd ls --org my-org --name my-squid --tag prod', + 'sqd list --org my-org --json', + ]; + static flags = { org: SqdFlags.org({ required: false, @@ -34,11 +41,14 @@ export default class List extends CliCommand { default: false, allowNo: true, }), + json: Flags.boolean({ + description: 'Output in JSON format', + }), }; async run(): Promise { const { - flags: { truncate, reference, interactive, ...flags }, + flags: { truncate, json, reference, interactive, ...flags }, } = await this.parse(List); const { org, name, slot, tag } = reference ? reference : (flags as any); @@ -51,6 +61,15 @@ export default class List extends CliCommand { if (tag || slot) { squids = squids.filter((s) => s.slot === slot || s.tags.some((t) => t.name === tag)); } + + if (json) { + return this.log(JSON.stringify(squids.map(formatSquidJson), null, 2)); + } + + if (!squids.length) { + return this.log('No squids found'); + } + if (squids.length) { CliUx.ux.table( squids, diff --git a/src/commands/logs.ts b/src/commands/logs.ts index dd36438..94ad103 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -24,6 +24,13 @@ function parseDate(str: string): Date { export default class Logs extends CliCommand { static description = 'Fetch logs from a squid deployed to the Cloud'; + static examples = [ + 'sqd logs --reference my-squid@v1 --org my-org', + 'sqd logs --name my-squid --slot abc123 --since 2h', + 'sqd logs --reference my-squid@v1 -f', + 'sqd logs --reference my-squid@v1 --level error --container processor', + ]; + static flags = { org: SqdFlags.org({ required: false, @@ -74,7 +81,7 @@ export default class Logs extends CliCommand { summary: 'Follow', required: false, default: false, - exclusive: ['fromDate', 'pageSize'], + exclusive: ['since', 'pageSize'], }), }; @@ -116,6 +123,7 @@ export default class Logs extends CliCommand { return; } let cursor = undefined; + let isFirstPage = true; do { const { hasLogs, nextPage }: LogResult = await this.fetchLogs({ organization, @@ -129,11 +137,12 @@ export default class Logs extends CliCommand { search, }, }); - if (!hasLogs) { + if (!hasLogs && isFirstPage) { this.log('No logs found'); return; } - if (nextPage) { + isFirstPage = false; + if (nextPage && interactive) { const more = await CliUx.ux.prompt(`type "it" to fetch more logs...`); if (more !== 'it') { return; diff --git a/src/commands/prod.ts b/src/commands/prod.ts index d11d1dc..04c3c3f 100644 --- a/src/commands/prod.ts +++ b/src/commands/prod.ts @@ -9,7 +9,6 @@ export default class Prod extends Command { async run(): Promise { await this.parse(Prod); - // TODO write description this.log( [ chalk.yellow('*******************************************************'), diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 0a39946..a8f72b5 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -5,7 +5,7 @@ import inquirer from 'inquirer'; import { deleteSquid } from '../api'; import { SqdFlags } from '../command'; import { DeployCommand } from '../deploy-command'; -import { ParsedSquidReference, printSquid } from '../utils'; +import { formatSquidReference, ParsedSquidReference, printSquid } from '../utils'; import { DELETE_COLOR } from './deploy'; @@ -14,6 +14,12 @@ export default class Remove extends DeployCommand { static aliases = ['rm']; + static examples = [ + 'sqd remove --reference my-squid@v1 --force', + 'sqd remove --name my-squid --slot abc123 --org my-org --force', + 'sqd rm --reference my-squid@v1 -f', + ]; + static flags = { org: SqdFlags.org({ required: false, @@ -60,7 +66,14 @@ export default class Remove extends DeployCommand { ]; if (!interactive && !force) { - this.error([...warning, 'Please do it explicitly using --force flag'].join('\n')); + this.error( + [ + ...warning, + '', + 'Please do it explicitly using --force flag.', + `Example: sqd remove --reference ${name}@${slot || tag || 'v1'} --force`, + ].join('\n'), + ); } else { this.warn(warning.join('\n')); } @@ -83,5 +96,8 @@ export default class Remove extends DeployCommand { if (!deployment || !deployment.squid) return; this.logDeployResult(DELETE_COLOR, `The squid ${printSquid(squid)} was successfully deleted`); + this.log(`squid: ${formatSquidReference({ name: squid.name, slot: squid.slot })}`); + this.log(`deploy_id: ${deployment.id}`); + this.log(`duration: ${Math.round(deployment.totalElapsedTimeMs / 1000)}s`); } } diff --git a/src/commands/restart.ts b/src/commands/restart.ts index da25567..e803b4c 100644 --- a/src/commands/restart.ts +++ b/src/commands/restart.ts @@ -4,13 +4,15 @@ import { isNil, omitBy } from 'lodash'; import { restartSquid } from '../api'; import { SqdFlags } from '../command'; import { DeployCommand } from '../deploy-command'; -import { formatSquidReference as formatSquidReference, printSquid } from '../utils'; +import { printSquid } from '../utils'; import { UPDATE_COLOR } from './deploy'; export default class Restart extends DeployCommand { static description = 'Restart a squid deployed to the Cloud'; + static examples = ['sqd restart --reference my-squid@v1', 'sqd restart --name my-squid --slot abc123 --org my-org']; + static flags = { org: SqdFlags.org({ required: false, @@ -49,5 +51,8 @@ export default class Restart extends DeployCommand { if (!deployment || !deployment.squid) return; this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(squid)} has been successfully restarted`); + this.log(`squid: ${squid.name}@${squid.slot}`); + this.log(`deploy_id: ${deployment.id}`); + this.log(`duration: ${Math.round(deployment.totalElapsedTimeMs / 1000)}s`); } } diff --git a/src/commands/run.ts b/src/commands/run.ts index 1e17f80..1c146d8 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -124,6 +124,13 @@ function isSkipped({ include, exclude }: { include?: string[]; exclude?: string[ export default class Run extends CliCommand { static description = 'Run a squid project locally'; + static examples = [ + 'sqd run .', + 'sqd run . -m squid.prod.yaml', + 'sqd run . --exclude api', + 'sqd run . --include processor --retries -1', + ]; + static flags = { manifest: Flags.string({ char: 'm', @@ -175,7 +182,7 @@ export default class Run extends CliCommand { path: path.isAbsolute(envFile) ? envFile : path.join(squidDir, envFile), }); if (error) { - return this.error(error); + return this.error(error.message); } } diff --git a/src/commands/secrets/list.ts b/src/commands/secrets/list.ts index 4d8c7c3..b0a3728 100644 --- a/src/commands/secrets/list.ts +++ b/src/commands/secrets/list.ts @@ -1,17 +1,17 @@ -import { ux as CliUx, Flags } from '@oclif/core'; +import { ux as CliUx } from '@oclif/core'; import { listSecrets } from '../../api'; -import { CliCommand } from '../../command'; +import { CliCommand, SqdFlags } from '../../command'; export default class Ls extends CliCommand { static aliases = ['secrets ls']; static description = 'List organization secrets in the Cloud'; + static examples = ['sqd secrets list --org my-org']; + static flags = { - org: Flags.string({ - char: 'o', - description: 'Organization', + org: SqdFlags.org({ required: false, }), }; @@ -29,10 +29,7 @@ export default class Ls extends CliCommand { return this.log('There are no secrets'); } - const values: { name: string; value: string }[] = []; - for (const secret in response.secrets) { - values.push({ name: secret, value: response.secrets[secret] }); - } + const values = Object.entries(response.secrets).map(([name, value]) => ({ name, value })); CliUx.ux.table( values, { diff --git a/src/commands/secrets/remove.ts b/src/commands/secrets/remove.ts index fab4764..888893c 100644 --- a/src/commands/secrets/remove.ts +++ b/src/commands/secrets/remove.ts @@ -1,12 +1,15 @@ -import { Flags, Args } from '@oclif/core'; +import { Args } from '@oclif/core'; import { removeSecret } from '../../api'; -import { CliCommand } from '../../command'; +import { CliCommand, SqdFlags } from '../../command'; export default class Rm extends CliCommand { static aliases = ['secrets rm']; static description = 'Delete an organization secret in the Cloud'; + + static examples = ['sqd secrets remove DB_PASSWORD --org my-org']; + static args = { name: Args.string({ description: 'The secret name', @@ -15,9 +18,7 @@ export default class Rm extends CliCommand { }; static flags = { - org: Flags.string({ - char: 'o', - description: 'Organization', + org: SqdFlags.org({ required: false, }), }; @@ -31,6 +32,6 @@ export default class Rm extends CliCommand { const organization = await this.promptOrganization(org, { interactive }); await removeSecret({ organization, name }); - this.log(`Secret '${name}' removed`); + this.logSuccess(` Secret '${name}' removed`); } } diff --git a/src/commands/secrets/set.ts b/src/commands/secrets/set.ts index 704c3b1..e2c4b55 100644 --- a/src/commands/secrets/set.ts +++ b/src/commands/secrets/set.ts @@ -1,9 +1,7 @@ -import { Args, Flags } from '@oclif/core'; +import { Args } from '@oclif/core'; import { setSecret } from '../../api'; -import { CliCommand } from '../../command'; - -// TODO move to new API using put method +import { CliCommand, SqdFlags } from '../../command'; export default class Set extends CliCommand { static description = [ @@ -12,6 +10,11 @@ export default class Set extends CliCommand { `NOTE: The changes take affect only after a squid is restarted or updated.`, ].join('\n'); + static examples = [ + 'sqd secrets set DB_PASSWORD my-secret-value --org my-org', + 'echo "my-secret" | sqd secrets set DB_PASSWORD --org my-org', + ]; + static args = { name: Args.string({ description: 'The secret name', @@ -19,14 +22,12 @@ export default class Set extends CliCommand { }), value: Args.string({ description: 'The secret value', - required: true, + required: false, }), }; static flags = { - org: Flags.string({ - char: 'o', - description: 'Organization', + org: SqdFlags.org({ required: false, }), }; diff --git a/src/commands/tags/add.ts b/src/commands/tags/add.ts index a1136e7..5d92cf2 100644 --- a/src/commands/tags/add.ts +++ b/src/commands/tags/add.ts @@ -5,12 +5,17 @@ import inquirer from 'inquirer'; import { addSquidTag } from '../../api'; import { SqdFlags } from '../../command'; import { DeployCommand } from '../../deploy-command'; -import { formatSquidReference, printSquid } from '../../utils'; +import { printSquid } from '../../utils'; import { UPDATE_COLOR } from '../deploy'; export default class Add extends DeployCommand { static description = 'Add a tag to a squid'; + static examples = [ + 'sqd tags add prod --reference my-squid@v1 --org my-org', + 'sqd tags add prod --name my-squid --slot abc123 --allow-tag-reassign', + ]; + static args = { tag: Args.string({ description: `New tag to assign`, diff --git a/src/commands/tags/remove.ts b/src/commands/tags/remove.ts index 16f5dac..d1c3794 100644 --- a/src/commands/tags/remove.ts +++ b/src/commands/tags/remove.ts @@ -3,15 +3,20 @@ import { Args } from '@oclif/core'; import { removeSquidTag } from '../../api'; import { SqdFlags } from '../../command'; import { DeployCommand } from '../../deploy-command'; -import { formatSquidReference, printSquid } from '../../utils'; +import { printSquid } from '../../utils'; import { UPDATE_COLOR } from '../deploy'; export default class Remove extends DeployCommand { static description = 'Remove a tag from a squid'; + static examples = [ + 'sqd tags remove prod --reference my-squid@v1 --org my-org', + 'sqd tags remove prod --name my-squid --slot abc123', + ]; + static args = { tag: Args.string({ - description: `New tag to assign`, + description: `Tag to remove`, required: true, }), }; diff --git a/src/commands/view.ts b/src/commands/view.ts index 93158fd..c00d731 100644 --- a/src/commands/view.ts +++ b/src/commands/view.ts @@ -1,13 +1,10 @@ -import { json } from 'stream/consumers'; - import { ux as CliUx, Flags } from '@oclif/core'; -import { Manifest, ManifestValue } from '@subsquid/manifest'; +import { ManifestValue } from '@subsquid/manifest'; import chalk from 'chalk'; -import { func } from 'joi'; -import { startCase, toUpper } from 'lodash'; +import { startCase } from 'lodash'; import prettyBytes from 'pretty-bytes'; -import { getSquid, Squid, SquidAddonsPostgres } from '../api'; +import { getSquid, Squid } from '../api'; import { SquidAddonsHasuraResponseStatus, SquidApiResponseStatus, @@ -21,6 +18,12 @@ import { printSquid } from '../utils'; export default class View extends CliCommand { static description = 'View information about a squid'; + static examples = [ + 'sqd view --reference my-squid@v1', + 'sqd view --name my-squid --slot abc123 --org my-org', + 'sqd view --reference my-squid@v1 --json', + ]; + static flags = { org: SqdFlags.org({ required: false, @@ -47,18 +50,22 @@ export default class View extends CliCommand { flags: { reference, interactive, json, ...flags }, } = await this.parse(View); - this.validateSquidNameFlags({ reference, ...flags }); - const { org, name, slot, tag } = reference ? reference : (flags as any); - const organization = name - ? await this.promptSquidOrganization(org, name, { interactive }) - : await this.promptOrganization(org, { interactive }); + let squid; + if (name || reference) { + const organization = name + ? await this.promptSquidOrganization(org, name, { interactive }) + : await this.promptOrganization(org, { interactive }); - const squid = await getSquid({ organization, squid: { name, tag, slot } }); + squid = await getSquid({ organization, squid: { name, tag, slot } }); + } else { + const organization = await this.promptOrganization(org, { interactive }); + squid = await this.promptSquid(organization, { interactive }); + } if (json) { - return this.log(JSON.stringify(squid, null, 2)); + return this.log(JSON.stringify(formatSquidJson(squid), null, 2)); } this.log(`${chalk.bold('SQUID:')} ${printSquid(squid)} (${squid.tags.map((t) => t.name).join(', ')})`); @@ -124,7 +131,7 @@ export default class View extends CliCommand { name: 'Progress', value: `${formatNumber(processor.syncState.currentBlock)}/${formatNumber(processor.syncState.totalBlocks)} ` + - `(${Math.round((processor.syncState.currentBlock / processor.syncState.totalBlocks) * 100)}%)`, + `(${processor.syncState.totalBlocks > 0 ? Math.round((processor.syncState.currentBlock / processor.syncState.totalBlocks) * 100) : 0}%)`, }, { name: 'Profile', @@ -276,3 +283,53 @@ function formatPostgresStatus(status?: SquidDiskResponseUsageStatus): any { export function formatNumber(value: number) { return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(value); } + +export function formatSquidJson(squid: Squid) { + return { + name: squid.name, + reference: squid.reference, + slot: squid.slot, + description: squid.description ?? null, + tags: squid.tags.map((t) => t.name), + status: squid.status ?? null, + organization: { + name: squid.organization.name, + code: squid.organization.code, + }, + api: squid.api + ? { + status: squid.api.status, + urls: squid.api.urls?.map((u) => u.url) ?? [], + } + : null, + processors: (squid.processors ?? []).map((p) => ({ + name: p.name, + status: p.status, + syncState: p.syncState, + })), + addons: { + postgres: squid.addons?.postgres + ? { + connections: squid.addons.postgres.connections?.map((c) => c.uri) ?? [], + disk: squid.addons.postgres.disk, + } + : null, + neon: squid.addons?.neon + ? { + connections: squid.addons.neon.connections?.map((c) => c.uri) ?? [], + } + : null, + hasura: squid.addons?.hasura + ? { + status: squid.addons.hasura.status, + urls: squid.addons.hasura.urls?.map((u) => u.url) ?? [], + replicas: squid.addons.hasura.replicas, + } + : null, + }, + links: squid.links, + deployedAt: squid.deployedAt ?? null, + hibernatedAt: squid.hibernatedAt ?? null, + createdAt: squid.createdAt, + }; +} diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 6f5b6b5..4bedd32 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -5,6 +5,8 @@ import { getConfig } from '../config'; export default class Whoami extends CliCommand { static description = `Show the user details for the current Cloud account`; + static examples = ['sqd whoami']; + async run(): Promise { await this.parse(Whoami); @@ -18,6 +20,6 @@ export default class Whoami extends CliCommand { this.log(`Username: ${username}`); } this.log(`API URL: ${apiUrl}`); - this.log(`Token: ${credentials}`); + this.log(`Token: ${'*'.repeat(Math.max(0, credentials.length - 4))}${credentials.slice(-4)}`); } } diff --git a/src/deploy-command.ts b/src/deploy-command.ts index 64df5d0..64e5bfa 100644 --- a/src/deploy-command.ts +++ b/src/deploy-command.ts @@ -14,11 +14,10 @@ export abstract class DeployCommand extends CliCommand { if (!squid.lastDeploy) return false; if (squid.status !== 'DEPLOYING') return false; - const warning = `Squid ${printSquid(squid)} is being deploying. -You can not run deploys on the same squid in parallel`; + const warning = `Squid ${printSquid(squid)} is being deployed. You can not run deploys on the same squid in parallel.`; if (!interactive) { - this.error(warning); + this.error([warning, `Wait for the current deploy to finish and retry.`].join('\n')); } this.warn(warning); @@ -60,7 +59,9 @@ You can not run deploys on the same squid in parallel`; const warning = `The tag "${tag}" has already been assigned to ${printSquid(oldSquid)}.`; if (!interactive) { - this.error([warning, `Please do it explicitly ${using}`].join('\n')); + this.error( + [warning, `Please do it explicitly ${using}.`, `Example: sqd deploy . --allow-tag-reassign`].join('\n'), + ); } this.warn(warning);