oopsx 1.0.0 → 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 +8 -120
- package/bin/merr.js +130 -8
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,139 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oopsx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Play a meme sound when your terminal command fails. Comes with a default sound out of the box.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
+
npm install -g oopsx
|
|
21
9
|
```
|
|
22
10
|
|
|
23
|
-
|
|
11
|
+
Restart your terminal. Every failed command now plays a sound.
|
|
24
12
|
|
|
25
|
-
|
|
26
|
-
npm install -g merr
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Curl install (macOS / Linux)
|
|
13
|
+
## Custom sound
|
|
30
14
|
|
|
31
15
|
```bash
|
|
32
|
-
|
|
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
|
-
|
|
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,145 @@
|
|
|
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 } from "node:path";
|
|
3
6
|
import { Command } from "commander";
|
|
4
7
|
import { runCommand } from "../src/runner.js";
|
|
5
8
|
import { resolveSound, playAudio, setDefault, setVolume } from "../src/sound.js";
|
|
6
9
|
import { readConfig } from "../src/config.js";
|
|
7
10
|
|
|
8
|
-
const
|
|
11
|
+
const HOOK_MARKER = "# oopsx shell hook";
|
|
12
|
+
|
|
13
|
+
const getShellRc = () => {
|
|
14
|
+
const shell = (process.env.SHELL || "").toLowerCase();
|
|
15
|
+
if (shell.endsWith("zsh")) return { file: join(homedir(), ".zshrc"), type: "zsh" };
|
|
16
|
+
if (shell.endsWith("bash")) return { file: join(homedir(), ".bashrc"), type: "bash" };
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const hookSnippet = (type) => {
|
|
21
|
+
if (type === "zsh") {
|
|
22
|
+
return `
|
|
23
|
+
${HOOK_MARKER}
|
|
24
|
+
eval "$(oopsx init zsh)"`;
|
|
25
|
+
}
|
|
26
|
+
return `
|
|
27
|
+
${HOOK_MARKER}
|
|
28
|
+
eval "$(oopsx init bash)"`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SESSION_KEY = `OOPSX_PLAYED_${process.ppid}`;
|
|
9
32
|
|
|
10
33
|
const program = new Command();
|
|
11
34
|
|
|
12
35
|
program
|
|
13
|
-
.name("
|
|
36
|
+
.name("oopsx")
|
|
14
37
|
.description("Play a meme sound when your terminal command fails")
|
|
15
|
-
.version("1.0.0")
|
|
16
|
-
|
|
38
|
+
.version("1.0.0");
|
|
39
|
+
|
|
40
|
+
// --- `oopsx init <shell>` — output shell hook snippet ---
|
|
41
|
+
program
|
|
42
|
+
.command("init <shell>")
|
|
43
|
+
.description("Print shell hook to auto-play sound on errors (zsh or bash)")
|
|
44
|
+
.action((shell) => {
|
|
45
|
+
const normalized = shell.toLowerCase().trim();
|
|
46
|
+
|
|
47
|
+
if (normalized === "zsh") {
|
|
48
|
+
console.log(`
|
|
49
|
+
__oopsx_precmd() {
|
|
50
|
+
local exit_code=$?
|
|
51
|
+
[[ $exit_code -ne 0 ]] && (oopsx play &>/dev/null &)
|
|
52
|
+
return $exit_code
|
|
53
|
+
}
|
|
54
|
+
precmd_functions+=(__oopsx_precmd)
|
|
55
|
+
`.trim());
|
|
56
|
+
} else if (normalized === "bash") {
|
|
57
|
+
console.log(`
|
|
58
|
+
__oopsx_prompt_command() {
|
|
59
|
+
local exit_code=$?
|
|
60
|
+
[[ $exit_code -ne 0 ]] && (oopsx play &>/dev/null &)
|
|
61
|
+
return $exit_code
|
|
62
|
+
}
|
|
63
|
+
PROMPT_COMMAND="__oopsx_prompt_command;\${PROMPT_COMMAND}"
|
|
64
|
+
`.trim());
|
|
65
|
+
} else {
|
|
66
|
+
console.error(`Unsupported shell: ${shell}. Use "zsh" or "bash".`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- `oopsx setup` — auto-add hook to shell rc file ---
|
|
72
|
+
program
|
|
73
|
+
.command("setup")
|
|
74
|
+
.description("Automatically add the shell hook to your .zshrc or .bashrc")
|
|
75
|
+
.action(() => {
|
|
76
|
+
const rc = getShellRc();
|
|
77
|
+
if (!rc) {
|
|
78
|
+
console.error("Could not detect your shell. Manually add: eval \"$(oopsx init zsh)\" to your rc file.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (existsSync(rc.file)) {
|
|
83
|
+
const contents = readFileSync(rc.file, "utf-8");
|
|
84
|
+
if (contents.includes(HOOK_MARKER)) {
|
|
85
|
+
console.log(`Already set up in ${rc.file}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
appendFileSync(rc.file, hookSnippet(rc.type), "utf-8");
|
|
91
|
+
console.log(`✓ Added oopsx hook to ${rc.file}`);
|
|
92
|
+
console.log(` Open a new terminal tab to activate, or run:\n\n source ${rc.file}\n`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- `oopsx remove` — remove hook from shell rc file ---
|
|
96
|
+
program
|
|
97
|
+
.command("remove")
|
|
98
|
+
.description("Remove the shell hook from your .zshrc or .bashrc")
|
|
99
|
+
.action(() => {
|
|
100
|
+
const rc = getShellRc();
|
|
101
|
+
if (!rc || !existsSync(rc.file)) {
|
|
102
|
+
console.log("Nothing to remove.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const contents = readFileSync(rc.file, "utf-8");
|
|
107
|
+
if (!contents.includes(HOOK_MARKER)) {
|
|
108
|
+
console.log("No oopsx hook found in " + rc.file);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cleaned = contents
|
|
113
|
+
.split("\n")
|
|
114
|
+
.filter((line) => !line.includes("oopsx") && line !== HOOK_MARKER)
|
|
115
|
+
.join("\n");
|
|
116
|
+
|
|
117
|
+
writeFileSync(rc.file, cleaned, "utf-8");
|
|
118
|
+
console.log(`Removed oopsx hook from ${rc.file}`);
|
|
119
|
+
console.log("Restart your terminal or run: source " + rc.file);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// --- `oopsx play` — just play the sound (used by shell hook) ---
|
|
123
|
+
program
|
|
124
|
+
.command("play")
|
|
125
|
+
.description("Play the error sound (used internally by the shell hook)")
|
|
126
|
+
.action(async () => {
|
|
127
|
+
const config = readConfig();
|
|
128
|
+
const soundFile = await resolveSound({});
|
|
129
|
+
|
|
130
|
+
if (!soundFile) process.exit(0);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await playAudio(soundFile, config.volume ?? 80);
|
|
134
|
+
} catch {
|
|
135
|
+
// silent — this runs in background, don't pollute the terminal
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- `oopsx run <command...>` — wrap a single command (original behavior) ---
|
|
140
|
+
program
|
|
141
|
+
.command("run <command...>", { isDefault: true })
|
|
142
|
+
.description("Execute a command and play sound on failure")
|
|
17
143
|
.option("--sound <path>", "Use a local sound file (mp3/wav)")
|
|
18
144
|
.option("--url <url>", "Download and cache a sound from a URL")
|
|
19
145
|
.option("--volume <number>", "Playback volume 0–100", parseInt)
|
|
@@ -24,7 +150,6 @@ program
|
|
|
24
150
|
const config = readConfig();
|
|
25
151
|
const volume = options.volume ?? config.volume ?? 80;
|
|
26
152
|
|
|
27
|
-
// Persist volume if explicitly provided
|
|
28
153
|
if (options.volume !== undefined) {
|
|
29
154
|
setVolume(options.volume);
|
|
30
155
|
}
|
|
@@ -35,7 +160,6 @@ program
|
|
|
35
160
|
process.exit(0);
|
|
36
161
|
}
|
|
37
162
|
|
|
38
|
-
// --once: skip if already played in this shell session
|
|
39
163
|
if (options.once && process.env[SESSION_KEY]) {
|
|
40
164
|
process.exit(exitCode);
|
|
41
165
|
}
|
|
@@ -46,7 +170,6 @@ program
|
|
|
46
170
|
process.exit(exitCode);
|
|
47
171
|
}
|
|
48
172
|
|
|
49
|
-
// --set-default: persist the resolved sound
|
|
50
173
|
if (options.setDefault) {
|
|
51
174
|
setDefault(soundFile);
|
|
52
175
|
}
|
|
@@ -57,7 +180,6 @@ program
|
|
|
57
180
|
console.error(`Sound playback failed: ${err.message}`);
|
|
58
181
|
}
|
|
59
182
|
|
|
60
|
-
// Mark as played for --once (only useful if parent reads env, but we set it anyway)
|
|
61
183
|
if (options.once) {
|
|
62
184
|
process.env[SESSION_KEY] = "1";
|
|
63
185
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oopsx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Play a meme sound when your terminal command fails",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
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/
|
|
29
|
+
"url": "git+https://github.com/devesh760/oopsx.git"
|
|
30
30
|
},
|
|
31
|
-
"homepage": "https://github.com/devesh760/
|
|
31
|
+
"homepage": "https://github.com/devesh760/oopsx#readme",
|
|
32
32
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/devesh760/
|
|
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",
|