initial commit
6 files changed,  +345, -0
A .gitignore
+1, -0
1@@ -0,0 +1 @@
2+result/
A LICENSE
+21, -0
 1@@ -0,0 +1,21 @@
 2+MIT License
 3+
 4+Copyright (c) 2026 [email protected]
 5+
 6+Permission is hereby granted, free of charge, to any person obtaining a copy
 7+of this software and associated documentation files (the "Software"), to deal
 8+in the Software without restriction, including without limitation the rights
 9+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+copies of the Software, and to permit persons to whom the Software is
11+furnished to do so, subject to the following conditions:
12+
13+The above copyright notice and this permission notice shall be included in all
14+copies or substantial portions of the Software.
15+
16+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+SOFTWARE.
A README.md
+61, -0
 1@@ -0,0 +1,61 @@
 2+# concurrently
 3+
 4+Run multiple commands with multiplexed, colorized output.
 5+
 6+## Usage
 7+
 8+```bash
 9+concurrently NAME1 'COMMAND1' [NAME2 'COMMAND2' ...]
10+```
11+
12+## Example
13+
14+```bash
15+concurrently Vite 'pnpm exec vite' Go 'find . -name "*.go" | entr go build'
16+```
17+
18+Output:
19+```
20+[Vite] Dev server running on port 5173...
21+[Go] Compiled output
22+```
23+
24+## Features
25+
26+- Multiple commands run in parallel
27+- Each process output prefixed with `[NAME]` in a distinct color
28+- Colors cycle through 8 vibrant colors for dark terminals
29+- SIGINT (Ctrl+C) terminates all processes cleanly
30+- Line-buffered output for real-time display
31+
32+## Installation
33+
34+### Using Nix Flakes
35+
36+```bash
37+nix run git+https://github.com/yourusername/concurrently -- Vite 'npm run dev'
38+```
39+
40+Or add to your `flake.nix`:
41+
42+```nix
43+{
44+  inputs.concurrently.url = "git+https://github.com/yourusername/concurrently";
45+  
46+  outputs = { self, nixpkgs, concurrently }: {
47+    # Use in your packages
48+  };
49+}
50+```
51+
52+### Manual Installation
53+
54+Copy the `concurrently` script to your `$PATH`.
55+
56+## Color Palette
57+
58+Colors cycle through: Cyan, Green, Yellow, Magenta, Blue, Red, Orange, White
59+
60+## License
61+
62+MIT
A concurrently
+150, -0
  1@@ -0,0 +1,150 @@
  2+#!/usr/bin/env bash
  3+#
  4+# concurrently - Run multiple commands with multiplexed output
  5+# Usage: concurrently NAME1 'COMMAND1' [NAME2 'COMMAND2' ...]
  6+#
  7+# Runs all commands in parallel with output prefixed by [NAME].
  8+# Press Ctrl+C to terminate all processes.
  9+#
 10+
 11+set -euo pipefail
 12+
 13+# Color palette for dark terminal themes
 14+# Format: \e[38;2;R;G;Bm (256-color true color)
 15+declare -a COLORS=(
 16+    '\e[38;2;0;255;255m'    # Cyan
 17+    '\e[38;2;0;255;0m'      # Green
 18+    '\e[38;2;255;255;0m'    # Yellow
 19+    '\e[38;2;255;0;255m'    # Magenta
 20+    '\e[38;2;0;160;255m'    # Blue
 21+    '\e[38;2;255;100;100m'  # Red (soft)
 22+    '\e[38;2;255;165;0m'    # Orange
 23+)
 24+readonly RESET='\e[0m'
 25+readonly NUM_COLORS=${#COLORS[@]}
 26+
 27+# Global exit code for trap
 28+EXIT_CODE=0
 29+
 30+# Validate we have at least one name-command pair
 31+if [[ $# -lt 2 ]]; then
 32+    echo "Usage: concurrently NAME1 'COMMAND1' [NAME2 'COMMAND2' ...]" >&2
 33+    exit 1
 34+fi
 35+
 36+# Validate we have an even number of arguments (name-cmd pairs)
 37+if [[ $(( $# % 2 )) -ne 0 ]]; then
 38+    echo "Error: Arguments must be name-command pairs" >&2
 39+    exit 1
 40+fi
 41+
 42+# Create temp directory for FIFOs
 43+readonly TMPDIR=$(mktemp -d /tmp/concurrently.XXXXXX)
 44+readonly FIFODIR="$TMPDIR/fifos"
 45+readonly PIDDIR="$TMPDIR/pids"
 46+mkdir -p "$FIFODIR" "$PIDDIR"
 47+
 48+# Track PIDs
 49+declare -a JOB_PIDS=()
 50+declare -a READER_PIDS=()
 51+declare -a JOB_NAMES=()
 52+
 53+# Cleanup function
 54+cleanup() {
 55+    local exit_code=${1:-0}
 56+
 57+    # Kill all reader processes
 58+    for pid in "${READER_PIDS[@]}"; do
 59+        kill "$pid" 2>/dev/null || true
 60+    done
 61+
 62+    # Kill all job processes
 63+    for pid in "${JOB_PIDS[@]}"; do
 64+        if kill -0 "$pid" 2>/dev/null; then
 65+            kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
 66+        fi
 67+    done
 68+
 69+    # Remove temp directory
 70+    rm -rf "$TMPDIR"
 71+
 72+    exit "$exit_code"
 73+}
 74+
 75+trap 'cleanup $EXIT_CODE' INT TERM EXIT
 76+
 77+# Start reader process for a FIFO
 78+# Reads lines from FIFO and prefixes with colored [NAME]
 79+start_reader() {
 80+    local name="$1"
 81+    local fifo="$2"
 82+    local color="$3"
 83+
 84+    (
 85+        while IFS= read -r line; do
 86+            printf "$color[%s]$RESET %s\n" "$name" "$line"
 87+        done < "$fifo"
 88+    ) &
 89+
 90+    READER_PIDS+=($!)
 91+}
 92+
 93+# Start all jobs
 94+job_index=0
 95+while [[ $# -gt 0 ]]; do
 96+    name="$1"
 97+    cmd="$2"
 98+    shift 2
 99+
100+    # Validate name
101+    if [[ "$name" == */* ]]; then
102+        echo "Error: Name cannot contain '/': $name" >&2
103+        EXIT_CODE=1
104+        cleanup 1
105+    fi
106+
107+    # Check for duplicates
108+    for existing_name in "${JOB_NAMES[@]}"; do
109+        if [[ "$existing_name" == "$name" ]]; then
110+            echo "Error: Duplicate name: $name" >&2
111+            EXIT_CODE=1
112+            cleanup 1
113+        fi
114+    done
115+
116+    JOB_NAMES+=("$name")
117+    fifo="$FIFODIR/$name"
118+
119+    # Create FIFO
120+    mkfifo "$fifo"
121+
122+    # Assign color (modulo to cycle through palette)
123+    color_index=$((job_index % NUM_COLORS))
124+    color="${COLORS[$color_index]}"
125+
126+    # Start reader for this FIFO with assigned color
127+    start_reader "$name" "$fifo" "$color"
128+
129+    # Start job writing to FIFO (line buffered)
130+    (
131+        exec stdbuf -oL bash -c "$cmd" > "$fifo" 2>&1
132+    ) &
133+
134+    JOB_PIDS+=($!)
135+    job_index=$((job_index + 1))
136+done
137+
138+# Wait for all jobs to exit
139+for i in "${!JOB_NAMES[@]}"; do
140+    name="${JOB_NAMES[$i]}"
141+    pid="${JOB_PIDS[$i]}"
142+
143+    # Wait for this specific job
144+    if kill -0 "$pid" 2>/dev/null; then
145+        wait "$pid" 2>/dev/null || true
146+        echo "[$name] (process exited)"
147+    fi
148+done
149+
150+# Give readers time to flush
151+cleanup 0
A flake.lock
+61, -0
 1@@ -0,0 +1,61 @@
 2+{
 3+  "nodes": {
 4+    "flake-utils": {
 5+      "inputs": {
 6+        "systems": "systems"
 7+      },
 8+      "locked": {
 9+        "lastModified": 1731533236,
10+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
11+        "owner": "numtide",
12+        "repo": "flake-utils",
13+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
14+        "type": "github"
15+      },
16+      "original": {
17+        "owner": "numtide",
18+        "repo": "flake-utils",
19+        "type": "github"
20+      }
21+    },
22+    "nixpkgs": {
23+      "locked": {
24+        "lastModified": 1776949667,
25+        "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
26+        "owner": "NixOS",
27+        "repo": "nixpkgs",
28+        "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
29+        "type": "github"
30+      },
31+      "original": {
32+        "owner": "NixOS",
33+        "ref": "nixpkgs-unstable",
34+        "repo": "nixpkgs",
35+        "type": "github"
36+      }
37+    },
38+    "root": {
39+      "inputs": {
40+        "flake-utils": "flake-utils",
41+        "nixpkgs": "nixpkgs"
42+      }
43+    },
44+    "systems": {
45+      "locked": {
46+        "lastModified": 1681028828,
47+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
48+        "owner": "nix-systems",
49+        "repo": "default",
50+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
51+        "type": "github"
52+      },
53+      "original": {
54+        "owner": "nix-systems",
55+        "repo": "default",
56+        "type": "github"
57+      }
58+    }
59+  },
60+  "root": "root",
61+  "version": 7
62+}
A flake.nix
+51, -0
 1@@ -0,0 +1,51 @@
 2+{
 3+  description = "concurrently - Run multiple commands with multiplexed output";
 4+
 5+  inputs = {
 6+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
 7+    flake-utils.url = "github:numtide/flake-utils";
 8+  };
 9+
10+  outputs = { self, nixpkgs, flake-utils }:
11+    flake-utils.lib.eachDefaultSystem (system:
12+      let
13+        pkgs = nixpkgs.legacyPackages.${system};
14+      in
15+      {
16+        packages = {
17+          default = pkgs.stdenvNoCC.mkDerivation {
18+            pname = "concurrently";
19+            version = "1.0.0";
20+
21+            src = ./.;
22+
23+            dontBuild = true;
24+            dontConfigure = true;
25+
26+            installPhase = ''
27+              mkdir -p $out/bin
28+              cp concurrently $out/bin/concurrently
29+              chmod +x $out/bin/concurrently
30+
31+              # Patch shebang to use nixpkgs bash
32+              substituteInPlace $out/bin/concurrently \
33+                --replace "#!/usr/bin/env bash" "#!${pkgs.bash}/bin/bash"
34+            '';
35+
36+            meta = with pkgs.lib; {
37+              description = "Run multiple commands with multiplexed, colorized output";
38+              homepage = "https://kilimanjaro.io/concurrently";
39+              license = licenses.mit;
40+              platforms = platforms.all;
41+              mainProgram = "concurrently";
42+            };
43+          };
44+        };
45+
46+        apps = {
47+          default = flake-utils.lib.mkApp {
48+            drv = self.packages.${system}.default;
49+          };
50+        };
51+      });
52+}