--- /dev/null
+# Transcriber
+
+Transcriber is a [Bun](https://bun.sh/) CLI for executing combinations of shell
+commands.
+
+## Example
+
+Given the following `input.json`:
+
+```json
+{
+ "command": "echo $arg1 $arg2 $arg3",
+ "args": {
+ "arg1": ["value1", "value2"],
+ "arg2": ["value3", "value4"],
+ "arg3": ["value5", "value6"]
+ }
+}
+```
+
+Do:
+
+```plaintext
+bun run index.ts -- --input input.json
+$ echo value1 value3 value5
+value1 value3 value5
+$ echo value1 value3 value6
+value1 value3 value6
+$ echo value1 value4 value5
+value1 value4 value5
+$ echo value1 value4 value6
+value1 value4 value6
+$ echo value2 value3 value5
+value2 value3 value5
+$ echo value2 value3 value6
+value2 value3 value6
+$ echo value2 value4 value5
+value2 value4 value5
+$ echo value2 value4 value6
+value2 value4 value6
+```
+
+The commands are run using [Bun Shell](https://bun.sh/docs/runtime/shell), which
+implements its own set of
+[built-in commands](https://bun.sh/docs/runtime/shell#builtin-commands).
--- /dev/null
+import { $ } from "bun";
+import { Command } from "@commander-js/extra-typings";
+
+const program = new Command()
+ .name("transcriber")
+ .description("CLI for executing combinations of shell commands.")
+ .version("0.1.0")
+ .requiredOption(
+ "-i, --input <string>",
+ "JSON file with command and arguments to execute",
+ );
+program.parse();
+const options = program.opts();
+
+const file = Bun.file(options.input);
+const json = await file.json();
+const { command, args } = json;
+
+// Based on: https://stackoverflow.com/a/32839413/21084807
+function getCombinations(
+ args,
+ index = 0,
+ results: Array<Record<string, string>> = [],
+ current = {},
+): Array<Record<string, string>> {
+ const allKeys = Object.keys(args);
+ const key = allKeys[index];
+ const values = args[key];
+
+ for (let i = 0; i < values.length; i++) {
+ current[key] = values[i];
+
+ if (index + 1 < allKeys.length) {
+ getCombinations(args, index + 1, results, current);
+ } else {
+ results.push(structuredClone(current));
+ }
+ }
+
+ return results;
+}
+
+const c = Object.defineProperty([command], "raw", {
+ value: [command],
+}) as unknown as TemplateStringsArray;
+
+if (args === undefined) {
+ console.log(`$ ${command}`);
+ await $(c);
+} else {
+ const runs = getCombinations(args);
+ for (const options of runs) {
+ let input = command;
+ for (const [key, value] of Object.entries(options)) {
+ input = input.replaceAll(`$${key}`, value);
+ }
+ console.log(`$ ${input}`);
+ await $(c).env(options);
+ }
+}