@jtaco/customcmd 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # CustomCMD
2
+ An easy to use custom terminal command creator that can run multiple steps with a single command.
3
+
4
+ ## Installation
5
+
6
+ **Via npm:**
7
+
8
+ npm install -g customcmd
9
+
10
+ **Via git:**
11
+
12
+ git clone https://github.com/J-Taco/custom-command.git
13
+ cd custom-command
14
+ npm link
15
+
16
+ **Verify install:**
17
+
18
+ customcmd --version
19
+
20
+ ## Usage
21
+ CustomCMD lets you define commands that run one or more terminal commands in sequence. Once defined, run them by name as if they were built-in terminal commands.
22
+
23
+ customcmd define my-command --description "My first command" --cmd "echo Hello, World!"
24
+ customcmd my-command
25
+
26
+ ## Commands
27
+
28
+ | Command | Description |
29
+ |---|---|
30
+ | `customcmd define <name> [options]` | Define a new command |
31
+ | `customcmd run <name> [args]` | Run a command (name alone also works) |
32
+ | `customcmd list` | List all defined commands |
33
+ | `customcmd info <name>` | Show details of a command |
34
+ | `customcmd edit <name> [options]` | Update an existing command |
35
+ | `customcmd remove <name>` | Delete a command |
36
+ | `customcmd ui` | Launch the web UI |
37
+
38
+ ### Define & Edit Options
39
+
40
+ | Option | Description |
41
+ |---|---|
42
+ | `--description <text>` | A description of what the command does |
43
+ | `--cmd <command>` | A step to run (repeat for multiple steps) |
44
+ | `--arg <arg>` | An argument definition (repeat for multiple args) |
45
+
46
+ > **Note:** `edit` only updates the fields you pass — everything else stays the same. If you pass a --cmd or --arg it will overwrite all of them.
47
+
48
+ ## Argument Types
49
+
50
+ CustomCMD supports three types of arguments used as placeholders in your commands with `${arg-name}` syntax.
51
+
52
+ ### Positional
53
+ Required arguments passed by position.
54
+
55
+ customcmd define greet --arg "name:positional" --cmd "echo Hello ${name}!"
56
+ customcmd greet Joshua
57
+ # -> Hello Joshua!
58
+
59
+ ### Named
60
+ Optional arguments with a default value, passed with --arg-name value.
61
+
62
+ customcmd define greet --arg "name:named:World" --cmd "echo Hello ${name}!"
63
+ customcmd greet # -> Hello World!
64
+ customcmd greet --name Joshua # -> Hello Joshua!
65
+
66
+ ### Flag
67
+ Boolean arguments — true if passed, false if not.
68
+
69
+ customcmd define build --arg "verbose:flag" --cmd "echo Building... ${verbose}"
70
+ customcmd build # -> Building... false
71
+ customcmd build --verbose # -> Building... true
72
+
73
+ ## Examples
74
+
75
+ **Create a new Flutter project:**
76
+
77
+ customcmd define new-flutter --description "Creates a new Flutter project and opens VS Code" --arg "identifier:positional" --cmd "flutter create ${identifier}" --cmd "code ${identifier}"
78
+
79
+ customcmd new-flutter my_app
80
+
81
+ **Git add, commit and push in one command:**
82
+
83
+ customcmd define push --description "Stages, commits and pushes changes" --arg "message:positional" --cmd "git add ." --cmd "git commit -m ${message}" --cmd "git push"
84
+
85
+ customcmd push "My commit message"
86
+
87
+ ## Web UI
88
+ CustomCMD includes a local web interface for managing your commands visually.
89
+
90
+ customcmd ui
91
+ # -> Opens http://localhost:4242 in your browser
92
+ # -> Press q to quit
93
+
94
+ ## License
95
+ ISC
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { runCommand } from '../src/commands/run.js';
4
+
5
+ program.name('customcmd').description('An easy to use custom terminal command creator that can do multiple steps with one command').version('1.1.0');
6
+
7
+ import '../src/commands/define.js';
8
+ import '../src/commands/list.js';
9
+ import '../src/commands/remove.js';
10
+ import '../src/commands/run.js';
11
+ import '../src/commands/info.js';
12
+ import '../src/commands/ui.js';
13
+ import '../src/commands/edit.js';
14
+
15
+ program
16
+ .arguments('<name> [userInputs...]')
17
+ .allowUnknownOption()
18
+ .action(async (name, userInputs) => {
19
+ await runCommand(name, userInputs);
20
+ });
21
+
22
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@jtaco/customcmd",
3
+ "version": "1.1.0",
4
+ "description": "An easy to use custom terminal command creator that can do multiple steps with one command",
5
+ "license": "ISC",
6
+ "author": "JTaco",
7
+ "type": "module",
8
+ "bin": {
9
+ "customcmd": "./bin/customcmd.js",
10
+ "custcmd": "./bin/customcmd.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/"
15
+ ],
16
+ "scripts": {
17
+ "test": "npm -watch bin/index.js"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^12.0.0",
21
+ "express": "^4.18.0",
22
+ "open": "^10.2.0"
23
+ }
24
+ }
@@ -0,0 +1,43 @@
1
+ import { program } from 'commander';
2
+ import { save } from "../core/store.js";
3
+
4
+ program
5
+ .command('define <name>')
6
+ .description('Define a custom command')
7
+ .option('--description <description>', 'Command description')
8
+ .option('--cmd <cmd...>', 'Command(s) to save')
9
+ .option('--arg <arg...>', 'Argument(s) to save')
10
+ .action(async (name, options) => {
11
+ const object = {
12
+ description: options.description ?? '',
13
+ commands: options.cmd ?? [],
14
+ arguments: (options.arg ?? []).map(arg => argumentFormat(arg)),
15
+ };
16
+
17
+ const saved = await save(name, object);
18
+
19
+ if (saved == 'exists') {
20
+ console.error(`Error: ${name} is already a registered command! `);
21
+ } else if (saved == 'success') {
22
+ console.log(`Command "${name}" saved successfully.`);
23
+ } else {
24
+ console.error(`An unknown error occured when creating ${name}. Try again. `);
25
+ }
26
+ });
27
+
28
+ export function argumentFormat(argString) {
29
+ const parts = argString.split(':');
30
+
31
+ if (parts[1] == 'named') {
32
+ return {
33
+ name: parts[0],
34
+ type: parts[1],
35
+ default: parts[2],
36
+ };
37
+ } else {
38
+ return {
39
+ name: parts[0],
40
+ type: parts[1],
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ import { program } from 'commander';
2
+ import { update } from '../core/store.js';
3
+ import { argumentFormat } from './define.js';
4
+
5
+ program
6
+ .command('edit <name>')
7
+ .description('Update an already existing command')
8
+ .option('--description <description>', 'Command description')
9
+ .option('--cmd <cmd...>', 'Command(s) to save')
10
+ .option('--arg <arg...>', 'Argument(s) to save')
11
+ .action(async (name, options) => {
12
+ const object = {};
13
+
14
+ if (options.description !== undefined) object.description = options.description;
15
+ if (options.cmd !== undefined) object.commands = options.cmd;
16
+ if (options.arg !== undefined) object.arguments = options.arg.map(arg => argumentFormat(arg));
17
+
18
+ const saved = await update(name, object);
19
+
20
+ if (saved == 'not-exists') {
21
+ console.error(`Error: ${name} does not exist. Create a command with customcmd define <name>`);
22
+ } else if (saved == 'success') {
23
+ console.log(`Command ${name} updated successfully.`);
24
+ } else {
25
+ console.error(`An unknown error occured when updating ${name}. Try again. `)
26
+ }
27
+ });
@@ -0,0 +1,25 @@
1
+ import { program } from "commander";
2
+ import { getOne } from "../core/store.js";
3
+
4
+ program
5
+ .command('info <name>')
6
+ .description('Get all the info for one command')
7
+ .action((name) => {
8
+ const data = getOne(name);
9
+ const longestLength = Math.max(...data.arguments.map(arg => arg.name.length));
10
+
11
+ console.log(`Command: ${name}`);
12
+ console.log(`Description: ${data.description}`);
13
+ console.log('\nArguments:')
14
+ data.arguments.forEach(arg => {
15
+ if (arg.type == 'named') {
16
+ console.log(`- ${arg.name.padEnd(longestLength)} (${arg.type}, default: ${arg.default})`);
17
+ } else {
18
+ console.log(`- ${arg.name.padEnd(longestLength)} (${arg.type})`);
19
+ }
20
+ });
21
+ console.log('\nSteps:')
22
+ for (let i = 0; i < data.commands.length; i++) {
23
+ console.log(`${i + 1}. ${data.commands[i]}`);
24
+ }
25
+ });
@@ -0,0 +1,19 @@
1
+ import { program } from "commander";
2
+ import { getAll } from "../core/store.js";
3
+
4
+ program
5
+ .command('list')
6
+ .description('Lists all custom commands')
7
+ .action(() => {
8
+ const data = getAll();
9
+ const keys = Object.keys(data);
10
+
11
+ if (keys.length == 0) {
12
+ console.log('No defined commands.');
13
+ } else {
14
+ keys.forEach(key => {
15
+ const cmd = data[key];
16
+ console.log(`${key} - ${cmd.description}`);
17
+ });
18
+ }
19
+ });
@@ -0,0 +1,10 @@
1
+ import { program } from "commander";
2
+ import { remove } from "../core/store.js";
3
+
4
+ program
5
+ .command('remove <name>')
6
+ .description('Remove a custom command')
7
+ .action(async (name) => {
8
+ await remove(name);
9
+ console.log(`Command ${name} was removed. `)
10
+ });
@@ -0,0 +1,40 @@
1
+ import { program } from "commander";
2
+ import { getOne } from "../core/store.js";
3
+ import { execute } from "../core/executor.js";
4
+
5
+ program
6
+ .command('run <name> [userInputs...]')
7
+ .allowUnknownOption()
8
+ .action(async (name, userInputs) => {
9
+ await runCommand(name, userInputs);
10
+ });
11
+
12
+ export function runCommand(name, userInputs) {
13
+ const commandDef = getOne(name);
14
+
15
+ if (commandDef == null || commandDef == undefined) {
16
+ console.log(`Could not find a command named ${name}. Try defining a new command: `);
17
+ console.log('customcmd define <name> --description <cool description> --cmd <commands to run in order. Can do multiple --cmd> --arg <arguments for your command. Can do multiple --arg>');
18
+ return;
19
+ }
20
+
21
+ const namedArgs = {};
22
+ const positionalArgs = [];
23
+
24
+ for (let i = 0; i < userInputs.length; i++) {
25
+ if (userInputs[i].startsWith('--')) {
26
+ const argName = userInputs[i].slice(2);
27
+
28
+ if (userInputs[i + 1] != undefined && !userInputs[i + 1].startsWith('--')) {
29
+ namedArgs[argName] = userInputs[i + 1]
30
+ i++;
31
+ } else {
32
+ namedArgs[argName] = true;
33
+ }
34
+ } else {
35
+ positionalArgs.push(userInputs[i]);
36
+ }
37
+ }
38
+
39
+ execute(commandDef, positionalArgs, namedArgs);
40
+ }
@@ -0,0 +1,21 @@
1
+ import { program } from "commander";
2
+ import open from "open";
3
+
4
+ program
5
+ .command('ui')
6
+ .description('Start the web UI for easy configuration')
7
+ .action(async () => {
8
+ await import('../server/server.js');
9
+ await open("http://localhost:4242");
10
+
11
+ process.stdin.setRawMode(true);
12
+ process.stdin.resume();
13
+ process.stdin.setEncoding('utf8');
14
+
15
+ process.stdin.on('data', (key) => {
16
+ if (key === 'q' || key === '\u0003') {
17
+ console.log('Closing web UI. ');
18
+ process.exit();
19
+ }
20
+ });
21
+ });
@@ -0,0 +1,24 @@
1
+ import { execSync } from 'child_process';
2
+ import { resolve } from './resolver.js';
3
+
4
+ export function replacePlaceholders(commandString, mapping) {
5
+ Object.keys(mapping).forEach(key => {
6
+ commandString = commandString.replaceAll(`\${${key}}`, mapping[key]);
7
+ });
8
+
9
+ return commandString;
10
+ }
11
+
12
+ export function execute(commandDef, positionalArgs, namedArgs) {
13
+ const mapping = resolve(commandDef.arguments, positionalArgs, namedArgs);
14
+
15
+ if (mapping == undefined) {
16
+ console.log("Command failed to run. ");
17
+ return;
18
+ }
19
+
20
+ commandDef.commands.forEach(commandString => {
21
+ const resolvedCommand = replacePlaceholders(commandString, mapping);
22
+ execSync(resolvedCommand, { stdio: 'inherit', shell: true });
23
+ });
24
+ }
@@ -0,0 +1,37 @@
1
+ export function resolve(argumentDefs, positionalArgs, namedArgs) {
2
+ let positionalArgCounter = 0;
3
+ for (let i = 0; i < argumentDefs.length; i++) {
4
+ if (argumentDefs[i].type == 'positional') {
5
+ positionalArgCounter++;
6
+ }
7
+ }
8
+
9
+ if (positionalArgCounter != positionalArgs.length) {
10
+ console.error(`Incorrect number of positional arguments!\nExpected ${positionalArgCounter} but got ${positionalArgs.length}!`);
11
+ return;
12
+ }
13
+
14
+ const mapping = {};
15
+ let positionalIndex = 0;
16
+
17
+ for (let i = 0; i < argumentDefs.length; i++) {
18
+ if (argumentDefs[i].type == 'positional') {
19
+ mapping[argumentDefs[i].name] = positionalArgs[positionalIndex];
20
+ positionalIndex++;
21
+ } else if (argumentDefs[i].type == 'named') {
22
+ if (argumentDefs[i].name in namedArgs) {
23
+ mapping[argumentDefs[i].name] = namedArgs[argumentDefs[i].name];
24
+ } else {
25
+ mapping[argumentDefs[i].name] = argumentDefs[i].default;
26
+ }
27
+ } else if (argumentDefs[i].type == 'flag') {
28
+ if (argumentDefs[i].name in namedArgs) {
29
+ mapping[argumentDefs[i].name] = true;
30
+ } else {
31
+ mapping[argumentDefs[i].name] = false;
32
+ }
33
+ }
34
+ }
35
+
36
+ return mapping;
37
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ const dirPath = path.join(os.homedir(), ".customcmd");
6
+ const dataPath = path.join(dirPath, "commands.json");
7
+
8
+ if (!fs.existsSync(dataPath)) {
9
+ fs.mkdirSync(dirPath, { recursive: true });
10
+ fs.writeFileSync(dataPath, "{}");
11
+ }
12
+
13
+ export function getAll() {
14
+ try {
15
+ const rawData = fs.readFileSync(dataPath, "utf8");
16
+ return JSON.parse(rawData);
17
+ } catch (e) {
18
+ return e;
19
+ }
20
+ }
21
+
22
+ export function getOne(name) {
23
+ const all = getAll();
24
+ return all[name];
25
+ }
26
+
27
+ export function save(name, command) {
28
+ const existing = getOne(name);
29
+ if (existing == null || existing == undefined) {
30
+ try {
31
+ let commandsJson = getAll();
32
+ commandsJson[name] = command;
33
+ fs.writeFileSync(dataPath, JSON.stringify(commandsJson));
34
+ return "success";
35
+ } catch (e) {
36
+ return 'failed';
37
+ }
38
+ } else {
39
+ return 'exists';
40
+ }
41
+ }
42
+
43
+ export function update(name, command) {
44
+ const exists = getOne(name);
45
+ if (exists == null || exists == undefined) {
46
+ return 'not-exist';
47
+ } else {
48
+ try {
49
+ let commandsJson = getAll();
50
+ const updated = { ...exists, ...command };
51
+ commandsJson[name] = updated;
52
+ fs.writeFileSync(dataPath, JSON.stringify(commandsJson));
53
+ return 'success';
54
+ } catch (e) {
55
+ return 'failed';
56
+ }
57
+ }
58
+ }
59
+
60
+ export function remove(name) {
61
+ try {
62
+ let commandsJson = getAll();
63
+ delete commandsJson[name];
64
+ fs.writeFileSync(dataPath, JSON.stringify(commandsJson));
65
+ } catch (e) {
66
+ return e;
67
+ }
68
+ }
@@ -0,0 +1,73 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Custom Command Control</title>
8
+ <link rel="stylesheet" href="styles.css">
9
+ </head>
10
+
11
+ <body>
12
+ <header>
13
+ <h1>CustomCMD</h1>
14
+ <button id="add-cmd">Add Command</button>
15
+ </header>
16
+
17
+ <div id="commands-grid"></div>
18
+
19
+ <div id="info-modal" class="modal-overlay">
20
+ <div class="modal">
21
+ <div class="modal-header">
22
+ <h2 id="modal-name"></h2>
23
+ <button id="modal-close">x</button>
24
+ </div>
25
+
26
+ <p id="modal-description"></p>
27
+
28
+ <h3>Arguments</h3>
29
+ <div id="modal-arguments"></div>
30
+
31
+ <h3>Steps</h3>
32
+ <div id="modal-steps"></div>
33
+
34
+ <div id="modal-buttons">
35
+ <button id="modal-edit">Edit Command</button>
36
+ <button id="modal-delete">Delete Command</button>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="add-modal" class="modal-overlay">
42
+ <div class="modal">
43
+ <div class="modal-header">
44
+ <h2>Add Command</h2>
45
+ <button id="add-modal-close">x</button>
46
+ </div>
47
+
48
+ <label>Name</label>
49
+ <input id="add-name" type="text" placeholder="e.g. new-flutter" />
50
+
51
+ <label>Description</label>
52
+ <input id="add-description" type="text" placeholder="What does this command do?" />
53
+
54
+ <div class="modal-section-header">
55
+ <h3>Steps</h3>
56
+ <button id="add-step-btn">+ Add Step</button>
57
+ </div>
58
+ <div id="add-steps"></div>
59
+
60
+ <div class="modal-section-header">
61
+ <h3>Arguments</h3>
62
+ <button id="add-arg-btn">+ Add Argument</button>
63
+ </div>
64
+ <div id="add-arguments"></div>
65
+
66
+ <button id="add-submit">Save Command</button>
67
+ </div>
68
+ </div>
69
+
70
+ <script src="index.js"></script>
71
+ </body>
72
+
73
+ </html>
@@ -0,0 +1,255 @@
1
+ const modal = document.getElementById('info-modal');
2
+ const addModal = document.getElementById('add-modal');
3
+ let editingCommand = null;
4
+
5
+ function renderCommands(commands) {
6
+ const grid = document.getElementById("commands-grid");
7
+ grid.innerHTML = '';
8
+
9
+ if (Object.keys(commands).length == 0) {
10
+ grid.innerHTML = "<h1>You have no commands.</h1>";
11
+ return;
12
+ }
13
+
14
+ Object.keys(commands).forEach(key => {
15
+ const command = commands[key];
16
+
17
+ const card = document.createElement('div');
18
+ card.className = 'command-card';
19
+ card.innerHTML = `
20
+ <h2>${key}</h2>
21
+ <p>${command.description}</p>
22
+ `;
23
+
24
+ card.addEventListener('click', () => {
25
+ document.getElementById('modal-name').textContent = key;
26
+ document.getElementById('modal-description').textContent = command.description;
27
+
28
+ const argsDiv = document.getElementById('modal-arguments');
29
+ argsDiv.innerHTML = '';
30
+
31
+ command.arguments.forEach((arg) => {
32
+ if (arg.type == 'named') {
33
+ argsDiv.innerHTML += `<p>- ${arg.name} (${arg.type}, default: ${arg.default})</p> `;
34
+ } else {
35
+ argsDiv.innerHTML += `<p>- ${arg.name} (${arg.type})</p> `;
36
+ }
37
+ });
38
+
39
+ const stepsDiv = document.getElementById('modal-steps');
40
+ stepsDiv.innerHTML = '';
41
+
42
+ command.commands.forEach((step, index) => {
43
+ stepsDiv.innerHTML += `<p> ${index + 1}. ${step}</p> `;
44
+ });
45
+
46
+ document.getElementById('modal-edit').onclick = async () => {
47
+ editingCommand = key;
48
+
49
+ document.getElementById('add-name').value = key;
50
+ document.getElementById('add-description').value = command.description;
51
+
52
+ document.getElementById('add-steps').innerHTML = '';
53
+ document.getElementById('add-arguments').innerHTML = '';
54
+
55
+ command.commands.forEach(step => {
56
+ const stepsDiv = document.getElementById('add-steps');
57
+ const input = document.createElement('input');
58
+ input.type = 'text';
59
+ input.placeholder = 'e.g. flutter create ${bundle-id}';
60
+ input.className = 'step-input';
61
+ input.value = step;
62
+ stepsDiv.appendChild(input);
63
+ });
64
+
65
+ command.arguments.forEach(arg => {
66
+ const argsDiv = document.getElementById('add-arguments');
67
+
68
+ const row = document.createElement('div');
69
+ row.className = 'arg-row';
70
+
71
+ const nameInput = document.createElement('input');
72
+ nameInput.type = 'text';
73
+ nameInput.className = 'arg-name';
74
+ nameInput.value = arg.name;
75
+
76
+ const select = document.createElement('select');
77
+ select.className = 'arg-type';
78
+ select.innerHTML = `
79
+ <option value="positional">positional</option>
80
+ <option value="named">named</option>
81
+ <option value="flag">flag</option>`;
82
+ select.value = arg.type;
83
+
84
+ const defaultInput = document.createElement('input');
85
+ defaultInput.type = 'text';
86
+ defaultInput.placeholder = 'default value';
87
+ defaultInput.className = 'arg-default';
88
+ defaultInput.value = arg.default ?? '';
89
+
90
+ row.append(nameInput);
91
+ row.append(select);
92
+ row.append(defaultInput);
93
+ argsDiv.append(row);
94
+ })
95
+
96
+ document.querySelector('#add-modal .modal-header h2').textContent = 'Edit Command';
97
+ document.getElementById('add-name').disabled = true;
98
+
99
+ modal.classList.remove('active');
100
+ addModal.classList.add('active');
101
+ }
102
+
103
+ document.getElementById('modal-delete').onclick = async () => {
104
+ if (confirm('Are you sure you want to delete this command? It CANNOT be undone. ')) {
105
+ await fetch(`/api/commands/${key}`, { method: 'DELETE' });
106
+ modal.classList.remove('active');
107
+ await renderCommands(await getCommands());
108
+ }
109
+ };
110
+
111
+ document.getElementById('info-modal').classList.add('active');
112
+ });
113
+
114
+ grid.appendChild(card);
115
+ });
116
+ }
117
+
118
+ async function getCommands() {
119
+ try {
120
+ const res = await fetch("/api/commands");
121
+ const commands = await res.json();
122
+ return commands;
123
+ } catch (error) {
124
+ console.error("Failed to fetch commands. ");
125
+ console.error(error);
126
+ }
127
+ }
128
+
129
+ document.addEventListener("DOMContentLoaded", async () => {
130
+ await renderCommands(await getCommands());
131
+
132
+ const closeBtn = document.getElementById('modal-close');
133
+
134
+ closeBtn.addEventListener('click', () => {
135
+ modal.classList.remove('active');
136
+ });
137
+
138
+ modal.addEventListener('click', (e) => {
139
+ if (e.target == modal) {
140
+ modal.classList.remove('active');
141
+ }
142
+ });
143
+
144
+ const addCmdBtn = document.getElementById('add-cmd');
145
+ const addModalClose = document.getElementById('add-modal-close');
146
+
147
+ addCmdBtn.addEventListener('click', () => {
148
+ addModal.classList.add('active');
149
+ });
150
+
151
+ addModalClose.addEventListener('click', () => {
152
+ addModal.classList.remove('active');
153
+ document.querySelector('#add-modal .modal-header h2').textContent = "Add Command";
154
+ document.getElementById('add-name').disabled = false;
155
+ editingCommand = null;
156
+ });
157
+
158
+ document.getElementById('add-step-btn').addEventListener('click', () => {
159
+ const stepsDiv = document.getElementById('add-steps');
160
+
161
+ const input = document.createElement('input');
162
+ input.type = 'text';
163
+ input.placeholder = 'e.g. flutter create ${bundle-id}';
164
+ input.className = 'step-input';
165
+
166
+ stepsDiv.appendChild(input);
167
+ });
168
+
169
+ document.getElementById('add-arg-btn').addEventListener('click', () => {
170
+ const argsDiv = document.getElementById('add-arguments');
171
+
172
+ const row = document.createElement('div');
173
+ row.className = 'arg-row';
174
+
175
+ const nameInput = document.createElement('input');
176
+ nameInput.type = 'text';
177
+ nameInput.placeholder = 'arg name';
178
+ nameInput.className = 'arg-name';
179
+
180
+ const select = document.createElement('select');
181
+ select.className = 'arg-type';
182
+ select.innerHTML = `<option value="positional">positional</option>
183
+ <option value="named">named</option>
184
+ <option value="flag">flag</option>`;
185
+
186
+ const defaultInput = document.createElement('input');
187
+ defaultInput.type = 'text';
188
+ defaultInput.placeholder = 'default value';
189
+ defaultInput.className = 'arg-default';
190
+
191
+ row.append(nameInput);
192
+ row.append(select);
193
+ row.append(defaultInput);
194
+ argsDiv.appendChild(row);
195
+ });
196
+
197
+ document.getElementById('add-submit').addEventListener('click', async () => {
198
+ const name = document.getElementById('add-name').value;
199
+ if (!name) {
200
+ alert('Please enter a command name');
201
+ return;
202
+ }
203
+ const stepInputs = document.querySelectorAll('.step-input');
204
+ if (stepInputs.length == 0) {
205
+ alert('Please add at least one step');
206
+ return;
207
+ }
208
+ document.getElementById('add-name').value = '';
209
+ const description = document.getElementById('add-description').value;
210
+ document.getElementById('add-description').value = '';
211
+
212
+ const commands = Array.from(stepInputs).map(input => input.value);
213
+ document.getElementById('add-steps').innerHTML = '';
214
+
215
+ const argRows = document.querySelectorAll('.arg-row');
216
+ const args = Array.from(argRows).map(row => {
217
+ return {
218
+ name: row.querySelector('.arg-name').value,
219
+ type: row.querySelector('.arg-type').value,
220
+ default: row.querySelector('.arg-default').value
221
+ }
222
+ });
223
+ document.getElementById('add-arguments').innerHTML = '';
224
+
225
+ if (editingCommand) {
226
+ await fetch(`/api/commands/${editingCommand}`, {
227
+ method: 'PUT',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify({ name, description, commands, arguments: args })
230
+ });
231
+ editingCommand = null;
232
+ } else {
233
+ await fetch('/api/commands', {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify({ name, description, commands, arguments: args })
237
+ });
238
+ }
239
+
240
+ addModal.classList.remove('active');
241
+ document.querySelector('#add-modal .modal-header h2').textContent = "Add Command";
242
+ document.getElementById('add-name').disabled = false;
243
+ await renderCommands(await getCommands());
244
+ });
245
+
246
+ document.addEventListener('keydown', (e) => {
247
+ if (e.key == 'Escape') {
248
+ modal.classList.remove('active');
249
+ addModal.classList.remove('active');
250
+ document.querySelector('#add-modal .modal-header h2').textContent = "Add Command";
251
+ document.getElementById('add-name').disabled = false;
252
+ editingCommand = null;
253
+ }
254
+ });
255
+ });
@@ -0,0 +1,262 @@
1
+ body {
2
+ --bg: #0d1117;
3
+ --surface: #161b27;
4
+ --card-bg: #1c2333;
5
+ --card-hover: #222940;
6
+ --accent: #3665b6;
7
+ --foreground: #e4dfdf;
8
+ --subtle: #8b949e;
9
+ --border: #ffffff15;
10
+
11
+ margin: 0;
12
+ padding: 0;
13
+ background-color: var(--bg);
14
+ color: var(--foreground);
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segeo UI', sans-serif;
16
+ }
17
+
18
+ header {
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ padding: 20px 32px;
23
+ border-bottom: 1px solid var(--border);
24
+ background-color: var(--surface);
25
+ }
26
+
27
+ header h1 {
28
+ margin: 0;
29
+ font-size: 20px;
30
+ font-weight: 600;
31
+ letter-spacing: 0.5px;
32
+ }
33
+
34
+ header button {
35
+ background-color: var(--accent);
36
+ color: white;
37
+ border: none;
38
+ border-radius: 8px;
39
+ padding: 8px 18px;
40
+ cursor: pointer;
41
+ font-size: 14px;
42
+ font-weight: 500;
43
+ transition: all 0.2s ease;
44
+ }
45
+
46
+ header button:hover {
47
+ opacity: 0.75;
48
+ border-radius: 12px;
49
+ }
50
+
51
+ #commands-grid {
52
+ padding: 32px;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 10px;
56
+ }
57
+
58
+ #commands-grid h1 {
59
+ align-self: center;
60
+ justify-self: center;
61
+ text-align: center;
62
+ font-size: medium;
63
+ opacity: 0.6;
64
+ padding-top: 4%;
65
+ }
66
+
67
+ .command-card {
68
+ background-color: var(--card-bg);
69
+ border: 1px solid var(--border);
70
+ border-radius: 10px;
71
+ padding: 16px 20px;
72
+ cursor: pointer;
73
+ transition: all 0.2s ease;
74
+ }
75
+
76
+ .command-card:hover {
77
+ background-color: var(--card-hover);
78
+ }
79
+
80
+ .command-card h2 {
81
+ margin: 0 0 6px 0;
82
+ font-size: 20px;
83
+ font-weight: 600;
84
+ }
85
+
86
+ .command-card p {
87
+ margin: 0;
88
+ font-size: 14px;
89
+ color: var(--subtle);
90
+ }
91
+
92
+ .modal-overlay {
93
+ display: none;
94
+ position: fixed;
95
+ top: 0;
96
+ left: 0;
97
+ width: 100%;
98
+ height: 100%;
99
+ background-color: #00000080;
100
+ justify-content: center;
101
+ align-items: center;
102
+ z-index: 100;
103
+ }
104
+
105
+ .modal-overlay.active {
106
+ display: flex;
107
+ }
108
+
109
+ .modal {
110
+ background-color: var(--surface);
111
+ border: 1px solid var(--border);
112
+ border-radius: 12px;
113
+ padding: 24px;
114
+ width: 500px;
115
+ max-width: 90%;
116
+ max-height: 80vh;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ .modal-header {
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ margin-bottom: 16px;
125
+ }
126
+
127
+ .modal-header h2 {
128
+ margin: 0;
129
+ }
130
+
131
+ .modal-header button {
132
+ background: none;
133
+ border: none;
134
+ color: var(--subtle);
135
+ font-size: 22px;
136
+ cursor: pointer;
137
+ }
138
+
139
+ .modal h3 {
140
+ padding-top: 4px;
141
+ }
142
+
143
+ #modal-buttons {
144
+ display: flex;
145
+ }
146
+
147
+ #modal-delete {
148
+ margin-top: 24px;
149
+ background-color: #c0392b;
150
+ color: white;
151
+ border: none;
152
+ border-radius: 8px;
153
+ padding: 8px 18px;
154
+ cursor: pointer;
155
+ font-size: 14px;
156
+ transition: all 0.2s ease;
157
+ }
158
+
159
+ #modal-delete:hover {
160
+ opacity: 0.75;
161
+ }
162
+
163
+ #modal-edit {
164
+ margin-top: 24px;
165
+ margin-right: 12px;
166
+ background-color: var(--accent);
167
+ color: white;
168
+ border: none;
169
+ border-radius: 8px;
170
+ padding: 8px 18px;
171
+ cursor: pointer;
172
+ font-size: 14px;
173
+ transition: all 0.2s ease;
174
+ }
175
+
176
+ #modal-edit:hover {
177
+ opacity: 0.75;
178
+ }
179
+
180
+ .modal input {
181
+ width: 100%;
182
+ background-color: var(--card-bg);
183
+ border: 1px solid var(--border);
184
+ border-radius: 8px;
185
+ padding: 8px 12px;
186
+ color: var(--foreground);
187
+ font-size: 14px;
188
+ margin-bottom: 16px;
189
+ box-sizing: border-box;
190
+ }
191
+
192
+ .modal input:focus {
193
+ outline: none;
194
+ border-color: var(--accent);
195
+ }
196
+
197
+ .modal label {
198
+ display: block;
199
+ font-size: 13px;
200
+ color: var(--subtle);
201
+ margin-bottom: 6px;
202
+ }
203
+
204
+ .modal-section-header {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ align-items: center;
208
+ margin-bottom: 8px;
209
+ }
210
+
211
+ .modal-section-header h3 {
212
+ margin: 0;
213
+ }
214
+
215
+ .modal-section-header button {
216
+ background: none;
217
+ border: 1px solid var(--border);
218
+ color: var(--foreground);
219
+ border-radius: 6px;
220
+ padding: 4px 10px;
221
+ cursor: pointer;
222
+ font-size: 13px;
223
+ }
224
+
225
+ #add-submit {
226
+ margin-top: 24px;
227
+ background-color: var(--accent);
228
+ color: white;
229
+ border: none;
230
+ border-radius: 8px;
231
+ padding: 8px 18px;
232
+ cursor: pointer;
233
+ font-size: 14px;
234
+ width: 100%;
235
+ transition: all 0.2s ease;
236
+ }
237
+
238
+ #add-submit:hover {
239
+ opacity: 0.75;
240
+ }
241
+
242
+ .arg-row {
243
+ display: flex;
244
+ gap: 8px;
245
+ margin-bottom: 8px;
246
+ }
247
+
248
+ .modal select {
249
+ background-color: var(--card-bg);
250
+ border: 1px solid var(--border);
251
+ border-radius: 8px;
252
+ padding: 8px 12px;
253
+ color: var(--foreground);
254
+ font-size: 14px;
255
+ cursor: pointer;
256
+ height: min-content;
257
+ }
258
+
259
+ .modal select:focus {
260
+ outline: none;
261
+ border-color: var(--accent);
262
+ }
@@ -0,0 +1,30 @@
1
+ import { Router } from "express";
2
+ import { getAll, save, remove, update, getOne } from '../../core/store.js';
3
+
4
+ const router = Router();
5
+
6
+ // Return all commands
7
+ router.get('/commands', (req, res) => {
8
+ res.json(getAll());
9
+ });
10
+
11
+ router.get('/commands/:name', (req, res) => {
12
+ res.json(getOne(req.params.name));
13
+ });
14
+
15
+ // Create a command
16
+ router.post('/commands', (req, res) => {
17
+ res.json(save(req.body.name, req.body));
18
+ });
19
+
20
+ router.put('/commands/:name', (req, res) => {
21
+ res.json(update(req.params.name, req.body));
22
+ });
23
+
24
+ // Delete a command
25
+ router.delete('/commands/:name', (req, res) => {
26
+ remove(req.params.name);
27
+ res.json({ success: true });
28
+ });
29
+
30
+ export default router;
@@ -0,0 +1,13 @@
1
+ import express from 'express';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import router from './routes/api.js';
5
+
6
+ const PORT = 4242;
7
+ const app = express();
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ app.use(express.json());
11
+ app.use(express.static(path.join(__dirname, 'public')));
12
+ app.use('/api', router);
13
+ app.listen(PORT, () => console.log(`UI running at http://localhost:${PORT}\nPress q to stop the server\n`));