oopsx 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +8 -120
  2. package/bin/merr.js +134 -8
  3. package/package.json +9 -5
package/README.md CHANGED
@@ -1,139 +1,27 @@
1
- # merr
1
+ # oopsx
2
2
 
3
- > Play a meme sound when your terminal command fails.
3
+ Play a meme sound when your terminal command fails. Comes with a default sound out of the box.
4
4
 
5
- Ever wish your terminal could roast you for typos and bad commands? **merr** wraps any shell command and plays a sound when it exits with a non-zero code. Pick your own meme sound, or use the default.
6
-
7
- ```
8
- $ merr git puhs origin main
9
- fatal: 'puhs' is not a git command.
10
- 🔊 *sad trombone plays*
11
- ```
12
-
13
- ---
14
-
15
- ## Installation
16
-
17
- ### npx (zero install)
5
+ ## Install
18
6
 
19
7
  ```bash
20
- npx merr ls /nonexistent/path
8
+ npm install -g oopsx
21
9
  ```
22
10
 
23
- ### Global install via npm
11
+ Restart your terminal. Every failed command now plays a sound.
24
12
 
25
- ```bash
26
- npm install -g merr
27
- ```
28
-
29
- ### Curl install (macOS / Linux)
13
+ ## Custom sound
30
14
 
31
15
  ```bash
32
- curl -fsSL https://raw.githubusercontent.com/devesh760/merr/main/install.sh | bash
16
+ oopsx --sound ~/bruh.mp3 --set-default echo hi
33
17
  ```
34
18
 
35
- ---
36
-
37
- ## Usage
38
-
39
- ```bash
40
- # Basic — plays default sound on failure
41
- merr ls /does/not/exist
42
-
43
- # Use a custom local sound
44
- merr --sound ~/sounds/bruh.mp3 make build
45
-
46
- # Use a sound from a URL (cached automatically)
47
- merr --url https://example.com/fail.mp3 npm test
48
-
49
- # Set volume (0–100)
50
- merr --volume 50 python script.py
51
-
52
- # Save a sound as the new default
53
- merr --sound ~/sounds/oof.mp3 --set-default echo "saved!"
54
-
55
- # Play sound only once per shell session
56
- merr --once cargo build
57
- ```
58
-
59
- ### CLI Options
60
-
61
- | Option | Description |
62
- | ------------------- | ---------------------------------------------- |
63
- | `--sound <path>` | Use a local `.mp3` or `.wav` file |
64
- | `--url <url>` | Download and cache a sound from a URL |
65
- | `--volume <0-100>` | Playback volume (default: `80`) |
66
- | `--set-default` | Persist the resolved sound as the new default |
67
- | `--once` | Play sound only once per shell session |
68
-
69
- ---
70
-
71
- ## How It Works
72
-
73
- 1. **merr** runs your command with inherited stdio (you see all output as normal).
74
- 2. If the command exits with code `0` — nothing happens.
75
- 3. If the command exits with a non-zero code — merr plays a sound.
76
- 4. merr then exits with the **same exit code** as the original command.
77
-
78
- ### Sound Resolution Priority
79
-
80
- 1. `--sound <path>` — explicit local file
81
- 2. `--url <url>` — downloads and caches the file under `~/.merr/sounds/`
82
- 3. Default sound at `~/.merr/default.mp3`
83
-
84
- ---
85
-
86
- ## Custom Sound Examples
87
-
88
- ```bash
89
- # Sad trombone on test failure
90
- merr --sound ~/memes/sad-trombone.mp3 npm test
91
-
92
- # Windows XP error from a URL
93
- merr --url "https://example.com/xp-error.mp3" cargo build
94
-
95
- # Set a default once, use it everywhere
96
- merr --sound ~/memes/bruh.mp3 --set-default echo "done"
97
- merr npm test # now uses bruh.mp3 automatically
98
- ```
99
-
100
- ---
101
-
102
- ## Config
103
-
104
- merr stores configuration in `~/.merr/`:
105
-
106
- ```
107
- ~/.merr/
108
- ├── config.json # volume and preferences
109
- ├── default.mp3 # default sound file
110
- └── sounds/ # cached URL downloads (hashed filenames)
111
- ```
112
-
113
- ---
114
-
115
- ## Platform Support
116
-
117
- | Platform | Player Backend |
118
- | -------- | ---------------------- |
119
- | macOS | `afplay` (built-in) |
120
- | Linux | `aplay` / `mpg123` |
121
- | Windows | PowerShell fallback |
122
-
123
- ---
124
-
125
19
  ## Uninstall
