oops-cli 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.ja.md +8 -0
- data/CHANGELOG.md +8 -0
- data/README.ja.md +58 -16
- data/README.md +58 -16
- data/lib/oops/cli.rb +29 -5
- data/lib/oops/config.rb +14 -2
- data/lib/oops/recorder.rb +2 -1
- data/lib/oops/session_log.rb +42 -13
- data/lib/oops/shell/oops.fish +99 -0
- data/lib/oops/shell_init.rb +15 -9
- data/lib/oops/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e1003df0d3be3f85135c9f36b34aaed5d5ce02002a22d1c75baafd235180b15
|
|
4
|
+
data.tar.gz: b67d4984e973482dc9c4f4938d14e65dfc4e99c7c149a2deea7a6aee0e2c2297
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6754adb99ca3b4ae7f0294eb84547f630fd6fcb9bbe9191cf9112e77d9590b6dd5561aa94300e2c19e683d1a9e50072358d5814485de8c355ece40e349ca360
|
|
7
|
+
data.tar.gz: ede05535447c896dca5e443e883c9c90d7b225a7ccea09ee0628601d8650f9b1642aa2098c7c0ebedb6d0c8433645134402352140d45cb2dffc25eac07425fba
|
data/CHANGELOG.ja.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.md) | 日本語
|
|
4
4
|
|
|
5
|
+
## 0.3.0 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
- `oops clear` で閉じたタブのログを削除できます。`oops clear --all` は今のタブのログも空にします([#42](https://github.com/ysb3996-blip/oops/issues/42))。
|
|
8
|
+
- 変更: 閉じたタブのログは、最後のコマンドから 7 日たつと、タブが記録を始めるときに削除されるようになりました。日数は設定の `retention_days` で変えられます。`retention_days: false` にすると、50 MB の上限までは残ります([#42](https://github.com/ysb3996-blip/oops/issues/42))。
|
|
9
|
+
- fish に対応しました。`~/.config/fish/config.fish` に `oops init fish | source` を足します([#9](https://github.com/ysb3996-blip/oops/issues/9))。
|
|
10
|
+
- zsh のプラグインマネージャー(antidote・zinit・Oh My Zsh など)向けに `oops.plugin.zsh` を追加。clone したリポジトリの oops を動かすので、gem のインストールはいりません。
|
|
11
|
+
- Homebrew tap を用意しました: `brew install ysb3996-blip/oops/oops`。Homebrew の Ruby で oops を動かします。
|
|
12
|
+
|
|
5
13
|
## 0.2.0 - 2026-11-01
|
|
6
14
|
|
|
7
15
|
- `oops status` を追加。`recording`・`paused`・`off` のどれかを 1 行で出力し、記録中のときだけ終了コード 0 を返します。プロンプトやスクリプトから、このシェルが記録されているか確認できます。
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
English | [日本語](CHANGELOG.ja.md)
|
|
4
4
|
|
|
5
|
+
## 0.3.0 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
- `oops clear` deletes the logs of closed tabs, and `oops clear --all` also empties the current tab's log ([#42](https://github.com/ysb3996-blip/oops/issues/42)).
|
|
8
|
+
- Changed: logs of closed tabs are now deleted 7 days after their last command, when a tab starts recording. Set `retention_days` in the config to change the number of days, or `retention_days: false` to keep them until the 50 MB limit ([#42](https://github.com/ysb3996-blip/oops/issues/42)).
|
|
9
|
+
- fish support: add `oops init fish | source` to `~/.config/fish/config.fish` ([#9](https://github.com/ysb3996-blip/oops/issues/9)).
|
|
10
|
+
- `oops.plugin.zsh` for zsh plugin managers (antidote, zinit, Oh My Zsh, ...). It runs oops from the cloned repository, so no gem install is needed.
|
|
11
|
+
- Homebrew tap: `brew install ysb3996-blip/oops/oops`. The formula runs oops with Homebrew's Ruby.
|
|
12
|
+
|
|
5
13
|
## 0.2.0 - 2026-11-01
|
|
6
14
|
|
|
7
15
|
- `oops status` prints `recording`, `paused` or `off` on one line and exits 0 only when recording, so a prompt or script can show whether this shell is recorded.
|
data/README.ja.md
CHANGED
|
@@ -21,9 +21,41 @@
|
|
|
21
21
|
gem install oops-cli
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Homebrew
|
|
24
|
+
Homebrew でも入れられます。
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
```sh
|
|
27
|
+
brew install ysb3996-blip/oops/oops
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Homebrew 版は Homebrew の Ruby で oops を動かすので、`PATH` の先に古い Ruby があっても動きます。
|
|
31
|
+
|
|
32
|
+
macOS または Linux(WSL は Linux 扱い)、Ruby 3.3 以降、zsh・bash・fish のどれかが必要です。実行時に依存する gem はありません。
|
|
33
|
+
|
|
34
|
+
`gem install` は、そのとき有効な Ruby にだけ oops を入れます。gem は Ruby のバージョンごとに別々です。rbenv・asdf・mise などで別の Ruby を使うタブでは、`oops` が `command not found` のエラーになります(シェルの起動時にも出ます)。いつでも使える直し方は、有効な Ruby で `gem install oops-cli` をもう一度実行することです(rbenv なら続けて `rbenv rehash`)。`.ruby-version`・`.tool-versions`・`mise.toml` で Ruby を指定しているディレクトリの外では、代わりにインストールした Ruby を標準(グローバル)の Ruby に設定することもできます(例: `rbenv global 3.3.5`)。
|
|
35
|
+
|
|
36
|
+
### zsh のプラグインマネージャーで入れる
|
|
37
|
+
|
|
38
|
+
zsh なら、プラグインとして読み込むこともできます。clone したリポジトリの oops をそのまま動かすので、gem のインストールはいりません(Ruby 3.3 以上は必要です)。下の「シェルの設定」の 1 行の代わりになります。
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
# antidote: ~/.zsh_plugins.txt に追加
|
|
42
|
+
ysb3996-blip/oops
|
|
43
|
+
|
|
44
|
+
# zinit: ~/.zshrc に。`wait`(turbo モード)は付けない
|
|
45
|
+
zinit light ysb3996-blip/oops
|
|
46
|
+
|
|
47
|
+
# Oh My Zsh: clone してから ~/.zshrc の plugins=(...) に oops を追加
|
|
48
|
+
git clone https://github.com/ysb3996-blip/oops "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/oops"
|
|
49
|
+
|
|
50
|
+
# プラグインマネージャーなし: clone してから ~/.zshrc に source の行を追加
|
|
51
|
+
git clone https://github.com/ysb3996-blip/oops ~/.oops
|
|
52
|
+
source ~/.oops/oops.plugin.zsh
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- 読み込みを後回しにしないでください(zinit の `wait`、zsh-defer など)。プラグインはシェルを記録用に起動し直すので、最初のプロンプトより前に済ませる必要があります。
|
|
56
|
+
- 下の「シェルの設定」にある、行を置く場所の注意は、プラグインを読み込む場所にそのまま当てはまります。powerlevel10k の instant prompt を使っている場合、プラグインは instant prompt のブロックより上で読み込む必要がありますが、プラグインマネージャーはふつうそうなりません。その場合は rc ファイルの 1 行を使ってください。
|
|
57
|
+
- `PATH` の Ruby が古いと、`oops: ... init zsh failed (oops needs Ruby 3.3 or later); recording disabled` と出て、記録なしで普通に起動します。
|
|
58
|
+
- プラグインマネージャーは `main` ブランチから更新するので、最新のリリースより先の内容になることがあります。
|
|
27
59
|
|
|
28
60
|
## シェルの設定
|
|
29
61
|
|
|
@@ -34,14 +66,21 @@ eval "$(oops init zsh)" # zsh
|
|
|
34
66
|
eval "$(oops init bash)" # bash
|
|
35
67
|
```
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
fish では、`~/.config/fish/config.fish` に次の 1 行を足します。
|
|
70
|
+
|
|
71
|
+
```fish
|
|
72
|
+
oops init fish | source
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
そのあと新しいタブを開いてください。開いているタブでは `exec zsh`(bash なら `exec bash`、fish なら `exec fish`)で反映されます。
|
|
38
76
|
|
|
39
77
|
この行を置く場所が重要です。
|
|
40
78
|
|
|
41
79
|
- oops の入った Ruby を `PATH` に通す設定(rbenv、asdf、mise、Homebrew の `shellenv` など)より**後ろ**に置きます。macOS ではログインシェルが `PATH` をリセットするため、この行が先にあるとシステムの Ruby 2.6 が選ばれて oops が起動しないことがあります。oops が動かなくてもシェル自体は普通に起動します。記録されないだけです。
|
|
42
80
|
- 2 回実行したくない重い処理や出力の多い処理(nvm、プロンプトテーマ、バナーなど)より**前**に置きます。このスニペットは、小さな記録用プログラム(`oops record`、pty 中継)の下でシェルをもう一度起動します。そのため、この行より上は外側のシェルと内側のシェルで 2 回実行され、下は内側のシェルでだけ実行されます。
|
|
43
81
|
- powerlevel10k を使う場合は、`~/.zshrc` の先頭にある instant prompt のブロックより**上**に置きます。instant prompt は `~/.zshrc` の残りを読む間、シェルの入出力をリダイレクトし、その間スニペットは何もしません(実際の端末でだけ動作するため)。すると oops は黙って記録しなくなります。`~/.zshrc` に行があるのにタブが記録されないときは `oops doctor` を実行してください。`recording: no` と表示されます。
|
|
44
|
-
- zsh ではシェルがログインシェルではない形で再起動されます。`~/.zlogin` にだけ書いた設定は、記録されるシェルには適用されません。`~/.zprofile` か `~/.zshrc`(この行より上)に置いてください。bash ではログインシェルは `-l` 付きで再起動されます。
|
|
82
|
+
- zsh ではシェルがログインシェルではない形で再起動されます。`~/.zlogin` にだけ書いた設定は、記録されるシェルには適用されません。`~/.zprofile` か `~/.zshrc`(この行より上)に置いてください。bash ではログインシェルは `-l` 付きで、fish では `--login` 付きで再起動されます。
|
|
83
|
+
- fish は最初のプロンプトを出す直前に再起動されます。そのため、行をどこに置いても、`config.fish` 全体が外側と記録される側の両方で実行されます(もっと早く再起動すると、プロンプトを出さない `fish -i -c` や `fish -i script.fish` まで再起動してしまうためです)。
|
|
45
84
|
|
|
46
85
|
おかしいと感じたら `oops doctor` を実行します。このタブが記録されているか、シェルとフックの読み込み状況、使われるクリップボードの方法、設定ファイル、セッションログのサイズ、Ruby のバージョンを報告します。
|
|
47
86
|
|
|
@@ -55,10 +94,11 @@ eval "$(oops init bash)" # bash
|
|
|
55
94
|
| `oops 3` / `oops -n 3` | 直前の 3 コマンドを(古い順に)1 つのコードブロックとしてコピーする |
|
|
56
95
|
| `oops err` | 終了コードが 0 以外だった直近のコマンドをコピーする(成功したコマンドは飛ばす) |
|
|
57
96
|
| `oops err -n 2` | 直近の失敗した 2 コマンドを古い順にコピーする(成功したコマンドは飛ばす) |
|
|
58
|
-
| `oops init zsh` / `oops init bash` | シェルのスニペットを出力する(`eval`
|
|
97
|
+
| `oops init zsh` / `oops init bash` / `oops init fish` | シェルのスニペットを出力する(`eval` 用。fish は `\| source`) |
|
|
59
98
|
| `oops doctor` | 記録・フック・クリップボード・設定・ログサイズを診断する |
|
|
60
99
|
| `oops pause` / `oops resume` | 記録を止める/再開する。停止中は何もディスクに書かれない |
|
|
61
100
|
| `oops status` | `recording`・`paused`・`off` のどれかを 1 行で出力する(スクリプトやプロンプト向け) |
|
|
101
|
+
| `oops clear` / `oops clear --all` | 閉じたタブのログを削除する。`--all` を付けると、今のタブのログも空にする |
|
|
62
102
|
| `oops record -- <shell>` | 内部用: 記録用プログラムの下でシェルを起動する。スニペットが呼び出す |
|
|
63
103
|
| `oops --version`, `oops --help` | バージョンと使い方 |
|
|
64
104
|
|
|
@@ -156,12 +196,14 @@ replace: # コピー時に適用する単純な文字列置換
|
|
|
156
196
|
secrets: # マスクする追加の正規表現
|
|
157
197
|
- "sk_live_[A-Za-z0-9]+"
|
|
158
198
|
clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52 | 任意のコマンド(標準入力から読む)
|
|
199
|
+
retention_days: 7 # 閉じたタブのログをこの日数で削除する。false なら残す
|
|
159
200
|
```
|
|
160
201
|
|
|
161
202
|
- 置換とマスクはコピーする瞬間に適用されます。記録済みのログは変更されないため、設定を変えると以前のコマンドにも反映されます。
|
|
162
203
|
- `replace` のキーは長いものから順に、大文字小文字を区別して適用されます。
|
|
163
204
|
- 未知のキーは警告されて無視されます。YAML が壊れている場合も警告が出て、設定なしで続行します。
|
|
164
205
|
- `clipboard: auto` のとき、oops は macOS では `pbcopy`、WSL では `clip.exe`、Wayland では `wl-copy`、X11 では `xclip` か `xsel` を試し、だめなら OSC 52 端末エスケープシーケンスにフォールバックします(SSH 越しでもこれが使われます)。どれを選んだかは `oops doctor` で確認できます。
|
|
206
|
+
- `retention_days`(標準は 7)は、閉じたタブがログに最後に書き込んだ時点から数えます。古いログは、タブが記録を始めるときに削除されます。`false` にすると、閉じたタブのログは 50 MB の上限で削除されるまで残ります。
|
|
165
207
|
- 自分で指定した `clipboard:` のコマンドは、独自のプロセスグループで 5 秒のタイムアウト付きで実行されます。そのため端末の読み取りや設定(パスワード入力、`stty` など)はできません。コピー中の Ctrl-C もそのコマンドには届きません。
|
|
166
208
|
|
|
167
209
|
組み込みのマスク対象: AWS アクセスキー、GitHub / GitLab / Slack / npm / OpenAI・Anthropic(`sk-...`)/ Stripe / Google の API トークン、Slack の webhook URL、JWT、`Authorization` ヘッダーの値、秘密鍵ブロック(END 行がなくても)、`key`・`token`・`secret`・`password`・`passwd`・`pwd` を含む名前に代入された値、`--password <値>`・`curl -u user:pass`・`mysql -p<値>`・`docker login -p <値>` で渡されたパスワード、メールアドレス、URL 中の認証情報、公開 IPv4 アドレス(ループバックとプライベート範囲は対象外)。出力だけでなくコマンドラインにも適用されます。
|
|
@@ -172,15 +214,15 @@ clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52
|
|
|
172
214
|
|
|
173
215
|

|
|
174
216
|
|
|
175
|
-
1. `oops init` は、rc ファイルから `eval`
|
|
217
|
+
1. `oops init` は、rc ファイルから `eval` する(fish では `| source` する)スニペットを出力します。まだ記録されていない端末の対話シェルでは、シェルを `oops record` の下で再起動します。
|
|
176
218
|
2. `oops record` は、Ruby 製の小さな pty 中継です。キー入力とシェルの出力をそのまま通し、ウィンドウのリサイズをシェルに伝えます。
|
|
177
|
-
3. シェルのフック(zsh は `preexec`/`precmd`、bash は `DEBUG` トラップと `PROMPT_COMMAND
|
|
178
|
-
4. コマンドが終わるたびに、中継は 1 行の JSON(コマンド、ディレクトリ、時刻、終了コード、生の出力)をタブごとのログに追記します。置き場所は `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` です。出力は 1 コマンドあたり 1 MB まで(末尾を残す)。各タブのログは 50 MB までで、超えると新しいコマンドだけを残して書き直されます(約 37 MiB
|
|
219
|
+
3. シェルのフック(zsh は `preexec`/`precmd`、bash は `DEBUG` トラップと `PROMPT_COMMAND`、fish は `fish_preexec`/`fish_postexec` イベント)が、各コマンドの前後に目に見えない APC マーカーを出力します。中継がそれを取り除くので、端末には見えません。
|
|
220
|
+
4. コマンドが終わるたびに、中継は 1 行の JSON(コマンド、ディレクトリ、時刻、終了コード、生の出力)をタブごとのログに追記します。置き場所は `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` です。出力は 1 コマンドあたり 1 MB まで(末尾を残す)。各タブのログは 50 MB までで、超えると新しいコマンドだけを残して書き直されます(約 37 MiB まで縮む)。タブが記録を始めるとき、閉じたタブのログのうち `retention_days`(標準は 7 日)を過ぎたものが削除され、続いて残りが古い順に、全ログの合計が 50 MB 以下になるまで削除されます。開いているタブのログは削除されないので、忙しいタブが複数あると合計がそれを超えることがあります。
|
|
179
221
|
5. `oops` は現在のタブのログの末尾を読み、テキストを整えてマスクし、コードブロックを組み立ててクリップボードに渡します。コマンドを再実行することはありません。
|
|
180
222
|
|
|
181
223
|
## プライバシーとセキュリティ
|
|
182
224
|
|
|
183
|
-
- セッションログには、記録されたすべてのコマンドの生の出力が**マスクされないまま**入っています。マスクと置換テーブルはコピーするときにだけ適用されます。ログは `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` にあり、あなただけが読めます(ファイルは 0600、ディレクトリは 0700
|
|
225
|
+
- セッションログには、記録されたすべてのコマンドの生の出力が**マスクされないまま**入っています。マスクと置換テーブルはコピーするときにだけ適用されます。ログは `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` にあり、あなただけが読めます(ファイルは 0600、ディレクトリは 0700)。閉じたタブのログは 7 日後に削除されます(`retention_days`)。`oops clear` ですぐに削除でき、`oops clear --all` なら今のタブのログも空にします。開いている他のタブのログも含めてすべて削除するには `rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/oops"` を実行します(標準では `rm -rf ~/.local/state/oops`)。ディスクに残したくない秘密情報を表示する前には `oops pause` を使ってください。
|
|
184
226
|
- セキュリティ上の問題を非公開で報告する方法は [SECURITY.ja.md](SECURITY.ja.md) を見てください。公開の issue には書かないでください。
|
|
185
227
|
- 記録される出力は信頼できません。実行したプログラムは、oops のマーカーに見せかけたテキストを出力できます。そのため oops は、`oops pause` / `oops resume` が作るフラグファイルと一致する一時停止・再開マーカーにだけ従います。出力によって黙って記録を止められることはありません。ただし、プログラムがログ上でコマンドの開始と終了の位置を偽装することはできるので、貼り付ける内容は確認してください。
|
|
186
228
|
|
|
@@ -194,16 +236,16 @@ clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52
|
|
|
194
236
|
- vim・less・tmux などの全画面アプリの出力は保存されず、`[full-screen app output omitted]` に置き換わります。
|
|
195
237
|
- スペースで始まるコマンドラインは、zsh では記録されますが bash では記録されません(bash はそれを履歴に保存せず、oops はコマンドを履歴から読むため)。
|
|
196
238
|
- [bash-preexec](https://github.com/rcaloras/bash-preexec) を使う bash(atuin や Ghostty の bash 統合の背後にあるものも含む): oops は自前の `DEBUG` トラップを使わず、bash-preexec にフックを登録します。rc ファイルでどちらが先でも同じです。bash が履歴に保存しなかった行(`HISTCONTROL=ignorespace`、`HISTIGNORE`)は、この場合 `(not saved in history)` と記録されます。行のテキストを履歴から読むためです。bash-preexec がない場合、oops はそのような行の最初のコマンドを記録します。
|
|
197
|
-
- Ghostty 上の bash: Ghostty のシェル統合(プロンプトマーク、同じディレクトリでの新しいタブ、タイトル)は外側のシェルにだけ入るため、記録される側のシェルには入りません。足し戻すには、`~/.bashrc` の oops の行の下に次の行を置きます(bash 3.2 で確認済み): `[ -n "$GHOSTTY_RESOURCES_DIR" ] && source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/ghostty.bash"`。zsh は何もしなくて構いません。スニペットがやります。
|
|
239
|
+
- Ghostty 上の bash: Ghostty のシェル統合(プロンプトマーク、同じディレクトリでの新しいタブ、タイトル)は外側のシェルにだけ入るため、記録される側のシェルには入りません。足し戻すには、`~/.bashrc` の oops の行の下に次の行を置きます(bash 3.2 で確認済み): `[ -n "$GHOSTTY_RESOURCES_DIR" ] && source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/ghostty.bash"`。zsh と fish は何もしなくて構いません。スニペットがやります。
|
|
198
240
|
- bash: パイプラインの次の行では、`PIPESTATUS` に最後の終了ステータスしか入りません(`false | true` の後に `echo ${PIPESTATUS[*]}` を実行すると、`1 0` ではなく `0` と表示されます)。oops の `PROMPT_COMMAND` の項目が先に実行されるためです。`$?` と `$_` は保たれます。パイプラインの各ステータスを表示するプロンプト(starship の `pipestatus`)は、bash では表示が少なくなります。zsh は `pipestatus` を保ちます。
|
|
199
241
|
- macOS: `pbcopy` は文字エンコーディングを `LANG` / `LC_*` から決めます。日本語などの非 ASCII 文字が文字化けして届くときは、UTF-8 のロケール(例: `LANG=en_US.UTF-8`)が設定されているか確認してください。SSH 越しで起動したシェルでは設定されていないことがあります。
|
|
200
|
-
-
|
|
242
|
+
- ネイティブ Windows にはまだ対応していません。
|
|
201
243
|
|
|
202
244
|
## 開発
|
|
203
245
|
|
|
204
246
|
### 手元の環境を整える
|
|
205
247
|
|
|
206
|
-
Git、Ruby 3.3 以降、[Bundler](https://bundler.io/)(Ruby に付属)、そして zsh と bash の両方が必要です(エンドツーエンドのテストが本物のシェルを起動するため)。macOS には zsh と bash が入っています。Debian / Ubuntu では `sudo apt-get install zsh` を実行します。
|
|
248
|
+
Git、Ruby 3.3 以降、[Bundler](https://bundler.io/)(Ruby に付属)、そして zsh と bash の両方が必要です(エンドツーエンドのテストが本物のシェルを起動するため)。macOS には zsh と bash が入っています。Debian / Ubuntu では `sudo apt-get install zsh` を実行します。fish はなくても構いません。入っていなければ fish のエンドツーエンドのテストは飛ばされます(CI では入れています)。
|
|
207
249
|
|
|
208
250
|
```sh
|
|
209
251
|
git clone https://github.com/ysb3996-blip/oops.git
|
|
@@ -237,18 +279,18 @@ bundle exec exe/oops doctor
|
|
|
237
279
|
|
|
238
280
|
```sh
|
|
239
281
|
gem build oops-cli.gemspec
|
|
240
|
-
gem install ./oops-cli-0.
|
|
282
|
+
gem install ./oops-cli-0.3.0.gem # ファイル名は lib/oops/version.rb に合わせる
|
|
241
283
|
rbenv rehash # rbenv を使う場合のみ
|
|
242
284
|
```
|
|
243
285
|
|
|
244
|
-
- gem は Ruby のバージョンごとに入ります。新しいタブが、インストールした Ruby とは別の Ruby を使っていると(たとえばリポジトリは 4.0.9 を指定しているのに、標準の Ruby が 3.3.5
|
|
286
|
+
- gem は Ruby のバージョンごとに入ります。新しいタブが、インストールした Ruby とは別の Ruby を使っていると(たとえばリポジトリは 4.0.9 を指定しているのに、標準の Ruby が 3.3.5 の場合)、シェルの起動時に oops が見つからなくなります(bash では `bash: oops: command not found`、zsh では `command not found: oops` と出ます)。その Ruby でも `gem install` を実行してください(例: `RBENV_VERSION=3.3.5 gem install ./oops-cli-0.3.0.gem`)。
|
|
245
287
|
- 変更のたびに入れ直してください。インストールされた gem はコピーで、チェックアウトへのリンクではありません。
|
|
246
288
|
- 削除するには `gem uninstall oops-cli` です。
|
|
247
289
|
- `eval "$(oops init zsh)"` は、プロンプトで打つだけでなく rc ファイルに書いてください。記録されるシェルは rc ファイルを読み直すため、行がないとフックが入りません(`oops doctor` に `hooks: missing` と出ます)。
|
|
248
290
|
|
|
249
291
|
作業中の端末で `oops record` を実行しないでください。テストは自前の pty を通して動かします。試すときは、新しい端末タブを使います。試した結果として残るログを消すには、「プライバシーとセキュリティ」を参照してください。
|
|
250
292
|
|
|
251
|
-
テストは、書式、秘密情報のマスク、ログの読み取り、ストリームパーサー、そして実際の zsh
|
|
293
|
+
テストは、書式、秘密情報のマスク、ログの読み取り、ストリームパーサー、そして実際の zsh・bash・fish を使った中継のエンドツーエンド実行を対象にしています。
|
|
252
294
|
|
|
253
295
|
リリース前の手動チェックリスト:
|
|
254
296
|
|
|
@@ -259,7 +301,7 @@ rbenv rehash # rbenv を使う場合のみ
|
|
|
259
301
|
|
|
260
302
|
## コントリビュート
|
|
261
303
|
|
|
262
|
-
バグ報告、アイデア、プルリクエストを歓迎します。[CONTRIBUTING.ja.md](CONTRIBUTING.ja.md) を読んでください。`good first issue` のラベルが付いた issue
|
|
304
|
+
バグ報告、アイデア、プルリクエストを歓迎します。[CONTRIBUTING.ja.md](CONTRIBUTING.ja.md) を読んでください。`good first issue` のラベルが付いた issue が、始めやすい入口です。プルリクエストには、テストと rubocop の結果、新しいタブでの動作確認を必ず貼ってください([プルリクエストに必ず貼るもの](CONTRIBUTING.ja.md#プルリクエストに必ず貼るもの))。
|
|
263
305
|
|
|
264
306
|
## ライセンス
|
|
265
307
|
|
data/README.md
CHANGED
|
@@ -21,9 +21,41 @@ What lands on your clipboard is a code block with the command, its output, and t
|
|
|
21
21
|
gem install oops-cli
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Homebrew
|
|
24
|
+
Or with Homebrew:
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
```sh
|
|
27
|
+
brew install ysb3996-blip/oops/oops
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The Homebrew formula runs oops with Homebrew's own Ruby, so it works even if an older Ruby comes first on your `PATH`.
|
|
31
|
+
|
|
32
|
+
Requires Ruby 3.3 or newer on macOS or Linux (WSL counts as Linux), and zsh, bash or fish. There are no runtime gem dependencies.
|
|
33
|
+
|
|
34
|
+
`gem install` puts oops into the Ruby that is active at that moment; each Ruby version has its own gems. With rbenv, asdf or mise, a tab that uses another Ruby gives a `command not found` error for `oops`, also when the shell starts. The fix that always works is to run `gem install oops-cli` again under the active Ruby (with rbenv, follow with `rbenv rehash`). Outside directories that pin a Ruby with `.ruby-version`, `.tool-versions` or `mise.toml`, you can instead set the Ruby you installed into as your global Ruby (for example `rbenv global 3.3.5`).
|
|
35
|
+
|
|
36
|
+
### With a zsh plugin manager
|
|
37
|
+
|
|
38
|
+
For zsh you can load oops as a plugin instead. It runs oops from the cloned repository, so there is no gem to install (Ruby 3.3 or newer is still needed), and the plugin replaces the rc line below.
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
# antidote: add to ~/.zsh_plugins.txt
|
|
42
|
+
ysb3996-blip/oops
|
|
43
|
+
|
|
44
|
+
# zinit: in ~/.zshrc, without `wait` (turbo mode)
|
|
45
|
+
zinit light ysb3996-blip/oops
|
|
46
|
+
|
|
47
|
+
# Oh My Zsh: clone, then add oops to plugins=(...) in ~/.zshrc
|
|
48
|
+
git clone https://github.com/ysb3996-blip/oops "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/oops"
|
|
49
|
+
|
|
50
|
+
# No plugin manager: clone, then add the source line to ~/.zshrc
|
|
51
|
+
git clone https://github.com/ysb3996-blip/oops ~/.oops
|
|
52
|
+
source ~/.oops/oops.plugin.zsh
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- Load it right away, not deferred (zinit `wait`, zsh-defer). The plugin starts your shell again under the recorder, which should happen before the first prompt.
|
|
56
|
+
- The notes on where the line goes (below) apply to where the plugin is loaded. With powerlevel10k's instant prompt, the plugin has to be loaded above the instant prompt block, which plugin managers usually are not; use the rc line there.
|
|
57
|
+
- If the Ruby on `PATH` is too old, the shell prints `oops: ... init zsh failed (oops needs Ruby 3.3 or later); recording disabled` and starts as usual, without recording.
|
|
58
|
+
- Plugin managers update from the `main` branch, which can be ahead of the latest release.
|
|
27
59
|
|
|
28
60
|
## Set up your shell
|
|
29
61
|
|
|
@@ -34,14 +66,21 @@ eval "$(oops init zsh)" # zsh
|
|
|
34
66
|
eval "$(oops init bash)" # bash
|
|
35
67
|
```
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
For fish, add this line to `~/.config/fish/config.fish`:
|
|
70
|
+
|
|
71
|
+
```fish
|
|
72
|
+
oops init fish | source
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then open a new tab. Existing tabs pick it up after `exec zsh` (or `exec bash`, `exec fish`).
|
|
38
76
|
|
|
39
77
|
Where to put the line matters:
|
|
40
78
|
|
|
41
79
|
- Put it **after** whatever puts the Ruby that has oops on your `PATH` (rbenv, asdf, mise, Homebrew `shellenv`, and so on). On macOS, login shells reset `PATH`, so if the line comes first, the system Ruby 2.6 may be picked and oops will not start. If oops cannot run, your shell still starts normally, just without recording.
|
|
42
80
|
- Put it **before** slow or noisy setup that you do not want to run twice (nvm, prompt themes, banners). The snippet starts your shell again under a small recorder (`oops record`, a pty relay), so anything above the line runs once in the outer shell and again in the inner one. Anything below it runs only in the inner shell.
|
|
43
81
|
- With powerlevel10k, put the line **above** the instant prompt block at the top of `~/.zshrc`. The instant prompt redirects the shell's input and output while the rest of `~/.zshrc` runs, and the snippet does nothing then (it only acts on a real terminal), so oops would silently not record. If a tab is not recorded although the line is in `~/.zshrc`, run `oops doctor`: it shows `recording: no`.
|
|
44
|
-
- In zsh the shell is restarted as a non-login shell, so setup that lives only in `~/.zlogin` is not applied to the recorded shell. Keep it in `~/.zprofile` or `~/.zshrc` (above the line). In bash, a login shell is restarted with `-l`.
|
|
82
|
+
- In zsh the shell is restarted as a non-login shell, so setup that lives only in `~/.zlogin` is not applied to the recorded shell. Keep it in `~/.zprofile` or `~/.zshrc` (above the line). In bash, a login shell is restarted with `-l`, and in fish with `--login`.
|
|
83
|
+
- fish is restarted just before its first prompt, so all of `config.fish` runs in both the outer and the recorded shell, wherever the line is. (Starting it earlier would also catch `fish -i -c` and `fish -i script.fish`, which never show a prompt and must not be restarted.)
|
|
45
84
|
|
|
46
85
|
If something looks wrong, run `oops doctor`. It reports whether this tab is recorded, the shell and whether the hooks are loaded, which clipboard method will be used, the config file, the size of the session logs, and the Ruby version.
|
|
47
86
|
|
|
@@ -55,10 +94,11 @@ To turn oops off for one shell, start it with `OOPS_DISABLE=1` (for example `OOP
|
|
|
55
94
|
| `oops 3` / `oops -n 3` | Copy the last 3 commands (oldest first) as one code block |
|
|
56
95
|
| `oops err` | Copy the last command that exited non-zero, skipping over successful ones |
|
|
57
96
|
| `oops err -n 2` | Copy the last 2 failed commands (oldest first), skipping over successful ones |
|
|
58
|
-
| `oops init zsh` / `oops init bash` | Print the shell snippet (for `eval`) |
|
|
97
|
+
| `oops init zsh` / `oops init bash` / `oops init fish` | Print the shell snippet (for `eval`, or `\| source` in fish) |
|
|
59
98
|
| `oops doctor` | Diagnose recording, hooks, clipboard, config and log size |
|
|
60
99
|
| `oops pause` / `oops resume` | Stop and restart recording. While paused, nothing is written to disk |
|
|
61
100
|
| `oops status` | Print `recording`, `paused` or `off` on one line, for scripts and prompts |
|
|
101
|
+
| `oops clear` / `oops clear --all` | Delete the logs of closed tabs; `--all` also empties this tab's log |
|
|
62
102
|
| `oops record -- <shell>` | Internal: runs a shell under the recorder. The snippet calls this |
|
|
63
103
|
| `oops --version`, `oops --help` | Version and usage |
|
|
64
104
|
|
|
@@ -156,12 +196,14 @@ replace: # plain string replacements applied when copying
|
|
|
156
196
|
secrets: # extra regular expressions to mask
|
|
157
197
|
- "sk_live_[A-Za-z0-9]+"
|
|
158
198
|
clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52 | any command (reads stdin)
|
|
199
|
+
retention_days: 7 # delete closed tabs' logs after this many days; false keeps them
|
|
159
200
|
```
|
|
160
201
|
|
|
161
202
|
- Replacements and masking are applied at the moment you copy; the recorded log stays untouched, so a config change also affects earlier commands.
|
|
162
203
|
- `replace` keys are applied longest first and are case-sensitive.
|
|
163
204
|
- Unknown keys produce a warning and are ignored. A broken YAML file produces a warning and oops carries on without a config.
|
|
164
205
|
- With `clipboard: auto`, oops tries `pbcopy` on macOS, `clip.exe` on WSL, `wl-copy` on Wayland, `xclip` or `xsel` on X11, and falls back to the OSC 52 terminal escape sequence (also used over SSH). `oops doctor` shows which one it picked.
|
|
206
|
+
- `retention_days` (default 7) counts from the last time a closed tab wrote its log. Old logs are deleted when a tab starts recording. With `false`, closed tabs' logs stay until the 50 MB limit removes them.
|
|
165
207
|
- A custom `clipboard:` command runs in its own process group with a 5 second timeout, so it must not read from or configure the terminal (no password prompts, no `stty`); Ctrl-C during a copy does not reach it.
|
|
166
208
|
|
|
167
209
|
Built-in masking covers AWS access keys, GitHub, GitLab, Slack, npm, OpenAI/Anthropic (`sk-...`), Stripe and Google API tokens, Slack webhook URLs, JWTs, `Authorization` header values, private key blocks (also when the END line is missing), values assigned to names containing `key`, `token`, `secret`, `password`, `passwd` or `pwd`, passwords given as `--password <value>`, `curl -u user:pass`, `mysql -p<value>` or `docker login -p <value>`, email addresses, credentials inside URLs, and public IPv4 addresses (loopback and private ranges are left alone). It is applied to the command line as well as the output.
|
|
@@ -172,15 +214,15 @@ Masking is pattern based, so it is a safety net, not a guarantee: a secret in a
|
|
|
172
214
|
|
|
173
215
|

|
|
174
216
|
|
|
175
|
-
1. `oops init` prints a snippet that you `eval` from your rc file. In an interactive shell on a terminal that is not recorded yet, it restarts the shell under `oops record`.
|
|
217
|
+
1. `oops init` prints a snippet that you `eval` from your rc file (`| source` in fish). In an interactive shell on a terminal that is not recorded yet, it restarts the shell under `oops record`.
|
|
176
218
|
2. `oops record` is a small Ruby pty relay: it passes your keystrokes and the shell's output through untouched and forwards window resizes to the shell.
|
|
177
|
-
3. Hooks in the shell (zsh `preexec`/`precmd`, bash `DEBUG` trap and `PROMPT_COMMAND`) emit invisible APC markers around each command. The relay strips them, so your terminal never sees them.
|
|
178
|
-
4. For each finished command the relay appends one JSON line (command, directory, times, exit code, raw output) to a per-tab log under `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/`. Output is capped at 1 MB per command (the tail is kept). Each tab's log is capped at 50 MB: when it passes that, it is rewritten to keep only its newest commands (down to about 37 MiB). When a tab starts recording, logs of closed tabs are deleted, oldest
|
|
219
|
+
3. Hooks in the shell (zsh `preexec`/`precmd`, bash `DEBUG` trap and `PROMPT_COMMAND`, fish `fish_preexec`/`fish_postexec` events) emit invisible APC markers around each command. The relay strips them, so your terminal never sees them.
|
|
220
|
+
4. For each finished command the relay appends one JSON line (command, directory, times, exit code, raw output) to a per-tab log under `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/`. Output is capped at 1 MB per command (the tail is kept). Each tab's log is capped at 50 MB: when it passes that, it is rewritten to keep only its newest commands (down to about 37 MiB). When a tab starts recording, logs of closed tabs older than `retention_days` (7 by default) are deleted, and then the oldest of the rest until all logs together take at most 50 MB; logs of open tabs are never deleted, so several busy tabs can together use more.
|
|
179
221
|
5. `oops` reads the end of the current tab's log, cleans and masks the text, builds the code block and hands it to the clipboard. Nothing is ever re-run.
|
|
180
222
|
|
|
181
223
|
## Privacy and security
|
|
182
224
|
|
|
183
|
-
- The session logs hold the raw, **unmasked** output of every recorded command. Masking and the replace table are applied only when you copy. The logs live under `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/`, readable only by you (mode 0600, directory 0700). To delete
|
|
225
|
+
- The session logs hold the raw, **unmasked** output of every recorded command. Masking and the replace table are applied only when you copy. The logs live under `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/`, readable only by you (mode 0600, directory 0700). Logs of closed tabs are deleted after 7 days (`retention_days`). `oops clear` deletes them now, and `oops clear --all` also empties the current tab's log. To delete everything, including the logs of other open tabs, run `rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/oops"` (by default `rm -rf ~/.local/state/oops`). Use `oops pause` before showing secrets you do not want on disk.
|
|
184
226
|
- To report a security problem privately, see [SECURITY.md](SECURITY.md). Please do not open a public issue for it.
|
|
185
227
|
- Recorded output is untrusted: any program you run can print text that looks like oops's markers. oops therefore obeys a pause or resume marker only when it agrees with the flag file that `oops pause` and `oops resume` set, so output cannot silently pause recording. A program can still fake where a command starts and ends in the log, so check what you paste.
|
|
186
228
|
|
|
@@ -194,16 +236,16 @@ Masking is pattern based, so it is a safety net, not a guarantee: a secret in a
|
|
|
194
236
|
- Output of full-screen apps (vim, less, tmux and so on) is not kept; it is replaced by `[full-screen app output omitted]`.
|
|
195
237
|
- A command line that starts with a space is recorded in zsh but dropped in bash (bash does not save it to history, which is where oops reads the command).
|
|
196
238
|
- bash with [bash-preexec](https://github.com/rcaloras/bash-preexec) (also behind atuin and Ghostty's bash integration): oops registers its hooks with it instead of using its own `DEBUG` trap, whichever of the two comes first in your rc file. A line that bash did not save to history (`HISTCONTROL=ignorespace`, `HISTIGNORE`) is then recorded as `(not saved in history)`, because the text of the line is read from history there. Without bash-preexec, oops records the first command of such a line instead.
|
|
197
|
-
- bash in Ghostty: Ghostty adds its shell integration (prompt marks, new tabs in the same directory, titles) only to the outer shell, so the recorded shell does not get it. To add it back, put this below the oops line in `~/.bashrc` (checked with bash 3.2): `[ -n "$GHOSTTY_RESOURCES_DIR" ] && source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/ghostty.bash"`. zsh
|
|
239
|
+
- bash in Ghostty: Ghostty adds its shell integration (prompt marks, new tabs in the same directory, titles) only to the outer shell, so the recorded shell does not get it. To add it back, put this below the oops line in `~/.bashrc` (checked with bash 3.2): `[ -n "$GHOSTTY_RESOURCES_DIR" ] && source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/ghostty.bash"`. zsh and fish need nothing: the snippet does it.
|
|
198
240
|
- bash: the line after a pipeline sees `PIPESTATUS` with only the last status (`false | true`, then `echo ${PIPESTATUS[*]}` prints `0`, not `1 0`), because oops's `PROMPT_COMMAND` entry runs first. `$?` and `$_` are kept. A prompt that shows pipeline statuses (starship's `pipestatus`) shows less in bash. zsh keeps `pipestatus`.
|
|
199
241
|
- macOS: `pbcopy` chooses its text encoding from `LANG` / `LC_*`. If Japanese or other non-ASCII text arrives garbled, check that a UTF-8 locale is set (for example `LANG=en_US.UTF-8`), which can be missing in shells started over SSH.
|
|
200
|
-
-
|
|
242
|
+
- Native Windows is not supported yet.
|
|
201
243
|
|
|
202
244
|
## Development
|
|
203
245
|
|
|
204
246
|
### Set up your machine
|
|
205
247
|
|
|
206
|
-
You need Git, Ruby 3.3 or newer, [Bundler](https://bundler.io/) (it ships with Ruby), and both zsh and bash (the end-to-end tests start real shells). macOS has zsh and bash; on Debian or Ubuntu run `sudo apt-get install zsh`.
|
|
248
|
+
You need Git, Ruby 3.3 or newer, [Bundler](https://bundler.io/) (it ships with Ruby), and both zsh and bash (the end-to-end tests start real shells). macOS has zsh and bash; on Debian or Ubuntu run `sudo apt-get install zsh`. fish is optional: its end-to-end tests are skipped when it is not installed (CI installs it).
|
|
207
249
|
|
|
208
250
|
```sh
|
|
209
251
|
git clone https://github.com/ysb3996-blip/oops.git
|
|
@@ -237,18 +279,18 @@ To try it as your shell does, build and install the gem into the Ruby you are us
|
|
|
237
279
|
|
|
238
280
|
```sh
|
|
239
281
|
gem build oops-cli.gemspec
|
|
240
|
-
gem install ./oops-cli-0.
|
|
282
|
+
gem install ./oops-cli-0.3.0.gem # the file name follows lib/oops/version.rb
|
|
241
283
|
rbenv rehash # only with rbenv
|
|
242
284
|
```
|
|
243
285
|
|
|
244
|
-
- A gem is installed per Ruby version. If a new tab uses another Ruby than the one you installed into (for example the repository pins 4.0.9, but your global Ruby is 3.3.5), the shell prints `oops: command not found
|
|
286
|
+
- A gem is installed per Ruby version. If a new tab uses another Ruby than the one you installed into (for example the repository pins 4.0.9, but your global Ruby is 3.3.5), the shell cannot find oops when it starts (bash prints `bash: oops: command not found`, zsh prints `command not found: oops`). Run `gem install` again under that Ruby, e.g. `RBENV_VERSION=3.3.5 gem install ./oops-cli-0.3.0.gem`.
|
|
245
287
|
- Reinstall after every change. The installed gem is a copy, not a link to your checkout.
|
|
246
288
|
- To remove it: `gem uninstall oops-cli`.
|
|
247
289
|
- Put `eval "$(oops init zsh)"` in your rc file, not just at the prompt: the recorded shell reads the rc file again, and without the line it has no hooks (`oops doctor` shows `hooks: missing`).
|
|
248
290
|
|
|
249
291
|
Never run `oops record` in the terminal you are working in; the tests drive it through their own pty. Try it in a new terminal tab instead. To delete the logs your experiments leave behind, see "Privacy and security".
|
|
250
292
|
|
|
251
|
-
The tests cover formatting, secret masking, the log reader, the stream parser and an end-to-end run of the relay with real zsh and
|
|
293
|
+
The tests cover formatting, secret masking, the log reader, the stream parser and an end-to-end run of the relay with real zsh, bash and fish.
|
|
252
294
|
|
|
253
295
|
Manual checklist before a release:
|
|
254
296
|
|
|
@@ -259,7 +301,7 @@ Manual checklist before a release:
|
|
|
259
301
|
|
|
260
302
|
## Contributing
|
|
261
303
|
|
|
262
|
-
Bug reports, ideas and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md); issues labeled `good first issue` are a good place to start.
|
|
304
|
+
Bug reports, ideas and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md); issues labeled `good first issue` are a good place to start. Every pull request must include the test and rubocop results and a check in a new terminal tab; see [What every pull request must include](CONTRIBUTING.md#what-every-pull-request-must-include).
|
|
263
305
|
|
|
264
306
|
## License
|
|
265
307
|
|
data/lib/oops/cli.rb
CHANGED
|
@@ -22,13 +22,14 @@ module Oops
|
|
|
22
22
|
when "doctor" then return doctor(argv.drop(1), env: env, stdin: stdin, stdout: stdout)
|
|
23
23
|
when "pause", "resume" then return toggle(argv.first, argv.drop(1), env: env, stdout: stdout)
|
|
24
24
|
when "status" then return status(argv.drop(1), env: env, stdout: stdout, stderr: stderr)
|
|
25
|
+
when "clear" then return clear(argv.drop(1), env: env, stdout: stdout)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
options = { head: nil, tail: nil, github: false, show_secrets: false, time: false, output_only: false }
|
|
28
29
|
count = nil
|
|
29
30
|
parser = OptionParser.new do |o|
|
|
30
31
|
o.banner = "Usage: oops [N | err] [-n N] [options]\n " \
|
|
31
|
-
"oops init <shell> | pause | resume | status | doctor"
|
|
32
|
+
"oops init <shell> | pause | resume | status | clear [--all] | doctor"
|
|
32
33
|
o.on("-n N", "Copy the last N commands (with err, last N failures)") do |v|
|
|
33
34
|
raise UsageError, COUNT_REPEATED if count
|
|
34
35
|
|
|
@@ -117,8 +118,12 @@ module Oops
|
|
|
117
118
|
path = env["OOPS_SESSION"].to_s
|
|
118
119
|
return SessionLog.new(path) if !path.empty? && File.file?(path)
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
line, rc = case env["SHELL"].to_s
|
|
122
|
+
when /bash\z/ then [%(eval "$(oops init bash)"), "~/.bashrc"]
|
|
123
|
+
when /fish\z/ then ["oops init fish | source", "~/.config/fish/config.fish"]
|
|
124
|
+
else [%(eval "$(oops init zsh)"), "~/.zshrc"]
|
|
125
|
+
end
|
|
126
|
+
raise Failure, "not recording in this shell. Add #{line} " \
|
|
122
127
|
"to #{rc} (after whatever puts oops's Ruby on PATH, as early as possible) " \
|
|
123
128
|
"and open a new tab."
|
|
124
129
|
end
|
|
@@ -176,6 +181,25 @@ module Oops
|
|
|
176
181
|
word == "recording" ? 0 : 1
|
|
177
182
|
end
|
|
178
183
|
|
|
184
|
+
# `oops clear [--all]`: deletes the logs of closed tabs; with --all, also
|
|
185
|
+
# empties this tab's log. Logs of other open tabs are never touched.
|
|
186
|
+
def self.clear(args, env:, stdout:)
|
|
187
|
+
raise UsageError, "usage: oops clear [--all]" unless args.empty? || args == ["--all"]
|
|
188
|
+
|
|
189
|
+
log = recording_log(env) if args == ["--all"]
|
|
190
|
+
begin
|
|
191
|
+
removed = SessionLog.clear!(Paths.sessions_dir(env: env))
|
|
192
|
+
stdout.puts "removed #{removed} #{removed == 1 ? 'log' : 'logs'} of closed tabs"
|
|
193
|
+
return 0 unless log
|
|
194
|
+
|
|
195
|
+
log.empty!
|
|
196
|
+
rescue SystemCallError => e
|
|
197
|
+
raise Failure, "could not delete the logs: #{e.message}"
|
|
198
|
+
end
|
|
199
|
+
stdout.puts "emptied this tab's log"
|
|
200
|
+
0
|
|
201
|
+
end
|
|
202
|
+
|
|
179
203
|
# `oops doctor`: diagnostics on stdout; always exits 0.
|
|
180
204
|
def self.doctor(args, env:, stdin:, stdout:)
|
|
181
205
|
raise UsageError, "usage: oops doctor" unless args.empty?
|
|
@@ -197,7 +221,7 @@ module Oops
|
|
|
197
221
|
Recorder.new(args, env: env, stdin: stdin, stdout: stdout, stderr: stderr).run
|
|
198
222
|
end
|
|
199
223
|
|
|
200
|
-
# `oops init zsh|bash`: prints the snippet for `eval "$(oops init zsh)"`.
|
|
224
|
+
# `oops init zsh|bash|fish`: prints the snippet for `eval "$(oops init zsh)"`.
|
|
201
225
|
def self.init(args, stdout:, stderr:)
|
|
202
226
|
supported = ShellInit::SHELLS.join(", ")
|
|
203
227
|
if args.size != 1
|
|
@@ -213,6 +237,6 @@ module Oops
|
|
|
213
237
|
end
|
|
214
238
|
|
|
215
239
|
private_class_method :positive, :target, :copy, :report_command, :report_counts, :recording_log, :toggle,
|
|
216
|
-
:write_marker, :status, :doctor
|
|
240
|
+
:write_marker, :status, :clear, :doctor
|
|
217
241
|
end
|
|
218
242
|
end
|
data/lib/oops/config.rb
CHANGED
|
@@ -3,9 +3,10 @@ require_relative "paths"
|
|
|
3
3
|
module Oops
|
|
4
4
|
# User configuration loaded from a YAML file; problems become warnings.
|
|
5
5
|
class Config
|
|
6
|
-
KNOWN_KEYS = %w[replace secrets clipboard].freeze
|
|
6
|
+
KNOWN_KEYS = %w[replace secrets clipboard retention_days].freeze
|
|
7
|
+
DEFAULT_RETENTION_DAYS = 7
|
|
7
8
|
|
|
8
|
-
attr_reader :replace, :secrets, :clipboard, :warnings
|
|
9
|
+
attr_reader :replace, :secrets, :clipboard, :retention_days, :warnings
|
|
9
10
|
|
|
10
11
|
def self.load(path = nil, env: ENV)
|
|
11
12
|
new(path || Paths.config_file(env: env))
|
|
@@ -15,6 +16,7 @@ module Oops
|
|
|
15
16
|
@replace = {}
|
|
16
17
|
@secrets = []
|
|
17
18
|
@clipboard = "auto"
|
|
19
|
+
@retention_days = DEFAULT_RETENTION_DAYS
|
|
18
20
|
@warnings = []
|
|
19
21
|
read(path)
|
|
20
22
|
end
|
|
@@ -41,6 +43,7 @@ module Oops
|
|
|
41
43
|
apply_replace(data["replace"]) if data.key?("replace")
|
|
42
44
|
apply_secrets(data["secrets"]) if data.key?("secrets")
|
|
43
45
|
apply_clipboard(data["clipboard"]) if data.key?("clipboard")
|
|
46
|
+
apply_retention_days(data["retention_days"]) if data.key?("retention_days")
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
# YAML reads `clipboard: true` (or false) as a boolean; as a command it
|
|
@@ -50,6 +53,15 @@ module Oops
|
|
|
50
53
|
@clipboard = text.empty? || %w[true false].include?(text) ? "auto" : value.to_s
|
|
51
54
|
end
|
|
52
55
|
|
|
56
|
+
# false keeps closed tabs' logs until the size cap removes them.
|
|
57
|
+
def apply_retention_days(value)
|
|
58
|
+
return @retention_days = nil if value == false
|
|
59
|
+
return @retention_days = value if value.is_a?(Integer) && value >= 1
|
|
60
|
+
|
|
61
|
+
@warnings << "retention_days must be a whole number of days (at least 1) or false; " \
|
|
62
|
+
"using #{DEFAULT_RETENTION_DAYS}"
|
|
63
|
+
end
|
|
64
|
+
|
|
53
65
|
def apply_replace(value)
|
|
54
66
|
return @warnings << "replace must be a mapping; ignored" unless value.is_a?(Hash)
|
|
55
67
|
|
data/lib/oops/recorder.rb
CHANGED
|
@@ -172,7 +172,8 @@ module Oops
|
|
|
172
172
|
|
|
173
173
|
# Rotation only frees space; failing to rotate must not stop recording.
|
|
174
174
|
def rotate_logs(dir)
|
|
175
|
-
|
|
175
|
+
days = Config.load(env: @env).retention_days
|
|
176
|
+
SessionLog.rotate!(dir, max_age: days && (days * 86_400))
|
|
176
177
|
rescue SystemCallError
|
|
177
178
|
nil
|
|
178
179
|
end
|
data/lib/oops/session_log.rb
CHANGED
|
@@ -25,19 +25,19 @@ module Oops
|
|
|
25
25
|
false
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Deletes
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
# Deletes logs of finished sessions: with max_age, those last written more
|
|
29
|
+
# than max_age seconds ago; then the oldest, until the total size of all
|
|
30
|
+
# session logs is at most max_bytes. Logs of live sessions are kept.
|
|
31
|
+
def self.rotate!(dir, max_bytes: DEFAULT_MAX_BYTES, max_age: nil, now: Time.now,
|
|
32
|
+
alive: method(:process_alive?))
|
|
33
|
+
logs = list(dir)
|
|
34
|
+
if max_age
|
|
35
|
+
logs = logs.reject do |log|
|
|
36
|
+
next false if log[:mtime] >= now - max_age || alive.call(log[:pid])
|
|
37
|
+
|
|
38
|
+
remove_log(File.join(dir, log[:name]))
|
|
39
|
+
true
|
|
39
40
|
end
|
|
40
|
-
{ name: name, pid: m[2].to_i, size: size }
|
|
41
41
|
end
|
|
42
42
|
total = logs.sum { |l| l[:size] }
|
|
43
43
|
logs.sort_by { |l| l[:name] }.each do |log|
|
|
@@ -51,6 +51,29 @@ module Oops
|
|
|
51
51
|
nil
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Deletes the logs of all finished sessions. Returns how many it deleted.
|
|
55
|
+
def self.clear!(dir, alive: method(:process_alive?))
|
|
56
|
+
dead = list(dir).reject { |log| alive.call(log[:pid]) }
|
|
57
|
+
dead.each { |log| remove_log(File.join(dir, log[:name])) }
|
|
58
|
+
dead.size
|
|
59
|
+
rescue Errno::ENOENT
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.list(dir)
|
|
64
|
+
Dir.children(dir).filter_map do |name|
|
|
65
|
+
m = LOG_NAME.match(name)
|
|
66
|
+
next unless m
|
|
67
|
+
|
|
68
|
+
stat = begin
|
|
69
|
+
File.stat(File.join(dir, name))
|
|
70
|
+
rescue SystemCallError
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
{ name: name, pid: m[2].to_i, size: stat.size, mtime: stat.mtime }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
54
77
|
def self.remove_log(file)
|
|
55
78
|
[file, "#{file}.paused", "#{file}.status", "#{file}.trim"].each do |f|
|
|
56
79
|
File.delete(f)
|
|
@@ -58,7 +81,7 @@ module Oops
|
|
|
58
81
|
nil
|
|
59
82
|
end
|
|
60
83
|
end
|
|
61
|
-
private_class_method :remove_log
|
|
84
|
+
private_class_method :list, :remove_log
|
|
62
85
|
|
|
63
86
|
def initialize(path, max_bytes: DEFAULT_MAX_BYTES)
|
|
64
87
|
@path = path
|
|
@@ -125,6 +148,12 @@ module Oops
|
|
|
125
148
|
nil
|
|
126
149
|
end
|
|
127
150
|
|
|
151
|
+
# The recorder opens the log for each record it appends, so the file can
|
|
152
|
+
# be cut to nothing while the session goes on.
|
|
153
|
+
def empty!
|
|
154
|
+
File.truncate(@path, 0)
|
|
155
|
+
end
|
|
156
|
+
|
|
128
157
|
private
|
|
129
158
|
|
|
130
159
|
def pause_path
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# oops: fish hooks. Put this line in ~/.config/fish/config.fish, after
|
|
2
|
+
# whatever puts oops's Ruby on PATH and as early as possible after that:
|
|
3
|
+
#
|
|
4
|
+
# oops init fish | source
|
|
5
|
+
#
|
|
6
|
+
# On a terminal that is not recorded yet, the shell is started again under
|
|
7
|
+
# `oops record` (a pty relay). Inside the recorded shell the hooks write
|
|
8
|
+
# invisible APC markers around every command:
|
|
9
|
+
#
|
|
10
|
+
# ESC _ oops;start;<seq>;<epoch>;<base64 cwd>;<base64 cmd> ESC \
|
|
11
|
+
# ESC _ oops;end;<seq>;<exit> ESC \
|
|
12
|
+
#
|
|
13
|
+
# "Recorded" means $OOPS_TTY (set by the relay) names this very terminal, so
|
|
14
|
+
# shells started from a recorded one on another tty (tmux, GUI terminals)
|
|
15
|
+
# get recorded on their own, and `exec fish` on the same tty is not recorded
|
|
16
|
+
# twice. Markers go straight to the terminal device, so a redirected stdout
|
|
17
|
+
# never receives them.
|
|
18
|
+
# This file is read from a pipe (`| source`), so stdin is not the terminal:
|
|
19
|
+
# the checks look at stdout and stderr instead.
|
|
20
|
+
if status is-interactive; and test -t 1; and test -t 2; and not set -q OOPS_DISABLE
|
|
21
|
+
and set -g __oops_tty (tty <&2 2>/dev/null)
|
|
22
|
+
set -g __oops_bin __OOPS_BIN__
|
|
23
|
+
if test "$OOPS_TTY" != "$__oops_tty"
|
|
24
|
+
# A failed exec ends fish, so check first; without the binary the
|
|
25
|
+
# shell simply runs unrecorded.
|
|
26
|
+
if command -q $__oops_bin
|
|
27
|
+
set -g __oops_login
|
|
28
|
+
status is-login; and set __oops_login --login
|
|
29
|
+
# Ghostty injects its shell integration through XDG_DATA_DIRS and
|
|
30
|
+
# removes the injection again, so the restarted shell would not
|
|
31
|
+
# get it. Note whether this one has it (its setup function waits
|
|
32
|
+
# for the first prompt), so the restarted shell can load it again.
|
|
33
|
+
set -g __oops_ghostty
|
|
34
|
+
functions -q __ghostty_setup; and set __oops_ghostty 1
|
|
35
|
+
# Restart at the first prompt, not now: `fish -i -c cmd` and
|
|
36
|
+
# `fish -i script.fish` count as interactive here but never show a
|
|
37
|
+
# prompt, so they run their command unrecorded instead of losing it.
|
|
38
|
+
function __oops_restart --on-event fish_prompt
|
|
39
|
+
functions -e __oops_restart
|
|
40
|
+
if test -n "$__oops_ghostty"
|
|
41
|
+
set -gx __OOPS_GHOSTTY 1
|
|
42
|
+
else
|
|
43
|
+
set -e __OOPS_GHOSTTY
|
|
44
|
+
end
|
|
45
|
+
exec $__oops_bin record -- (status fish-path) $__oops_login
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
echo "oops: cannot run $__oops_bin; recording disabled" >&2
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
function __oops_b64
|
|
52
|
+
printf '%s' $argv[1] | base64 | tr -d '\n'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# $argv[1] is the whole command line, several lines for a block. Very
|
|
56
|
+
# long lines are cut so the marker stays small. A line of spaces runs
|
|
57
|
+
# nothing, and the other shells make no record for it either.
|
|
58
|
+
function __oops_preexec --on-event fish_preexec
|
|
59
|
+
string trim -- $argv[1] | string length -q; or return
|
|
60
|
+
set -gx __OOPS_SEQ (math $__OOPS_SEQ + 1)
|
|
61
|
+
set -g __oops_open $__OOPS_SEQ
|
|
62
|
+
set -l cmd (string sub -l 8192 -- $argv[1] | string collect)
|
|
63
|
+
printf '\e_oops;start;%s;%s;%s;%s\e\\' $__OOPS_SEQ (date +%s) \
|
|
64
|
+
(__oops_b64 $PWD) (__oops_b64 "$cmd") 2>/dev/null >$__oops_tty
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# $status must be read before anything else runs.
|
|
68
|
+
function __oops_postexec --on-event fish_postexec
|
|
69
|
+
set -l exit_status $status
|
|
70
|
+
if test -n "$__oops_open"
|
|
71
|
+
printf '\e_oops;end;%s;%s\e\\' $__oops_open $exit_status 2>/dev/null >$__oops_tty
|
|
72
|
+
set -g __oops_open
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Once per shell process; sourcing config.fish again must not redo this.
|
|
77
|
+
if not set -q __oops_hooked
|
|
78
|
+
set -g __oops_hooked 1
|
|
79
|
+
set -g __oops_open
|
|
80
|
+
string match -qr '^[0-9]+$' -- "$__OOPS_SEQ"; or set -gx __OOPS_SEQ 0
|
|
81
|
+
# Same pid as the shell that set __OOPS_PID: this shell replaced it
|
|
82
|
+
# with `exec`, so that command (still open in the relay) is done.
|
|
83
|
+
if test "$__OOPS_PID" = "$fish_pid"; and test "$__OOPS_SEQ" != 0
|
|
84
|
+
printf '\e_oops;end;%s;%s\e\\' $__OOPS_SEQ 0 2>/dev/null >$__oops_tty
|
|
85
|
+
end
|
|
86
|
+
# Exported so a nested shell on this tty continues the numbering.
|
|
87
|
+
set -gx __OOPS_SEQ $__OOPS_SEQ
|
|
88
|
+
set -gx __OOPS_PID $fish_pid
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The shell this one replaced had Ghostty's integration (see above);
|
|
92
|
+
# load it here too, as Ghostty documents for a manual setup.
|
|
93
|
+
set -l ghostty "$GHOSTTY_RESOURCES_DIR/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish"
|
|
94
|
+
if set -q __OOPS_GHOSTTY; and test -n "$GHOSTTY_RESOURCES_DIR"; and test -r $ghostty
|
|
95
|
+
and not functions -q __ghostty_setup
|
|
96
|
+
source $ghostty
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/oops/shell_init.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module Oops
|
|
2
|
-
# The snippets printed by `oops init zsh|bash`, meant for
|
|
3
|
-
# `eval "$(oops init zsh)"` in the rc file,
|
|
4
|
-
# on PATH and as early as possible after
|
|
5
|
-
# lib/oops/shell/; the absolute path of the oops
|
|
6
|
-
# them, so a later change to PATH cannot break
|
|
2
|
+
# The snippets printed by `oops init zsh|bash|fish`, meant for
|
|
3
|
+
# `eval "$(oops init zsh)"` (or `oops init fish | source`) in the rc file,
|
|
4
|
+
# after whatever puts oops's Ruby on PATH and as early as possible after
|
|
5
|
+
# that. The templates live in lib/oops/shell/; the absolute path of the oops
|
|
6
|
+
# executable is written into them, so a later change to PATH cannot break
|
|
7
|
+
# the hooks.
|
|
7
8
|
module ShellInit
|
|
8
|
-
SHELLS = %w[zsh bash].freeze
|
|
9
|
+
SHELLS = %w[zsh bash fish].freeze
|
|
9
10
|
TEMPLATE_DIR = File.expand_path("shell", __dir__)
|
|
10
11
|
BIN_PLACEHOLDER = "__OOPS_BIN__".freeze
|
|
11
12
|
|
|
@@ -13,14 +14,19 @@ module Oops
|
|
|
13
14
|
raise ArgumentError, "unsupported shell: #{shell}" unless SHELLS.include?(shell)
|
|
14
15
|
|
|
15
16
|
template = File.read(File.join(TEMPLATE_DIR, "oops.#{shell}"), encoding: Encoding::UTF_8)
|
|
16
|
-
template.gsub(BIN_PLACEHOLDER) { shell_quote(bin) }
|
|
17
|
+
template.gsub(BIN_PLACEHOLDER) { shell == "fish" ? fish_quote(bin) : shell_quote(bin) }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
# One single-quoted word for
|
|
20
|
+
# One single-quoted word for zsh and bash: ' becomes '\''.
|
|
20
21
|
def self.shell_quote(text)
|
|
21
22
|
"'#{text.to_s.gsub("'") { "'\\''" }}'"
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
# fish single quotes take \' and \\ instead.
|
|
26
|
+
def self.fish_quote(text)
|
|
27
|
+
"'#{text.to_s.gsub(/[\\']/) { "\\#{_1}" }}'"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private_class_method :shell_quote, :fish_quote
|
|
25
31
|
end
|
|
26
32
|
end
|
data/lib/oops/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oops-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ysb3996-blip
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- lib/oops/secrets.rb
|
|
41
41
|
- lib/oops/session_log.rb
|
|
42
42
|
- lib/oops/shell/oops.bash
|
|
43
|
+
- lib/oops/shell/oops.fish
|
|
43
44
|
- lib/oops/shell/oops.zsh
|
|
44
45
|
- lib/oops/shell_init.rb
|
|
45
46
|
- lib/oops/stream_parser.rb
|