diff --git a/components.d.ts b/components.d.ts index 3e65c3cc..87f73674 100644 --- a/components.d.ts +++ b/components.d.ts @@ -42,6 +42,7 @@ declare module '@vue/runtime-core' { CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default'] CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] + CliCommandEditor: typeof import('./src/tools/cli-command-editor/cli-command-editor.vue')['default'] CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] CMarkdown: typeof import('./src/ui/c-markdown/c-markdown.vue')['default'] diff --git a/locales/de.yml b/locales/de.yml index 0ccdb71d..d37a1cf6 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -454,3 +454,10 @@ tools: text-to-binary: title: Text zu ASCII-Binär description: Konvertiere Text in seine ASCII-Binärrepräsentation und umgekehrt. + + cli-command-editor: + title: CLI-Befehlseditor + description: Wandeln Sie CLI-Befehle mit Optionen in eine leicht bearbeitbare Form um und generieren Sie den Befehl mit Eingabewerten. + command: Befehl + placeholder: Befehl hier einfügen + diff --git a/locales/en.yml b/locales/en.yml index d03d80d3..ca3362f6 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -392,3 +392,9 @@ tools: text-to-binary: title: Text to ASCII binary description: Convert text to its ASCII binary representation and vice-versa. + + cli-command-editor: + title: CLI command editor + description: Convert CLI commands with options into an easily editable form and generate the resulting command with input values. + command: Command + placeholder: Paste command here diff --git a/locales/es.yml b/locales/es.yml index 14e2bb66..aec440fa 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -70,3 +70,8 @@ tools: measurement: Measurement text: Text data: Data + cli-command-editor: + title: editor de comandos CLI + description: Convierta comandos CLI con opciones en un formato fácilmente editable y genere el comando resultante con valores de entrada. + command: Dominio + placeholder: Pegar comando aquí diff --git a/locales/fr.yml b/locales/fr.yml index 86bb47d6..8d83d350 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -80,3 +80,8 @@ tools: copied: Le token a été copié length: Longueur tokenPlaceholder: Le token... + cli-command-editor: + title: Éditeur de commandes CLI + description: Convertissez les commandes CLI avec des options dans un format facilement modifiable et générez la commande résultante avec des valeurs d'entrée. + command: Commande + placeholder: Coller la commande ici diff --git a/locales/no.yml b/locales/no.yml index ba4f9e47..9956682e 100644 --- a/locales/no.yml +++ b/locales/no.yml @@ -392,3 +392,9 @@ tools: text-to-binary: title: Tekst til ASCII binært description: Konverter tekst til sin ASCII binære representasjon og visa-versa. + + cli-command-editor: + title: CLI kommando editor + description: Konverter CLI-kommandoer med alternativer til et enkelt redigerbart format og generer den resulterende kommandoen med inndataverdier. + command: Kommando + placeholder: Lim inn kommando her diff --git a/locales/pt.yml b/locales/pt.yml index 5845eb2f..571f2598 100644 --- a/locales/pt.yml +++ b/locales/pt.yml @@ -70,3 +70,8 @@ tools: measurement: 'Medidas' text: 'Texto' data: 'Dados' + cli-command-editor: + title: Editor de comando CLI + description: Converta comandos CLI com opções em um formato facilmente editável e gere o comando resultante com valores de entrada. + command: Comando + placeholder: Cole o comando aqui diff --git a/locales/uk.yml b/locales/uk.yml index b0086226..400db440 100644 --- a/locales/uk.yml +++ b/locales/uk.yml @@ -70,3 +70,8 @@ tools: measurement: Вимірювання text: Текст data: Дані + cli-command-editor: + title: Редактор команд CLI + description: Перетворіть команди CLI з опціями у форму, яку легко редагувати, та згенеруйте результуючу команду з вхідними значеннями. + command: Команда + placeholder: Вставте команду сюди diff --git a/locales/vi.yml b/locales/vi.yml index 59514cd7..1a4af032 100644 --- a/locales/vi.yml +++ b/locales/vi.yml @@ -381,3 +381,9 @@ tools: text-to-binary: title: Chuyển đổi văn bản thành nhị phân ASCII description: Chuyển đổi văn bản thành biểu diễn nhị phân ASCII của nó và ngược lại. + + cli-command-editor: + title: Trình soạn thảo lệnh CLI + description: Chuyển đổi các lệnh CLI có tùy chọn thành dạng dễ chỉnh sửa và tạo lệnh kết quả với các giá trị đầu vào. + command: Yêu cầu + placeholder: Dán lệnh vào đây diff --git a/locales/zh.yml b/locales/zh.yml index 97968eb5..b5c2b380 100644 --- a/locales/zh.yml +++ b/locales/zh.yml @@ -388,3 +388,9 @@ tools: text-to-binary: title: 文本到 ASCII 二进制 description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。 + + cli-command-editor: + title: CLI 命令编辑器 + description: 将带有选项的 CLI 命令转换为易于编辑的形式,并生成带有输入值的结果命令。 + command: 命令 + placeholder: 将命令粘贴到此处 diff --git a/src/tools/cli-command-editor/cli-command-editor.e2e.spec.ts b/src/tools/cli-command-editor/cli-command-editor.e2e.spec.ts new file mode 100644 index 00000000..b80e444b --- /dev/null +++ b/src/tools/cli-command-editor/cli-command-editor.e2e.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - Cli command editor', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/cli-command-editor'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('Cli command editor - IT Tools'); + }); +}); diff --git a/src/tools/cli-command-editor/cli-command-editor.service.test.ts b/src/tools/cli-command-editor/cli-command-editor.service.test.ts new file mode 100644 index 00000000..9524564a --- /dev/null +++ b/src/tools/cli-command-editor/cli-command-editor.service.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from 'vitest'; +import { buildEditedCommand, buildOptionsObject, extractOptions, isOption, sanitizeOption } from './cli-command-editor.service'; + +describe('cli-command-editor', () => { + describe('extractOptions', () => { + it ('extracts all the options from a command', () => { + expect( + extractOptions('aws elb describe-load-balancers --load-balancer-name my-load-balancer')[0], + ).toContain('--load-balancer-name'); + + expect( + extractOptions('aws elb describe-load-balancers --load-balancer-name my-load-balancer --debug --query my-queryyy')[0], + ).toContain('--load-balancer-name'); + + expect( + extractOptions('aws elb describe-load-balancers --load-balancer-name my-load-balancer --debug --query my-queryyy')[1], + ).toContain('--debug'); + + expect( + extractOptions('aws elb describe-load-balancers --load-balancer-name my-load-balancer --debug --query my-queryyy')[2], + ).toContain('--query'); + }); + + it('extracts all the option from a command with a mix of hyphen and double hyphens', () => { + expect( + extractOptions('npm i lodash -g --legacy-peer-deps')[0], + ).toContain('-g'); + + expect( + extractOptions('npm i lodash -g --legacy-peer-deps')[1], + ).toContain('--legacy-peer-deps'); + }); + + it('shouldn\'t extract any options from a command without options', () => { + expect( + extractOptions('npm i lodash'), + ).toEqual([]); + }); + + it('shouldn\'t return any options if command is not passed', () => { + expect(extractOptions()).toEqual([]); + }); + }); + + describe('buildOptionsObject', () => { + it('returns a valid options object with the given options', () => { + expect( + buildOptionsObject(['--debug', '--load-balancer-names']), + ).toEqual({ + '--debug': '', + '--load-balancer-names': '', + }); + }); + + it('returns an empty obnject with blank options array', () => { + expect( + buildOptionsObject([]), + ).toEqual({}); + }); + }); + + describe('sanitizeOption', () => { + it('returns the sanitized option without `id` suffix', () => { + expect(sanitizeOption('--debug-id-1dfsj')) + .toEqual('--debug'); + }); + + it('returns the blank string', () => { + expect(sanitizeOption('')).toEqual(''); + }); + }); + + describe('isOption', () => { + it('returns true for a valid double hyphen option token', () => { + expect(isOption('--debug')).toBe(true); + }); + + it('returns true for a valid single hyphen option token', () => { + expect(isOption('-i')).toBe(true); + }); + + it('returns false for an non-option token', () => { + expect(isOption('hello-world')).toBe(false); + }); + }); + + describe('buildEditedCommand', () => { + it('returns the edited command', () => { + expect( + buildEditedCommand({ + '--debug-id-1dfsj': 'stdin', + '-p': '', + '-m': 'nahhhh', + }, { + '--debug-id-1dfsj': 'stdin', + '-p': '', + '-m': 'nahhhh', + }, 'aws node --debug stdio -p -m okayyy'), + ).toEqual('aws node --debug stdin -p -m nahhhh'); + + expect( + buildEditedCommand({ + '-d-id-1dfsj': '', + '-p-id-fdsd': '4444:3333', + '-p-id-fddd': '3333:4444', + '--name-id-nnnn': 'clickhouse-server', + '--ulimit-id-uuuu': 'nofile=3333:4444', + }, { + '-d-id-1dfsj': '', + '-p-id-fdsd': '4444:3333', + '-p-id-fddd': '3333:4444', + '--name-id-nnnn': 'clickhouse-server', + '--ulimit-id-uuuu': 'nofile=3333:4444', + }, 'docker run -d -p 18123:8123 -p 19000:9000 --name some-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server'), + ).toEqual('docker run -d -p 4444:3333 -p 3333:4444 --name clickhouse-server --ulimit nofile=3333:4444 clickhouse/clickhouse-server'); + }); + + it('returns the edited command when options object and CLI options order doesn\'t match', () => { + expect( + buildEditedCommand({ + '-d-id-t1dd3': 'true', + '--install-id-only123': 'nodemon', + }, { + '--install-id-only123': 'nodem', + '-d-id-t1dd3': 'false', + }, 'npm --install nodem -d false'), + ).toBe('npm --install nodemon -d true'); + }); + + it('returns the original command', () => { + expect( + buildEditedCommand({}, {}, 'npm install nodemon'), + ).toBe('npm install nodemon'); + + expect( + buildEditedCommand({}, {}, 'aws load-balancer describe-load-balancers all'), + ).toBe('aws load-balancer describe-load-balancers all'); + }); + }); +}); diff --git a/src/tools/cli-command-editor/cli-command-editor.service.ts b/src/tools/cli-command-editor/cli-command-editor.service.ts new file mode 100644 index 00000000..9793ac24 --- /dev/null +++ b/src/tools/cli-command-editor/cli-command-editor.service.ts @@ -0,0 +1,104 @@ +import { generateRandomId } from '@/utils/random'; + +export function isOption(token: string): boolean { + return token?.startsWith('--') || token?.startsWith('-'); +} + +export function extractOptions(command: string = ''): string[] { + /* + in a CLI, the options are either written with a hyphen or double hyphens, however, + script names or package/library sometimes include a hyphen, too, for example 'describe-load-balancers' + */ + + // split into tokens first + const tokens = command.split(' '); + + // map each token of the command to an option + const options = tokens.map((token: string) => { + // every option in a starts with either a hyphen or double hyphens + if (isOption(token)) { + const randomId = generateRandomId(); + return `${token}-${randomId}`; + } + + return ''; + }).filter((option: string): boolean => !!option); + return options; +} + +export function buildOptionsObject(options: string[]): Record { + const optionsObject: Record = {}; + + for (const option of options) { + optionsObject[option] = ''; + } + + return optionsObject; +} + +export function sanitizeOption(option: string): string { + return option.split('-id')?.[0]; +} + +export function buildEditedCommand(options: Record, originalOptions: Record, command: string): string { + if (!Object.keys(options).length) { + return command; + } + + const tokens = command.split(' '); + const editedTokens = []; + + // user may input the option value in any order, from the form + // preserve the original object with options in the correct + // order as they appear in the original command, this is done + // to handle the interpolation of edited option values into the + // command + originalOptions = Object.entries(options) + .reduce((previousValue: Record, currentValue: string[]) => { + previousValue[currentValue[0]] = currentValue[1]; + + return previousValue; + }, originalOptions); + + const defaultValues: Record = {}; + // replacing the options and their values (if any) with formatter ($i) to + // help in interpolation of the command + for (let i = 0, j = 0, n = tokens.length; i < n; ++i) { + const token = tokens[i]; + const nextToken = tokens[i + 1]; + + if (isOption(token)) { + editedTokens.push(`$${j}`); + + if (!isOption(nextToken)) { + ++i; + defaultValues[`$${j}`] = nextToken; + } + + ++j; + continue; + } + + editedTokens.push(token); + } + + let editedCommand = editedTokens.join(' '); + + const originalOptionKeys = Object.keys(originalOptions); + + for (let i = 0, n = originalOptionKeys.length; i < n; ++i) { + const key = originalOptionKeys[i]; + const keyWithoutIdSuffix = key.split('-id-')[0] || key; + + if (originalOptions[key]) { + editedCommand = editedCommand.replace(`$${i}`, `${keyWithoutIdSuffix} ${originalOptions[key]}`); + } + else { + const value = defaultValues[`$${i}`]; + const replaceValue = value ? `${keyWithoutIdSuffix} ${value}` : keyWithoutIdSuffix; + editedCommand = editedCommand.replace(`$${i}`, replaceValue); + } + } + + return editedCommand; +} diff --git a/src/tools/cli-command-editor/cli-command-editor.vue b/src/tools/cli-command-editor/cli-command-editor.vue new file mode 100644 index 00000000..8ed0b2c2 --- /dev/null +++ b/src/tools/cli-command-editor/cli-command-editor.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/tools/cli-command-editor/index.ts b/src/tools/cli-command-editor/index.ts new file mode 100644 index 00000000..32d6cdb9 --- /dev/null +++ b/src/tools/cli-command-editor/index.ts @@ -0,0 +1,12 @@ +import { Terminal2 } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'CLI command editor', + path: '/cli-command-editor', + description: '', + keywords: ['cli', 'command', 'editor'], + component: () => import('./cli-command-editor.vue'), + icon: Terminal2, + createdAt: new Date('2025-06-21'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..de28c2ed 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as cliCommandEditor } from './cli-command-editor'; import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -160,6 +161,7 @@ export const toolsByCategory: ToolCategory[] = [ emailNormalizer, regexTester, regexMemo, + cliCommandEditor, ], }, {