126
20
 
127
21
  ```bash
128
- # Remove the CLI
129
- npm uninstall -g merr
130
-
131
- # Remove config and cached sounds
132
- rm -rf ~/.merr
22
+ npm uninstall -g oopsx
133
23
  ```
134
24
 
135
- ---
136
-
137
25
  ## License
138
26
 
139
27
  MIT
package/bin/merr.js CHANGED
@@ -1,19 +1,149 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
3
7
  import { Command } from "commander";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
4
11
  import { runCommand } from "../src/runner.js";
5
12
  import { resolveSound, playAudio, setDefault, setVolume } from "../src/sound.js";
6
13
  import { readConfig } from "../src/config.js";
7
14
 
8
- const SESSION_KEY = `MERR_PLAYED_${process.ppid}`;
15
+ const HOOK_MARKER = "# oopsx shell hook";
16
+
17
+ const getShellRc = () => {
18
+ const shell = (process.env.SHELL || "").toLowerCase();
19
+ if (shell.endsWith("zsh")) return { file: join(homedir(), ".zshrc"), type: "zsh" };
20
+ if (shell.endsWith("bash")) return { file: join(homedir(), ".bashrc"), type: "bash" };
21
+ return null;
22
+ };
23
+
24
+ const hookSnippet = (type) => {
25
+ if (type === "zsh") {
26
+ return `
27
+ ${HOOK_MARKER}
28
+ eval "$(oopsx init zsh)"`;
29
+ }
30
+ return `
31
+ ${HOOK_MARKER}
32
+ eval "$(oopsx init bash)"`;
33
+ };
34
+
35
+ const SESSION_KEY = `OOPSX_PLAYED_${process.ppid}`;
9
36
 
10
37
  const program = new Command();
11
38
 
12
39
  program
13
- .name("merr")
40
+ .name("oopsx")
14
41
  .description("Play a meme sound when your terminal command fails")
