oops-cli 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.ja.md +14 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +21 -0
- data/README.ja.md +236 -0
- data/README.md +236 -0
- data/exe/oops +5 -0
- data/lib/oops/ansi.rb +52 -0
- data/lib/oops/cli.rb +186 -0
- data/lib/oops/clipboard.rb +218 -0
- data/lib/oops/config.rb +72 -0
- data/lib/oops/doctor.rb +133 -0
- data/lib/oops/formatter.rb +115 -0
- data/lib/oops/marker.rb +88 -0
- data/lib/oops/paths.rb +28 -0
- data/lib/oops/prompt_sp.rb +74 -0
- data/lib/oops/record.rb +41 -0
- data/lib/oops/recorder.rb +416 -0
- data/lib/oops/secrets.rb +85 -0
- data/lib/oops/session_log.rb +213 -0
- data/lib/oops/shell/oops.bash +336 -0
- data/lib/oops/shell/oops.zsh +102 -0
- data/lib/oops/shell_init.rb +26 -0
- data/lib/oops/stream_parser.rb +347 -0
- data/lib/oops/tty.rb +19 -0
- data/lib/oops/version.rb +3 -0
- data/lib/oops.rb +17 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 238211aa8b381cf9998c6a744627d61d276be3350aa273c9d7fbdba8c078125a
|
|
4
|
+
data.tar.gz: 1858aff54bd54535e3aa2ebc5b5a0c18b4f6168572c1e55e1dd0d13b97f31fdd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dcae845a6bc939a775258eba59a360dfff475dc91af52be47d7495eea915de21cd04ae1c82afc1ea157df0db3e84b0b576e029d1731e6347c3faa429cd18e0d9
|
|
7
|
+
data.tar.gz: 14472ae5e385e10142be06be2264eff1d828415fca08388fba48d66d8a7c258b64f8bde75da0f86f94b2ef511cd2b331de972563267f597595aadb7eac074d9c
|
data/CHANGELOG.ja.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 変更履歴
|
|
2
|
+
|
|
3
|
+
[English](CHANGELOG.md) | 日本語
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-10-30
|
|
6
|
+
|
|
7
|
+
最初のバージョン。
|
|
8
|
+
|
|
9
|
+
- `oops`、`oops N`、`oops err` で、直前のコマンドとその出力を Markdown のコードブロックとしてクリップボードにコピーする。
|
|
10
|
+
- `--github`、`--tail`、`--head`、`--show-secrets` オプション。
|
|
11
|
+
- 秘密情報(トークン、鍵、メールアドレス、URL 中の認証情報、公開 IPv4 アドレス、コマンドラインオプションで渡されたパスワード)は、出力だけでなくコマンドラインでも、標準でマスクされる。
|
|
12
|
+
- 置換テーブル、追加の秘密情報パターン、クリップボードの上書きを持つ設定ファイル。
|
|
13
|
+
- `oops init zsh|bash` による zsh と bash への対応。小さな pty 中継(`oops record`)で記録する。
|
|
14
|
+
- `oops pause`、`oops resume`、`oops doctor`。
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
English | [日本語](CHANGELOG.ja.md)
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-10-30
|
|
6
|
+
|
|
7
|
+
First version.
|
|
8
|
+
|
|
9
|
+
- `oops`, `oops N` and `oops err` copy the last command(s) and their output to the clipboard as a Markdown code block.
|
|
10
|
+
- `--github`, `--tail`, `--head` and `--show-secrets` options.
|
|
11
|
+
- Secrets (tokens, keys, emails, URL credentials, public IPv4 addresses, passwords given as command-line options) are masked by default, in the command line as well as the output.
|
|
12
|
+
- Config file with a replace table, extra secret patterns and a clipboard override.
|
|
13
|
+
- zsh and bash support through `oops init zsh|bash`, recorded by a small pty relay (`oops record`).
|
|
14
|
+
- `oops pause`, `oops resume` and `oops doctor`.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ysb3996-blip
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.ja.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# oops
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 日本語
|
|
4
|
+
|
|
5
|
+
直前のシェルコマンドとその出力を、貼り付けられる形でクリップボードにコピーします。
|
|
6
|
+
|
|
7
|
+
コマンドを実行して失敗した。その結果をチャットのアシスタントや Slack の同僚、GitHub の issue に見せたい。普通は、出力をドラッグで選択してコピーし、貼り付けて、コードフェンスを自分で付ける、という手間がかかります。oops なら 2 ステップです。
|
|
8
|
+
|
|
9
|
+
1. `oops`(直前の失敗したコマンドなら `oops err`)と打つ。
|
|
10
|
+
2. Cmd-V で貼り付ける。
|
|
11
|
+
|
|
12
|
+
クリップボードに入るのは、コマンド・出力・終了コードをまとめたコードブロックです。色は取り除かれ、トークン・鍵・メールアドレスはマスクされます。
|
|
13
|
+
|
|
14
|
+
## インストール
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install oops-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Homebrew tap: 準備中です。
|
|
21
|
+
|
|
22
|
+
macOS または Linux(WSL は Linux 扱い)、Ruby 3.3 以降、zsh または bash が必要です。実行時に依存する gem はありません。
|
|
23
|
+
|
|
24
|
+
## シェルの設定
|
|
25
|
+
|
|
26
|
+
rc ファイルに 1 行足します(zsh は `~/.zshrc`、bash は `~/.bashrc`。macOS で bash の設定を `~/.bash_profile` に置いているなら、そちらに書きます)。
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
eval "$(oops init zsh)" # zsh
|
|
30
|
+
eval "$(oops init bash)" # bash
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
そのあと新しいタブを開いてください。開いているタブでは `exec zsh`(bash なら `exec bash`)で反映されます。
|
|
34
|
+
|
|
35
|
+
この行を置く場所が重要です。
|
|
36
|
+
|
|
37
|
+
- oops の入った Ruby を `PATH` に通す設定(rbenv、asdf、mise、Homebrew の `shellenv` など)より**後ろ**に置きます。macOS ではログインシェルが `PATH` をリセットするため、この行が先にあるとシステムの Ruby 2.6 が選ばれて oops が起動しないことがあります。oops が動かなくてもシェル自体は普通に起動します。記録されないだけです。
|
|
38
|
+
- 2 回実行したくない重い処理や出力の多い処理(nvm、プロンプトテーマ、バナーなど)より**前**に置きます。このスニペットは、小さな記録用プログラム(`oops record`、pty 中継)の下でシェルをもう一度起動します。そのため、この行より上は外側のシェルと内側のシェルで 2 回実行され、下は内側のシェルでだけ実行されます。
|
|
39
|
+
- powerlevel10k を使う場合は、`~/.zshrc` の先頭にある instant prompt のブロックより**上**に置きます。instant prompt は `~/.zshrc` の残りを読む間、シェルの入出力をリダイレクトし、その間スニペットは何もしません(実際の端末でだけ動作するため)。すると oops は黙って記録しなくなります。`~/.zshrc` に行があるのにタブが記録されないときは `oops doctor` を実行してください。`recording: no` と表示されます。
|
|
40
|
+
- zsh ではシェルがログインシェルではない形で再起動されます。`~/.zlogin` にだけ書いた設定は、記録されるシェルには適用されません。`~/.zprofile` か `~/.zshrc`(この行より上)に置いてください。bash ではログインシェルは `-l` 付きで再起動されます。
|
|
41
|
+
|
|
42
|
+
おかしいと感じたら `oops doctor` を実行します。このタブが記録されているか、シェルとフックの読み込み状況、使われるクリップボードの方法、設定ファイル、セッションログのサイズ、Ruby のバージョンを報告します。
|
|
43
|
+
|
|
44
|
+
1 つのシェルだけ oops を無効にするには、`OOPS_DISABLE=1`(例: `OOPS_DISABLE=1 zsh`)を付けて起動します。スニペットは何もしなくなります。
|
|
45
|
+
|
|
46
|
+
## 使い方
|
|
47
|
+
|
|
48
|
+
| コマンド | 内容 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `oops` | 直前のコマンドとその出力をコピーする |
|
|
51
|
+
| `oops 3` | 直前の 3 コマンドを(古い順に)1 つのコードブロックとしてコピーする |
|
|
52
|
+
| `oops err` | 終了コードが 0 以外だった直近のコマンドをコピーする(成功したコマンドは飛ばす) |
|
|
53
|
+
| `oops init zsh` / `oops init bash` | シェルのスニペットを出力する(`eval` 用) |
|
|
54
|
+
| `oops doctor` | 記録・フック・クリップボード・設定・ログサイズを診断する |
|
|
55
|
+
| `oops pause` / `oops resume` | 記録を止める/再開する。停止中は何もディスクに書かれない |
|
|
56
|
+
| `oops record -- <shell>` | 内部用: 記録用プログラムの下でシェルを起動する。スニペットが呼び出す |
|
|
57
|
+
| `oops --version`, `oops --help` | バージョンと使い方 |
|
|
58
|
+
|
|
59
|
+
`oops`、`oops N`、`oops err` のオプション:
|
|
60
|
+
|
|
61
|
+
| オプション | 内容 |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `--github` | GitHub の issue / PR コメント向けの書式にする |
|
|
64
|
+
| `--tail N` | 出力の最後の N 行だけ残す |
|
|
65
|
+
| `--head N` | 出力の最初の N 行だけ残す(`--tail` と併用すると両端を残す) |
|
|
66
|
+
| `--show-secrets` | 秘密情報をマスクせず、置換テーブルも適用しない |
|
|
67
|
+
|
|
68
|
+
`oops` 自身はコピー対象になりません。直前のコマンドが `oops` だった場合は、その前のコマンドがコピーされます。
|
|
69
|
+
|
|
70
|
+
終了コード: `0` コピーした、`1` コピーしなかった(記録していない、まだコマンドがない、失敗したコマンドがない、一時停止中、クリップボードのコマンドが失敗)、`2` 使い方の誤り。
|
|
71
|
+
|
|
72
|
+
### 標準の出力
|
|
73
|
+
|
|
74
|
+
`oops` は `copied: $ bundle exec rspec (5 lines, 2 secrets masked)` のような 1 行を表示し、次の内容をクリップボードに入れます。
|
|
75
|
+
|
|
76
|
+
````
|
|
77
|
+
```
|
|
78
|
+
$ bundle exec rspec
|
|
79
|
+
....F
|
|
80
|
+
|
|
81
|
+
Failures:
|
|
82
|
+
1) User#name returns full name
|
|
83
|
+
expected "Taro Yamada", got "Taro"
|
|
84
|
+
(exit 1)
|
|
85
|
+
```
|
|
86
|
+
````
|
|
87
|
+
|
|
88
|
+
`(exit N)` は終了コードが 0 でないときだけ付きます。出力自体にコードフェンスが含まれているときは、バッククォート 4 つで囲みます。
|
|
89
|
+
|
|
90
|
+
### `--github`
|
|
91
|
+
|
|
92
|
+
出力が 30 行以内なら `console` ブロックになり、GitHub がシェルセッションとして色付けします。
|
|
93
|
+
|
|
94
|
+
````
|
|
95
|
+
```console
|
|
96
|
+
$ bundle exec rspec
|
|
97
|
+
....F
|
|
98
|
+
(exit 1)
|
|
99
|
+
```
|
|
100
|
+
````
|
|
101
|
+
|
|
102
|
+
それより長い出力は折りたたみ(`<details>`)で包まれ、要約にコマンド・終了コード・行数が出ます。
|
|
103
|
+
|
|
104
|
+
````
|
|
105
|
+
<details>
|
|
106
|
+
<summary><code>$ bundle exec rspec</code> — exit 1, 312 lines</summary>
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
$ bundle exec rspec
|
|
110
|
+
...
|
|
111
|
+
(exit 1)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
</details>
|
|
115
|
+
````
|
|
116
|
+
|
|
117
|
+
### `--tail` と `--head`
|
|
118
|
+
|
|
119
|
+
`oops --tail 50` は出力の最後の 50 行を残し、その前に `... 2,950 lines omitted ...` を入れます。`--head 20` は最初の 20 行を残し、その後ろに同じ印を入れます。両方指定すると、中間が印 1 行に置き換わります。`oops 3` では、行数の制限は各コマンドごとに適用されます。
|
|
120
|
+
|
|
121
|
+
## 設定
|
|
122
|
+
|
|
123
|
+
任意の設定ファイルは `${XDG_CONFIG_HOME:-~/.config}/oops/config.yml` です。
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
replace: # コピー時に適用する単純な文字列置換
|
|
127
|
+
takeuchi: dev
|
|
128
|
+
MacBook-Pro: laptop
|
|
129
|
+
secrets: # マスクする追加の正規表現
|
|
130
|
+
- "sk_live_[A-Za-z0-9]+"
|
|
131
|
+
clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52 | 任意のコマンド(標準入力から読む)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- 置換とマスクはコピーする瞬間に適用されます。記録済みのログは変更されないため、設定を変えると以前のコマンドにも反映されます。
|
|
135
|
+
- `replace` のキーは長いものから順に、大文字小文字を区別して適用されます。
|
|
136
|
+
- 未知のキーは警告されて無視されます。YAML が壊れている場合も警告が出て、設定なしで続行します。
|
|
137
|
+
- `clipboard: auto` のとき、oops は macOS では `pbcopy`、WSL では `clip.exe`、Wayland では `wl-copy`、X11 では `xclip` か `xsel` を試し、だめなら OSC 52 端末エスケープシーケンスにフォールバックします(SSH 越しでもこれが使われます)。どれを選んだかは `oops doctor` で確認できます。
|
|
138
|
+
- 自分で指定した `clipboard:` のコマンドは、独自のプロセスグループで 5 秒のタイムアウト付きで実行されます。そのため端末の読み取りや設定(パスワード入力、`stty` など)はできません。コピー中の Ctrl-C もそのコマンドには届きません。
|
|
139
|
+
|
|
140
|
+
組み込みのマスク対象: 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 アドレス(ループバックとプライベート範囲は対象外)。出力だけでなくコマンドラインにも適用されます。
|
|
141
|
+
|
|
142
|
+
マスクはパターンに基づくため、保証ではなく安全網です。知らない形式の秘密情報(たとえば 1 行に単独で出力された生のパスワード)は、そのままコピーされます。貼り付ける前に内容を確認し、必要なら設定ファイルの `secrets:` に自分のパターンを足してください。
|
|
143
|
+
|
|
144
|
+
## 仕組み
|
|
145
|
+
|
|
146
|
+
1. `oops init` は、rc ファイルから `eval` するスニペットを出力します。まだ記録されていない端末の対話シェルでは、シェルを `oops record` の下で再起動します。
|
|
147
|
+
2. `oops record` は、Ruby 製の小さな pty 中継です。キー入力とシェルの出力をそのまま通し、ウィンドウのリサイズをシェルに伝えます。
|
|
148
|
+
3. シェルのフック(zsh は `preexec`/`precmd`、bash は `DEBUG` トラップと `PROMPT_COMMAND`)が、各コマンドの前後に目に見えない APC マーカーを出力します。中継がそれを取り除くので、端末には見えません。
|
|
149
|
+
4. コマンドが終わるたびに、中継は 1 行の JSON(コマンド、ディレクトリ、時刻、終了コード、生の出力)をタブごとのログに追記します。置き場所は `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` です。出力は 1 コマンドあたり 1 MB まで(末尾を残す)。各タブのログは 50 MB までで、超えると新しいコマンドだけを残して書き直されます(約 37 MiB まで縮む)。タブが記録を始めるとき、閉じたタブのログは古い順に、全ログの合計が 50 MB 以下になるまで削除されます。開いているタブのログは削除されないので、忙しいタブが複数あると合計がそれを超えることがあります。
|
|
150
|
+
5. `oops` は現在のタブのログの末尾を読み、テキストを整えてマスクし、コードブロックを組み立ててクリップボードに渡します。コマンドを再実行することはありません。
|
|
151
|
+
|
|
152
|
+
## プライバシーとセキュリティ
|
|
153
|
+
|
|
154
|
+
- セッションログには、記録されたすべてのコマンドの生の出力が**マスクされないまま**入っています。マスクと置換テーブルはコピーするときにだけ適用されます。ログは `${XDG_STATE_HOME:-~/.local/state}/oops/sessions/` にあり、あなただけが読めます(ファイルは 0600、ディレクトリは 0700)。すべて削除するには `rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/oops"` を実行します(標準では `rm -rf ~/.local/state/oops`)。ディスクに残したくない秘密情報を表示する前には `oops pause` を使ってください。
|
|
155
|
+
- 記録される出力は信頼できません。実行したプログラムは、oops のマーカーに見せかけたテキストを出力できます。そのため oops は、`oops pause` / `oops resume` が作るフラグファイルと一致する一時停止・再開マーカーにだけ従います。出力によって黙って記録を止められることはありません。ただし、プログラムがログ上でコマンドの開始と終了の位置を偽装することはできるので、貼り付ける内容は確認してください。
|
|
156
|
+
|
|
157
|
+
## 既知の制限
|
|
158
|
+
|
|
159
|
+
- 記録中のタブごとに Ruby プロセスが 1 つ常駐します(約 17 MB)。
|
|
160
|
+
- 環境変数を落とす `sudo -s` や `su` は、入れ子の 2 つ目の記録を始めるため、それらのコマンドは 2 回ログに残ります。
|
|
161
|
+
- 使えるのは現在のタブだけです。閉じたタブや以前のタブのものは取り出せません。
|
|
162
|
+
- OSC 52 経由(SSH やクリップボードツールがない場合)では、端末が一度に送れる量を制限します。oops は約 100 KB に切り詰めて警告します。OSC 52 は一方通行のエスケープシーケンスなので、端末がコピーを受け付けたかどうかは分かりません。無視する端末や確認を求める端末もあり、tmux では `set -g set-clipboard on` が必要です。
|
|
163
|
+
- 8192 文字を超えるコマンドは、記録されるコマンドラインが切り詰められます。
|
|
164
|
+
- vim・less・tmux などの全画面アプリの出力は保存されず、`[full-screen app output omitted]` に置き換わります。
|
|
165
|
+
- スペースで始まるコマンドラインは、zsh では記録されますが bash では記録されません(bash はそれを履歴に保存せず、oops はコマンドを履歴から読むため)。
|
|
166
|
+
- [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 はそのような行の最初のコマンドを記録します。
|
|
167
|
+
- Ghostty 上の bash: Ghostty のシェル統合(プロンプトマーク、同じディレクトリでの新しいタブ、タイトル)は外側のシェルにだけ入るため、記録される側のシェルには入りません。足し戻すには、`~/.bashrc` の oops の行の下に次の行を置きます(bash 3.2 で確認済み): `[ -n "$GHOSTTY_RESOURCES_DIR" ] && source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/ghostty.bash"`。zsh は何もしなくて構いません。スニペットがやります。
|
|
168
|
+
- bash: パイプラインの次の行では、`PIPESTATUS` に最後の終了ステータスしか入りません(`false | true` の後に `echo ${PIPESTATUS[*]}` を実行すると、`1 0` ではなく `0` と表示されます)。oops の `PROMPT_COMMAND` の項目が先に実行されるためです。`$?` と `$_` は保たれます。パイプラインの各ステータスを表示するプロンプト(starship の `pipestatus`)は、bash では表示が少なくなります。zsh は `pipestatus` を保ちます。
|
|
169
|
+
- macOS: `pbcopy` は文字エンコーディングを `LANG` / `LC_*` から決めます。日本語などの非 ASCII 文字が文字化けして届くときは、UTF-8 のロケール(例: `LANG=en_US.UTF-8`)が設定されているか確認してください。SSH 越しで起動したシェルでは設定されていないことがあります。
|
|
170
|
+
- fish とネイティブ Windows にはまだ対応していません。
|
|
171
|
+
|
|
172
|
+
## 開発
|
|
173
|
+
|
|
174
|
+
### 手元の環境を整える
|
|
175
|
+
|
|
176
|
+
Git、Ruby 3.3 以降、[Bundler](https://bundler.io/)(Ruby に付属)、そして zsh と bash の両方が必要です(エンドツーエンドのテストが本物のシェルを起動するため)。macOS には zsh と bash が入っています。Debian / Ubuntu では `sudo apt-get install zsh` を実行します。
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
git clone https://github.com/ysb3996-blip/oops.git
|
|
180
|
+
cd oops
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
リポジトリは `.ruby-version` で Ruby 4.0.9 を指定しています。rbenv・asdf・mise などのバージョン管理ツールを使っている場合は、そのバージョンを入れてください(`rbenv install 4.0.9`、続けて `rbenv rehash`)。この指定を消すか無視すれば、3.3 以降の任意の Ruby でも動きます。`ruby -v` で確認します。
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
bin/setup # Ruby・zsh・bash を確認し、gem を入れて、チェックを実行する
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`bin/setup` は、`bundle install` のあとに `bundle exec rake` を実行するのと同じです。`bundle exec rake` はテスト全体(約 30 秒)を実行したあと rubocop(スタイルのチェック)を実行します。`bundle exec rake test` と `bundle exec rubocop` で、1 つずつ実行することもできます。
|
|
190
|
+
|
|
191
|
+
CI は Ubuntu と macOS で、Ruby 3.3・3.4・4.0 を使ってこの 2 つを実行し、gem のビルドと、bash スニペットへの shellcheck(`shellcheck -S warning lib/oops/shell/oops.bash bin/setup`。手元に入れていれば実行できます)も行います。プルリクエストを出す前に、次を実行してください。
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
bundle exec rake && gem build oops-cli.gemspec
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 変更を試す
|
|
198
|
+
|
|
199
|
+
テストファイル 1 つだけなら `bundle exec ruby -Itest -Ilib test/ansi_test.rb` で実行できます。何もインストールせず、チェックアウトしたものからコマンドを直接動かすには:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
bundle exec exe/oops --version
|
|
203
|
+
bundle exec exe/oops doctor
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
シェルから使うのと同じ形で試すには、使っている Ruby に gem をビルドしてインストールし、「シェルの設定」の 1 行を**新しい**タブで有効にします。
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
gem build oops-cli.gemspec
|
|
210
|
+
gem install ./oops-cli-0.1.0.gem # ファイル名は lib/oops/version.rb に合わせる
|
|
211
|
+
rbenv rehash # rbenv を使う場合のみ
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- gem は Ruby のバージョンごとに入ります。新しいタブが、インストールした Ruby とは別の Ruby を使っていると(たとえばリポジトリは 4.0.9 を指定しているのに、標準の Ruby が 3.3.5 の場合)、シェルに `oops: command not found` と出ます。その Ruby でも `gem install` を実行してください(例: `RBENV_VERSION=3.3.5 gem install ./oops-cli-0.1.0.gem`)。
|
|
215
|
+
- 変更のたびに入れ直してください。インストールされた gem はコピーで、チェックアウトへのリンクではありません。
|
|
216
|
+
- 削除するには `gem uninstall oops-cli` です。
|
|
217
|
+
- `eval "$(oops init zsh)"` は、プロンプトで打つだけでなく rc ファイルに書いてください。記録されるシェルは rc ファイルを読み直すため、行がないとフックが入りません(`oops doctor` に `hooks: missing` と出ます)。
|
|
218
|
+
|
|
219
|
+
作業中の端末で `oops record` を実行しないでください。テストは自前の pty を通して動かします。試すときは、新しい端末タブを使います。試した結果として残るログを消すには、「プライバシーとセキュリティ」を参照してください。
|
|
220
|
+
|
|
221
|
+
テストは、書式、秘密情報のマスク、ログの読み取り、ストリームパーサー、そして実際の zsh と bash を使った中継のエンドツーエンド実行を対象にしています。
|
|
222
|
+
|
|
223
|
+
リリース前の手動チェックリスト:
|
|
224
|
+
|
|
225
|
+
- Ghostty、Terminal.app、VS Code の統合ターミナルで、マーカーのゴミが表示されない。
|
|
226
|
+
- vim、less、全画面の端末アプリが、ウィンドウをリサイズしても追従する。
|
|
227
|
+
- 記録中のタブで `exec zsh` しても、2 つ目の記録が始まらない。
|
|
228
|
+
- 記録中のタブから `code .` で開いた VS Code のターミナルが、独立して記録される。
|
|
229
|
+
|
|
230
|
+
## コントリビュート
|
|
231
|
+
|
|
232
|
+
バグ報告、アイデア、プルリクエストを歓迎します。[CONTRIBUTING.ja.md](CONTRIBUTING.ja.md) を読んでください。`good first issue` のラベルが付いた issue が、始めやすい入口です。
|
|
233
|
+
|
|
234
|
+
## ライセンス
|
|
235
|
+
|
|
236
|
+
MIT。[LICENSE](LICENSE) を参照してください。
|
data/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# oops
|
|
2
|
+
|
|
3
|
+
English | [日本語](README.ja.md)
|
|
4
|
+
|
|
5
|
+
Copy the last shell command and its output to the clipboard, ready to paste.
|
|
6
|
+
|
|
7
|
+
You ran a command and it failed. You want to show it to a chat assistant, a teammate on Slack, or a GitHub issue. Normally that means dragging to select the output, copying, pasting, and adding the code fences yourself. With oops it is two steps:
|
|
8
|
+
|
|
9
|
+
1. Type `oops` (or `oops err` for the last failed command).
|
|
10
|
+
2. Paste with Cmd-V.
|
|
11
|
+
|
|
12
|
+
What lands on your clipboard is a code block with the command, its output, and the exit code. Colors are stripped, and tokens, keys and email addresses are masked.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install oops-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Homebrew tap: coming soon.
|
|
21
|
+
|
|
22
|
+
Requires Ruby 3.3 or newer on macOS or Linux (WSL counts as Linux), and zsh or bash. There are no runtime gem dependencies.
|
|
23
|
+
|
|
24
|
+
## Set up your shell
|
|
25
|
+
|
|
26
|
+
Add one line to your rc file (`~/.zshrc` for zsh, `~/.bashrc` for bash; if you keep your bash setup in `~/.bash_profile` on macOS, put it there):
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
eval "$(oops init zsh)" # zsh
|
|
30
|
+
eval "$(oops init bash)" # bash
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open a new tab. Existing tabs pick it up after `exec zsh` (or `exec bash`).
|
|
34
|
+
|
|
35
|
+
Where to put the line matters:
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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`.
|
|
40
|
+
- 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`.
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
To turn oops off for one shell, start it with `OOPS_DISABLE=1` (for example `OOPS_DISABLE=1 zsh`). The snippet then does nothing.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
| Command | What it does |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `oops` | Copy the last command and its output |
|
|
51
|
+
| `oops 3` | Copy the last 3 commands (oldest first) as one code block |
|
|
52
|
+
| `oops err` | Copy the last command that exited non-zero, skipping over successful ones |
|
|
53
|
+
| `oops init zsh` / `oops init bash` | Print the shell snippet (for `eval`) |
|
|
54
|
+
| `oops doctor` | Diagnose recording, hooks, clipboard, config and log size |
|
|
55
|
+
| `oops pause` / `oops resume` | Stop and restart recording. While paused, nothing is written to disk |
|
|
56
|
+
| `oops record -- <shell>` | Internal: runs a shell under the recorder. The snippet calls this |
|
|
57
|
+
| `oops --version`, `oops --help` | Version and usage |
|
|
58
|
+
|
|
59
|
+
Options for `oops`, `oops N` and `oops err`:
|
|
60
|
+
|
|
61
|
+
| Option | What it does |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `--github` | Format for a GitHub issue or PR comment |
|
|
64
|
+
| `--tail N` | Keep only the last N output lines |
|
|
65
|
+
| `--head N` | Keep only the first N output lines (with `--tail`, keeps both ends) |
|
|
66
|
+
| `--show-secrets` | Do not mask secrets and do not apply the replace table |
|
|
67
|
+
|
|
68
|
+
`oops` never copies itself: if the previous command was `oops`, you get the command before it.
|
|
69
|
+
|
|
70
|
+
Exit codes: `0` copied, `1` nothing copied (not recording, no command yet, no failed command, paused, or the clipboard command failed), `2` usage error.
|
|
71
|
+
|
|
72
|
+
### Default output
|
|
73
|
+
|
|
74
|
+
`oops` prints one line such as `copied: $ bundle exec rspec (5 lines, 2 secrets masked)` and puts this on the clipboard:
|
|
75
|
+
|
|
76
|
+
````
|
|
77
|
+
```
|
|
78
|
+
$ bundle exec rspec
|
|
79
|
+
....F
|
|
80
|
+
|
|
81
|
+
Failures:
|
|
82
|
+
1) User#name returns full name
|
|
83
|
+
expected "Taro Yamada", got "Taro"
|
|
84
|
+
(exit 1)
|
|
85
|
+
```
|
|
86
|
+
````
|
|
87
|
+
|
|
88
|
+
`(exit N)` appears only when the exit code is not 0. If the output itself contains a code fence, the block is fenced with four backticks instead.
|
|
89
|
+
|
|
90
|
+
### `--github`
|
|
91
|
+
|
|
92
|
+
Output of 30 lines or fewer becomes a `console` block, which GitHub highlights as a shell session:
|
|
93
|
+
|
|
94
|
+
````
|
|
95
|
+
```console
|
|
96
|
+
$ bundle exec rspec
|
|
97
|
+
....F
|
|
98
|
+
(exit 1)
|
|
99
|
+
```
|
|
100
|
+
````
|
|
101
|
+
|
|
102
|
+
Longer output is wrapped in a collapsible section whose summary shows the command, exit code and line count:
|
|
103
|
+
|
|
104
|
+
````
|
|
105
|
+
<details>
|
|
106
|
+
<summary><code>$ bundle exec rspec</code> — exit 1, 312 lines</summary>
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
$ bundle exec rspec
|
|
110
|
+
...
|
|
111
|
+
(exit 1)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
</details>
|
|
115
|
+
````
|
|
116
|
+
|
|
117
|
+
### `--tail` and `--head`
|
|
118
|
+
|
|
119
|
+
`oops --tail 50` keeps the last 50 lines of output and puts `... 2,950 lines omitted ...` before them. `--head 20` keeps the first 20 and puts the marker after. With both, the middle is replaced by one marker line. With `oops 3`, the limits apply to each command.
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
Optional file at `${XDG_CONFIG_HOME:-~/.config}/oops/config.yml`:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
replace: # plain string replacements applied when copying
|
|
127
|
+
takeuchi: dev
|
|
128
|
+
MacBook-Pro: laptop
|
|
129
|
+
secrets: # extra regular expressions to mask
|
|
130
|
+
- "sk_live_[A-Za-z0-9]+"
|
|
131
|
+
clipboard: auto # auto | pbcopy | wl-copy | xclip | xsel | clip.exe | osc52 | any command (reads stdin)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- Replacements and masking are applied at the moment you copy; the recorded log stays untouched, so a config change also affects earlier commands.
|
|
135
|
+
- `replace` keys are applied longest first and are case-sensitive.
|
|
136
|
+
- Unknown keys produce a warning and are ignored. A broken YAML file produces a warning and oops carries on without a config.
|
|
137
|
+
- 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.
|
|
138
|
+
- 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.
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
Masking is pattern based, so it is a safety net, not a guarantee: a secret in a format it does not know (a bare password printed on its own line, for example) is copied as it is. Read what you are about to paste, and add your own patterns under `secrets:` in the config file.
|
|
143
|
+
|
|
144
|
+
## How it works
|
|
145
|
+
|
|
146
|
+
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`.
|
|
147
|
+
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.
|
|
148
|
+
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.
|
|
149
|
+
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 first, until all logs together take at most 50 MB; logs of open tabs are never deleted, so several busy tabs can together use more.
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
## Privacy and security
|
|
153
|
+
|
|
154
|
+
- 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 all of them, 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.
|
|
155
|
+
- 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.
|
|
156
|
+
|
|
157
|
+
## Known limitations
|
|
158
|
+
|
|
159
|
+
- Each recorded tab keeps one Ruby process alive (about 17 MB).
|
|
160
|
+
- `sudo -s` or `su` that drops the environment starts a second, nested recording, so those commands are logged twice.
|
|
161
|
+
- Only the current tab is available. A tab you closed, or an earlier tab, cannot be retrieved.
|
|
162
|
+
- Via OSC 52 (SSH, or no clipboard tool), terminals limit how much can be sent at once. oops truncates to roughly 100 KB and warns. OSC 52 is a one-way escape sequence, so oops cannot tell whether the terminal accepted the copy; some terminals ignore it or ask first, and tmux needs `set -g set-clipboard on`.
|
|
163
|
+
- Commands longer than 8192 characters are truncated in the recorded command line.
|
|
164
|
+
- Output of full-screen apps (vim, less, tmux and so on) is not kept; it is replaced by `[full-screen app output omitted]`.
|
|
165
|
+
- 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).
|
|
166
|
+
- 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.
|
|
167
|
+
- 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 needs nothing: the snippet does it.
|
|
168
|
+
- 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`.
|
|
169
|
+
- 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.
|
|
170
|
+
- fish and native Windows are not supported yet.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
### Set up your machine
|
|
175
|
+
|
|
176
|
+
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`.
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
git clone https://github.com/ysb3996-blip/oops.git
|
|
180
|
+
cd oops
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The repository pins Ruby 4.0.9 in `.ruby-version`. With a version manager such as rbenv, asdf or mise, install that version (`rbenv install 4.0.9`, then `rbenv rehash`). Any Ruby 3.3 or newer also works if you delete or ignore the pin. Check with `ruby -v`.
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
bin/setup # checks Ruby, zsh and bash, installs gems, runs the checks
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`bin/setup` is the same as `bundle install` followed by `bundle exec rake`. `bundle exec rake` runs the whole test suite (about 30 seconds) and then rubocop (style check); `bundle exec rake test` and `bundle exec rubocop` run them one at a time.
|
|
190
|
+
|
|
191
|
+
CI runs both on Ubuntu and macOS with Ruby 3.3, 3.4 and 4.0, builds the gem, and runs shellcheck on the bash snippet (`shellcheck -S warning lib/oops/shell/oops.bash bin/setup`, if you have it). Before opening a pull request, run:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
bundle exec rake && gem build oops-cli.gemspec
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Try your changes
|
|
198
|
+
|
|
199
|
+
Run a single test file with `bundle exec ruby -Itest -Ilib test/ansi_test.rb`. To run the command straight from the checkout, without installing anything:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
bundle exec exe/oops --version
|
|
203
|
+
bundle exec exe/oops doctor
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To try it as your shell does, build and install the gem into the Ruby you are using, then add the line from "Set up your shell" to a **new** tab:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
gem build oops-cli.gemspec
|
|
210
|
+
gem install ./oops-cli-0.1.0.gem # the file name follows lib/oops/version.rb
|
|
211
|
+
rbenv rehash # only with rbenv
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- 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`. Run `gem install` again under that Ruby, e.g. `RBENV_VERSION=3.3.5 gem install ./oops-cli-0.1.0.gem`.
|
|
215
|
+
- Reinstall after every change. The installed gem is a copy, not a link to your checkout.
|
|
216
|
+
- To remove it: `gem uninstall oops-cli`.
|
|
217
|
+
- 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`).
|
|
218
|
+
|
|
219
|
+
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".
|
|
220
|
+
|
|
221
|
+
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 bash.
|
|
222
|
+
|
|
223
|
+
Manual checklist before a release:
|
|
224
|
+
|
|
225
|
+
- No marker garbage is visible in Ghostty, Terminal.app and the VS Code integrated terminal.
|
|
226
|
+
- vim, less and a full-screen terminal app keep up while you resize the window.
|
|
227
|
+
- `exec zsh` in a recorded tab does not start a second recording.
|
|
228
|
+
- A VS Code terminal opened with `code .` from a recorded tab is recorded on its own.
|
|
229
|
+
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
Bug reports, ideas and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md); issues labeled `good first issue` are a good place to start.
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT. See [LICENSE](LICENSE).
|
data/exe/oops
ADDED
data/lib/oops/ansi.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Oops
|
|
2
|
+
# Removes terminal control sequences from captured output. Binary-safe:
|
|
3
|
+
# never raises on invalid UTF-8 and never scrubs; the result keeps the
|
|
4
|
+
# input's encoding.
|
|
5
|
+
module Ansi
|
|
6
|
+
ESCAPES = Regexp.new(
|
|
7
|
+
[
|
|
8
|
+
"\\e\\[[0-?]*[ -/]*[@-~]", # CSI
|
|
9
|
+
"\\e\\][^\\a\\e]*(?:\\a|\\e\\\\|\\z)", # OSC
|
|
10
|
+
"\\e[P_^X][^\\e]*(?:\\e\\\\)?", # DCS, APC, PM, SOS (unterminated: up to the next ESC)
|
|
11
|
+
"\\e[ -/]+[0-~]", # nF sequences
|
|
12
|
+
"\\e[0-~]?", # 2-char ESC sequences, e.g. ESC 7, ESC 8, ESC =, ESC M
|
|
13
|
+
"\\a" # BEL
|
|
14
|
+
].join("|"),
|
|
15
|
+
Regexp::MULTILINE | Regexp::NOENCODING
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# "Move the cursor right N columns": full-screen UIs use it instead of
|
|
19
|
+
# printing spaces, so dropping it would glue words together.
|
|
20
|
+
CURSOR_FORWARD = /\e\[(\d*)C/n
|
|
21
|
+
MAX_FORWARD = 200
|
|
22
|
+
|
|
23
|
+
# "Move to column N": the same UIs place each word this way. Columns are
|
|
24
|
+
# not replayed; a jump that follows text becomes one space, so words stay
|
|
25
|
+
# apart. A NUL marks the jump while the other sequences are removed.
|
|
26
|
+
COLUMN_JUMP = /\e\[\d*G/n
|
|
27
|
+
JUMP_MARK = "\0".b
|
|
28
|
+
AFTER_TEXT_JUMP = /(?<=[^\s\0])\0+/n
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def strip(bytes)
|
|
33
|
+
encoding = bytes.encoding
|
|
34
|
+
text = bytes.b.gsub(CURSOR_FORWARD) { " ".b * Regexp.last_match(1).to_i.clamp(1, MAX_FORWARD) }
|
|
35
|
+
text = text.gsub(COLUMN_JUMP, JUMP_MARK).gsub(ESCAPES, "".b)
|
|
36
|
+
text = text.gsub(AFTER_TEXT_JUMP, " ".b).delete(JUMP_MARK).gsub("\r\n".b, "\n".b)
|
|
37
|
+
lines = text.split("\n".b, -1).map { |line| apply_line_controls(line, encoding) }
|
|
38
|
+
lines.join("\n".b).force_encoding(encoding)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def apply_line_controls(line, encoding)
|
|
42
|
+
line = line.split("\r".b).reverse.find { |part| !part.empty? } || "".b if line.include?("\r".b)
|
|
43
|
+
return line unless line.include?("\b".b)
|
|
44
|
+
|
|
45
|
+
units = line.dup.force_encoding(encoding)
|
|
46
|
+
units = units.valid_encoding? ? units.chars : line.chars
|
|
47
|
+
out = []
|
|
48
|
+
units.each { |u| u.b == "\b".b ? out.pop : out << u }
|
|
49
|
+
out.map(&:b).join
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|