oopsx 0.0.1 → 0.0.3

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 +5 -71
  2. package/bin/merr.js +128 -6
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -1,14 +1,6 @@
1
1
  # oopsx
2
2
 
3
- A CLI that plays a meme sound when your terminal command fails.
4
-
5
- Wrap any command with `oopsx`. If it fails (non-zero exit code), you hear a sound. If it passes, nothing happens. Your original output and exit code are preserved.
6
-
7
- ```
8
- $ oopsx git puhs origin main
9
- fatal: 'puhs' is not a git command.
10
- 🔊 *plays sound*
11
- ```
3
+ Play a meme sound when your terminal command fails. Comes with a default sound out of the box.
12
4
 
13
5
  ## Install
14
6
 
@@ -16,76 +8,18 @@ fatal: 'puhs' is not a git command.
16
8
  npm install -g oopsx
17
9
  ```
18
10
 
19
- Or run without installing:
20
-
21
- ```bash
22
- npx oopsx ls /nonexistent/path
23
- ```
24
-
25
- Or via curl (macOS / Linux):
26
-
27
- ```bash
28
- curl -fsSL https://raw.githubusercontent.com/devesh760/oopsx/main/install.sh | bash
29
- ```
11
+ Restart your terminal. Every failed command now plays a sound.
30
12
 
31
- ## Usage
32
-
33
- ```bash
34
- oopsx <command>
35
- ```
13
+ ## Custom sound
36
14
 
37
15
  ```bash
38
- oopsx ls /does/not/exist # plays default sound on failure
39
- oopsx --sound ~/bruh.mp3 npm test # use a custom sound
40
- oopsx --url https://x.com/f.mp3 make build # download + cache a sound
41
- oopsx --volume 50 python script.py # set volume (0-100)
42
- oopsx --sound ~/oof.mp3 --set-default echo hi # save as default
43
- oopsx --once cargo build # play only once per session
16
+ oopsx --sound ~/bruh.mp3 --set-default echo hi
44
17
  ```
45
18
 
46
- ## Options
47
-
48
- | Option | Description |
49
- | ------------------- | --------------------------------------------- |
50
- | `--sound <path>` | Use a local `.mp3` or `.wav` file |
51
- | `--url <url>` | Download and cache a sound from a URL |
52
- | `--volume <0-100>` | Playback volume (default: `80`) |
53
- | `--set-default` | Save the resolved sound as the new default |
54
- | `--once` | Play sound only once per shell session |
55
-
56
- ## How it works
57
-
58
- 1. Runs your command with inherited stdio — you see all output as normal.
59
- 2. If exit code is `0`, does nothing.
60
- 3. If exit code is non-zero, plays a sound.
61
- 4. Exits with the **same exit code** as your command.
62
-
63
- A default sound is bundled out of the box. You can replace it with `--sound <file> --set-default`.
64
-
65
- Sound priority: `--sound` > `--url` > `~/.merr/default.mp3`
66
-
67
- ## Config
68
-
69
- Stored in `~/.merr/`:
70
-
71
- ```
72
- ~/.merr/
73
- ├── config.json # volume
74
- ├── default.mp3 # default sound
75
- └── sounds/ # cached URL downloads
76
- ```
77
-
78
- ## Platform support
79
-
80
- | Platform | Backend |
81
- | -------- | -------------------- |
82
- | macOS | `afplay` (built-in) |
83
- | Linux | `aplay` / `mpg123` |
84
- | Windows | PowerShell fallback |
85
-
86
19
  ## Uninstall
87
20
 
88
21
  ```bash
22
+ oopsx remove
89
23
  npm uninstall -g oopsx
90
24
  rm -rf ~/.merr
91
25
  ```
package/bin/merr.js CHANGED
@@ -1,10 +1,33 @@
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
 
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
+
8
31
  const SESSION_KEY = `OOPSX_PLAYED_${process.ppid}`;
9
32
 
10
33
  const program = new Command();
@@ -12,8 +35,111 @@ const program = new Command();
12
35
  program
13
36
  .name("oopsx")
14
37
  .description("Play a meme sound when your terminal command fails")
15
- .version("1.0.0")
16
- .argument("<command...>", "Shell command to execute")
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("Restart your terminal or run: source " + rc.file);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "oopsx",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Play a meme sound when your terminal command fails",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,9 @@
32
32
  "bugs": {
33
33
  "url": "https://github.com/devesh760/oopsx/issues"
34
34
  },
35
+ "scripts": {
36
+ "postinstall": "node bin/merr.js setup"
37
+ },
35
38
  "dependencies": {
36
39
  "commander": "^12.1.0",
37
40
  "play-sound": "^1.1.6"