15
- .version("1.0.0")
16
- .argument("<command...>", "Shell command to execute")
42
+ .version(pkg.version);
43
+
44
+ // --- `oopsx init <shell>` — output shell hook snippet ---
45
+ program
46
+ .command("init <shell>")
47
+ .description("Print shell hook to auto-play sound on errors (zsh or bash)")
48
+ .action((shell) => {
49
+ const normalized = shell.toLowerCase().trim();
50
+
51
+ if (normalized === "zsh") {
52
+ console.log(`
53
+ __oopsx_precmd() {
54
+ local exit_code=$?
55
+ [[ $exit_code -ne 0 ]] && (oopsx play &>/dev/null &)
56
+ return $exit_code
57
+ }
58
+ precmd_functions+=(__oopsx_precmd)
59
+ `.trim());
60
+ } else if (normalized === "bash") {
61
+ console.log(`
62
+ __oopsx_prompt_command() {
63
+ local exit_code=$?
64
+ [[ $exit_code -ne 0 ]] && (oopsx play &>/dev/null &)
65
+ return $exit_code
66
+ }
67
+ PROMPT_COMMAND="__oopsx_prompt_command;\${PROMPT_COMMAND}"
68
+ `.trim());
69
+ } else {
70
+ console.error(`Unsupported shell: ${shell}. Use "zsh" or "bash".`);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
75
+ // --- `oopsx setup` — auto-add hook to shell rc file ---
76
+ program
77
+ .command("setup")
78
+ .description("Automatically add the shell hook to your .zshrc or .bashrc")
79
+ .action(() => {
80
+ const rc = getShellRc();
81
+ if (!rc) {
82
+ console.error("Could not detect your shell. Manually add: eval \"$(oopsx init zsh)\" to your rc file.");
83
+ process.exit(1);
84
+ }
85
+
86
+ if (existsSync(rc.file)) {
87
+ const contents = readFileSync(rc.file, "utf-8");
88
+ if (contents.includes(HOOK_MARKER)) {
89
+ console.log(`Already set up in ${rc.file}`);
90
+ return;
91
+ }
92
+ }
93
+
94
+ appendFileSync(rc.file, hookSnippet(rc.type), "utf-8");
95
+ console.log(`✓ Added oopsx hook to ${rc.file}`);
96
+ console.log(` Open a new terminal tab to activate, or run:\n\n source ${rc.file}\n`);
97
+ });
98
+
99
+ // --- `oopsx remove` — remove hook from shell rc file ---
100
+ program
101
+ .command("remove")
102
+ .description("Remove the shell hook from your .zshrc or .bashrc")
103
+ .action(() => {
104
+ const rc = getShellRc();
105
+ if (!rc || !existsSync(rc.file)) {
106
+ console.log("Nothing to remove.");
107
+ return;
108
+ }
109
+
110
+ const contents = readFileSync(rc.file, "utf-8");
111
+ if (!contents.includes(HOOK_MARKER)) {
112
+ console.log("No oopsx hook found in " + rc.file);
113
+ return;
114
+ }
115
+
116
+ const cleaned = contents
117
+ .split("\n")
118
+ .filter((line) => !line.includes("oopsx") && line !== HOOK_MARKER)
119
+ .join("\n");
120
+
121
+ writeFileSync(rc.file, cleaned, "utf-8");
122
+ console.log(`Removed oopsx hook from ${rc.file}`);
123
+ console.log("Restart your terminal or run: source " + rc.file);
124
+ });
125
+
126
+ // --- `oopsx play` — just play the sound (used by shell hook) ---
127
+ program
128
+ .command("play")
129
+ .description("Play the error sound (used internally by the shell hook)")
130
+ .action(async () => {
131
+ const config = readConfig();
132
+ const soundFile = await resolveSound({});
133
+
134
+ if (!soundFile) process.exit(0);
135
+
136
+ try {
137
+ await playAudio(soundFile, config.volume ?? 80);
138
+ } catch {
139
+ // silent — this runs in background, don't pollute the terminal
140
+ }
141
+ });
142
+
143
+ // --- `oopsx run <command...>` — wrap a single command (original behavior) ---
144
+ program
145
+ .command("run <command...>", { isDefault: true })
146
+ .description("Execute a command and play sound on failure")
17
147
  .option("--sound <path>", "Use a local sound file (mp3/wav)")
18
148
  .option("--url <url>", "Download and cache a sound from a URL")
19
149
  .option("--volume <number>", "Playback volume 0–100", parseInt)
@@ -24,7 +154,6 @@ program
24
154
  const config = readConfig();
25
155
  const volume = options.volume ?? config.volume ?? 80;
26
156
 
27
- // Persist volume if explicitly provided
28
157
  if (options.volume !== undefined) {
29
158
  setVolume(options.volume);
30
159
  }
@@ -35,7 +164,6 @@ program
35
164
  process.exit(0);
36
165
  }
37
166
 
38
- // --once: skip if already played in this shell session
39
167
  if (options.once && process.env[SESSION_KEY]) {
40
168
  process.exit(exitCode);
41
169
  }
@@ -46,7 +174,6 @@ program
46
174
  process.exit(exitCode);
47
175
  }
48
176
 
49
- // --set-default: persist the resolved sound
50
177
  if (options.setDefault) {
51
178
  setDefault(soundFile);
52
179
  }
@@ -57,7 +184,6 @@ program
57
184
  console.error(`Sound playback failed: ${err.message}`);
58
185
  }
59
186
 
60
- // Mark as played for --once (only useful if parent reads env, but we set it anyway)
61
187
  if (options.once) {
62
188
  process.env[SESSION_KEY] = "1";
63
189
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "oopsx",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Play a meme sound when your terminal command fails",
5
5
  "type": "module",
6
6
  "bin": {
7
- "merr": "./bin/merr.js"
7
+ "oopsx": "./bin/merr.js"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=18"
@@ -26,11 +26,15 @@
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "git+https://github.com/devesh760/merr.git"
29
+ "url": "git+https://github.com/devesh760/oopsx.git"
30
30
  },
31
- "homepage": "https://github.com/devesh760/merr#readme",
31
+ "homepage": "https://github.com/devesh760/oopsx#readme",
32
32
  "bugs": {
33
- "url": "https://github.com/devesh760/merr/issues"
33
+ "url": "https://github.com/devesh760/oopsx/issues"
34
+ },
35
+ "scripts": {
36
+ "postinstall": "node bin/merr.js setup",
37
+ "preuninstall": "node bin/merr.js remove && rm -rf ~/.merr"
34
38
  },
35
39
  "dependencies": {
36
40
  "commander": "^12.1.0",