1
0
Fork 0
mirror of https://github.com/CorentinTh/it-tools.git synced 2025-08-06 14:05:18 +02:00

feat(cli-command-editor): Added a new tool: CLI command editor

This commit is contained in:
Dhwanik Panchal 2025-06-28 13:20:23 +05:30
parent 07eea0f484
commit eeea527829
7 changed files with 335 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,15 @@
import { test, expect } 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');
});
test('', async ({ page }) => {
});
});

View file

@ -0,0 +1,142 @@
import { expect, describe, it } from 'vitest';
import { extractOptions, buildOptionsObject, sanitizeOption, buildEditedCommand, isOption } 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",
"-e-id-ckslc": "CLICKHOUSE_PASSWORD=nopassword",
"--name-id-nnnn": "clickhouse-server",
"--ulimit-id-uuuu": "nofile=3333:4444",
}, {
"-d-id-1dfsj": "",
"-p-id-fdsd": "4444:3333",
"-p-id-fddd": "3333:4444",
"-e-id-ckslc": "CLICKHOUSE_PASSWORD=nopassword",
"--name-id-nnnn": "clickhouse-server",
"--ulimit-id-uuuu": "nofile=3333:4444",
}, "docker run -d -p 18123:8123 -p 19000:9000 -e CLICKHOUSE_PASSWORD=changeme --name some-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server")
).toEqual("docker run -d -p 4444:3333 -p 3333:4444 -e CLICKHOUSE_PASSWORD=nopassword --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");
});
});
});

View file

@ -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[] {
/*
aws elb describe-load-balancers --load-balancer-name my-load-balancer
npm i forever -g
docker run -d -p 18123:8123 -p 19000:9000 -e CLICKHOUSE_PASSWORD=changeme --name some-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server
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<string, string> {
const optionsObject: Record<string, string> = {};
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<string, string>, originalOptions: Record<string, string>, 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<string, string>, currentValue: string[]) => {
previousValue[currentValue[0]] = currentValue[1]
return previousValue
}, originalOptions)
const defaultValues: Record<string, string> = {};
// 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;
}

View file

@ -0,0 +1,54 @@
<script setup lang="ts">
import * as service from "./cli-command-editor.service"
const inputCommand = ref("");
const options = computed(() => service.extractOptions(inputCommand.value));
const optionsObject = computed(() => service.buildOptionsObject(options.value));
const optionsInput = ref<{ [k: string]: string }>(optionsObject.value);
let command = ref("");
</script>
<template>
<c-card>
<n-grid x-gap="12" y-gap="12" cols="1 600:3">
<n-gi span="2">
<c-input-text
v-model:value="inputCommand"
label-position="left"
label-width="130px"
:label="$t('tools.cli-command-editor.command')"
:aria-label="$t('tools.cli-command-editor.command')"
:placeholder="$t('tools.cli-command-editor.placeholder')"
:aria-placeholder="$t('tools.cli-command-editor.placeholder')"
raw-text
@update:value="() => {command = inputCommand}"
/>
<div v-for="option in options" flex justify-center>
<c-input-text
v-model:value="optionsInput[option]"
label-position="left"
label-width="130px"
label-align="left"
:label="service.sanitizeOption(option)"
:aria-label="service.sanitizeOption(option)"
:placeholder="service.sanitizeOption(option)"
:aria-placeholder="service.sanitizeOption(option)"
@update:value="() => {command = service.buildEditedCommand(optionsInput, optionsObject, inputCommand)}"
mt-6
/>
</div>
<c-text-copyable
:value="command"
v-if="command"
font-mono
:show-icon="false"
mt-6
/>
</n-gi>
</n-grid>
</c-card>
</template>
<style lang="less" scoped>
</style>

View file

@ -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'),
});

View file

@ -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,
],
},
{