RetainProcess の構成を作る AI へのガイド
このガイドは、RetainProcess を初めて扱う AI アシスタント向けです。利用者の代わりに、RetainProcess に登録する「構成」を書くための書式と規則をまとめています。これは RetainProcess 1.0 の仕様です(出典: https://foo-tec.com/retainprocess/)。
1. RetainProcess とは
- macOS のメニューバーに常駐するアプリ。開発で使う、長く動き続けるコマンド(開発サーバー、ワーカー、ローカル LLM、自分でビルドする Mac アプリなど)を「構成」として登録し、ワンクリックで起動(Run)・停止(Stop)する。
- 動かしている間、CPU・メモリ(プロセスグループの合算)と stdout / stderr のログを表示する。ログはファイルにも保存し、古い世代を指定フォルダへ退避する。
- 停止はプロセスグループ単位: SIGTERM を送り、猶予秒数を過ぎてもグループが残っていれば SIGKILL。
- RetainProcess を終了しても、動かしているコマンドは止まらない。
- CLI
retain-processがあるが、CLI では構成を追加・編集できない。構成の追加は「取り込み(Import)」か、スタックのファイル(retainprocess.yml)で行う。
2. 出力の選び方(JSON か YAML か)
| 状況 | 出すもの | 利用者の反映方法 |
|---|---|---|
| 単独のコマンドを 1 つ以上登録したい | 構成の JSON(配列) | ポップアップの ⤓ → Import from Clipboard(または Import from File…) |
| 1 つのプロジェクトの複数のサービスを、依存の順に起動したい(DB → API → フロントなど) | retainprocess.yml | プロジェクトのディレクトリに保存し、そこで retain-process up |
しないこと: ~/Library/Application Support/RetainProcess/config.json を直接編集させない。アプリは起動時にしか読まず、動いているアプリが次に保存したときに上書きされて消える。
3. 利用者に確かめること
次の項目が会話から分からなければ、推測せずに質問する。プロジェクトのフォルダを示されたら、先に IDE の起動構成を読む(4 章)と、多くはそこで分かる。
- 動かすコマンドそのもの(ターミナルで打っているとおりに)
- どのディレクトリで動かすか(絶対パスか
~/から) - 必要な環境変数、または読む
.envファイル - 落ちたときに自動で起動し直したいか(
no/on-failure/always) - 複数のサービスに依存関係があるか。あるなら、準備完了をポートか HTTP で判定できるか
- (任意)案件ごとにまとめる名前(ワークスペース)
Mac アプリ(.app)を登録したいときは、7 章の「利用者に確かめること」も聞く。
4. IDE の起動構成から作る(IntelliJ・VS Code・Cursor など)
利用者がプロジェクトのフォルダを示したら、そこにある IDE の起動構成(Run Configuration / launch.json / tasks.json)を読み、RetainProcess の構成に変換できる。コマンド・引数・作業ディレクトリ・環境変数を聞き直さずに済む。
- フォルダを読める AI(Claude Code、Cursor のエージェント、Codex など)なら、フォルダのパスだけで始められる。
- ファイルを読めないチャットの AI なら、下の表のファイルの中身を貼り付けてもらう。
- RetainProcess 自体に IDE の構成を読む機能は無い。変換するのは AI で、結果は通常どおり取り込み用の JSON(または
retainprocess.yml)として出す。
探す場所
| IDE | ファイル | 備考 |
|---|---|---|
| JetBrains(IntelliJ IDEA・WebStorm・PyCharm・RustRover・GoLand など) | .run/*.run.xml.idea/runConfigurations/*.xml.idea/workspace.xml の <component name="RunManager"> | 1 つの <configuration> が 1 つの起動構成。name と type 属性を見る |
| VS Code・Cursor(Cursor は VS Code と同じファイル) | .vscode/launch.json.vscode/tasks.json*.code-workspace の launch / tasks | JSON にコメントや末尾のカンマがあってよい(JSONC) |
| Xcode・XcodeGen・Swift Package(Mac アプリ) | *.xcodeproj/xcshareddata/xcschemes/*.xcschemeproject.yml(XcodeGen)Package.swift | スキームの LaunchAction にある実行対象・引数(CommandLineArguments)・環境変数(EnvironmentVariables)を読む。変換のしかたは 7 章 |
| (IDE 以外の手がかり) | package.json の scripts、Procfile、Makefile、docker-compose.yml | IDE の構成が無いときの候補 |
選び方
- 変換するのは動き続けるものだけ: 開発サーバー、API、ワーカー、ウォッチ(
dev・serve・watch・startなど)。 - すぐ終わるもの(ビルド、テスト、lint、マイグレーション、コード生成)、デバッガのアタッチ(
"request": "attach")、iOS シミュレータや実機での実行、openでアプリを開くだけのものは、そのままでは変換しない。 - Mac アプリの起動(Xcode のスキーム、
AppleRunConfiguration、最後にopen X.appするスクリプトや Makefile のターゲット)は、ビルドしてから.appの中の実行ファイルをexecする形(7 章)に直せば変換できる。openのまま登録しない。 - 見つけた起動構成の一覧と、変換する / しないの判断を先に利用者に見せ、確認を取ってから出力する。
nameは元の起動構成の名前を使う。workspaceにプロジェクトのフォルダ名を入れると、まとめて絞り込める。- 依存関係がある複数の起動構成(VS Code の
compounds、tasks のdependsOn、「API を起動してからフロントを起動する」などの手順)は、retainprocess.ymlのスタックにするのも提案する。
変数の置き換え
| 元の書き方 | 置き換え | 備考 |
|---|---|---|
$PROJECT_DIR$、${workspaceFolder} | プロジェクトのフォルダのパス(~/… の形) | JetBrains の file://$PROJECT_DIR$ は file:// を外す |
$USER_HOME$、${userHome} | ~ | args の中では ~ が展開されないので、そこだけは絶対パスにする |
$MODULE_DIR$、$MODULE_WORKING_DIR$ | そのモジュールのフォルダ | 分からなければ利用者に聞く |
${env:NAME} | 書かない(ログインシェルの環境から引き継がれる) | ログインシェルに無い変数なら利用者に値を聞く |
${file}、${input:…}、${command:…} | 変換しない | 開いているファイルや入力に依存する構成は対象外として伝える |
JetBrains の起動構成の読み方(type ごと)
| type | 変換 | 読む項目 |
|---|---|---|
ShConfigurationType(シェルスクリプト) | EXECUTE_SCRIPT_FILE が true: executable_path = INTERPRETER_PATH、args = INTERPRETER_OPTIONS + SCRIPT_PATH + SCRIPT_OPTIONSfalse: executable_path = /bin/zsh、args = -lc 'exec <SCRIPT_TEXT>' | SCRIPT_WORKING_DIRECTORY → working_dir、<envs> → env_text |
js.build_tools.npm(npm / pnpm / yarn) | executable_path = <package-manager> の名前(無ければ npm)、args = <command> + <scripts> + <arguments>(例 run dev) | <package-json> のフォルダ → working_dir、<envs> |
CargoCommandRunConfiguration | executable_path = cargo、args = command の値(例 run --bin api) | workingDirectory、<envs> |
MAKEFILE_TARGET_RUN_CONFIGURATION | executable_path = make、args = target + arguments | workingDirectory。ビルドだけのターゲットは変換しない |
| Python・Node.js・Go の実行構成 | インタプリタ(Python は SDK のパス、例 .venv/bin/python)+ スクリプトまたは -m モジュール + 引数 | 作業ディレクトリ・環境変数・引数の項目。Python には PYTHONUNBUFFERED=1 を足す |
| Java の Application・Spring Boot | クラスパスが IDE 任せなので直接は変換できない。./gradlew bootRun や ./mvnw spring-boot:run などの代わりを提案し、利用者に確かめる | Gradle / Maven の構成があればそのタスク名 |
VS Code・Cursor の読み方
- launch.json(
"request": "launch"のもの):node/pwa-node:runtimeExecutable(無ければnode)+runtimeArgs+program+args。runtimeExecutableがnpmならnpm run-script devのようになるpython/debugpy:pythonの指定(無ければ.venv/bin/pythonを探す)+-m moduleまたはprogram+argsgo:go run <program>+argslldb/cppdbg:program+args(ビルド済みの実行ファイル。preLaunchTaskのビルドは含めない)- 共通:
cwd→working_dir、env→env_text、envFile→env_files。デバッガ専用の設定(console・stopOnEntry・sourceMapsなど)は捨てる
- tasks.json:
"isBackground": trueのものや、名前・コマンドがdev/watch/serveのものが候補。"type": "process"はcommandを実行ファイル、argsを引数に。"type": "shell"はシェルの構文を含みうるので/bin/zsh -lc '…'に。options.cwd/options.envも読む
例
JetBrains の .idea/runConfigurations/dev.xml:
<configuration default="false" name="dev" type="js.build_tools.npm"> <package-json value="$PROJECT_DIR$/package.json" /> <command value="run" /> <scripts><script value="dev" /></scripts> <package-manager value="/opt/homebrew/bin/pnpm" /> <envs><env name="PORT" value="3000" /></envs> </configuration>
VS Code の .vscode/launch.json:
{
"configurations": [
{
"name": "API",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": ["app.main:app", "--reload"],
"cwd": "${workspaceFolder}/api",
"envFile": "${workspaceFolder}/api/.env"
}
]
}
プロジェクトのフォルダが ~/src/shop なら、変換結果は次のとおり。
[
{
"name": "dev",
"executable_path": "pnpm",
"args": "run dev",
"working_dir": "~/src/shop",
"env_text": "PORT=3000\n",
"workspace": "shop"
},
{
"name": "API",
"executable_path": ".venv/bin/python",
"args": "-m uvicorn app.main:app --reload",
"working_dir": "~/src/shop/api",
"env_text": "PYTHONUNBUFFERED=1\n",
"env_files": [".env"],
"workspace": "shop"
}
]
補足: pnpm は名前で書けば、ログインシェルの PATH で見つかる(元の構成の /opt/homebrew/bin/pnpm のような絶対パスでもよい)。Python の仮想環境の場所(.venv)が構成に書かれていなければ、フォルダの中を確かめるか利用者に聞く。
5. 構成の JSON の書式
取り込みは次の 3 つの形を受け付ける。形 1(構成の配列)を使うこと。
- 構成の配列
[ {…}, {…} ](推奨) - 構成 1 件のオブジェクト
{ "name": …, … } config.json全体({"version": 1, "configs": [ … ]}。settingsは取り込まれない)
すべてのフィールドに既定値があり、省略してよい。既定値と同じなら書かないほうが読みやすい。id は書かない(取り込み時に必ず新しく採番される)。
| キー | 型 | 既定値 | 意味・規則 |
|---|---|---|---|
name | 文字列 | "" | 表示名。空なら Untitled。重複してよい。CLI では名前(大文字小文字を区別しない)で指定するので、短く一意にするとよい |
executable_path | 文字列 | "" | 必須。実行ファイル。コマンド名(npm)・絶対パス・相対パス(作業ディレクトリ基準)。先頭の ~/ は展開。コマンド名はログインシェルの PATH で探す |
args | 文字列 | "" | 引数を 1 本の文字列で。空白区切り、"…" / '…' でくくれる、\ でエスケープ。シェルを通さない(下の 6 章) |
working_dir | 文字列 | "" | 作業ディレクトリ。空ならホーム。先頭の ~/ は展開。存在しなければ起動に失敗する |
env_text | 文字列 | "" | 環境変数。KEY=VALUE を \n で区切る。# で始まる行と空行は無視。KEY は [A-Za-z_][A-Za-z0-9_]*。値の引用符は外されない(A="x" は値が "x" になる)。$VAR は展開しない |
env_files | 文字列の配列 | [] | 読む .env ファイル。~ を展開、相対なら作業ディレクトリ基準。Run のたびに読む。書式は KEY=VALUE、export 可、"…" / '…' の引用符は外す、${VAR} は展開しない |
workspace | 文字列 | "" | まとめて表示を絞り込むための名前。空 = どこにも属さない |
restart | 文字列 | "no" | 自動再起動。"no" / "on-failure"(異常終了時)/ "always"(正常終了でも)。間隔は 1 秒から倍々で最大 30 秒、60 秒未満の終了が 10 回続くと諦める |
tty | 真偽値 | false | 疑似端末で動かす。出力がバッファされず、色も出る。stdout と stderr は 1 本にまとまる。入力は送れない |
stop_grace_secs | 整数 | 5 | Stop で SIGTERM を送ってから SIGKILL までの秒数。0〜300 |
log_enabled | 真偽値 | true | ログをファイル(~/Library/Logs/RetainProcess/<id>/current.log)に保存するか |
log_max_size_mb | 整数 | 5 | 1 ファイルの上限(MB)。1〜1024 |
log_max_files | 整数 | 3 | 残す世代数。0〜50。あふれた分は退避先へ移す(退避先が無ければ削除) |
log_file_per_run | 真偽値 | true | Run のたびに新しいログファイルで始める |
archive_dir | 文字列 / null | null | あふれたログの退避先(絶対パス)。通常は書かない(利用者がアプリのダイアログで選ぶ。未設定ならアプリ全体の退避先を使う) |
全項目を書いた例(実際の構成)
RetainProcess で使っている Ollama の構成を、全項目のまま載せる。省略した項目は表の既定値になるので、普段はここまで書かなくてよい。
{
"id": "8e01a5a9-2f9c-4b4f-a977-5b90bfa5e6dd",
"name": "Ollama",
"executable_path": "ollama",
"args": "serve",
"working_dir": "",
"env_text": "# OLLAMA_HOST=127.0.0.1:11434\n# OLLAMA_MODELS=~/.ollama/models\n",
"env_files": [],
"workspace": "AI",
"stop_grace_secs": 10,
"log_enabled": true,
"log_max_size_mb": 5,
"log_max_files": 3,
"log_file_per_run": false,
"archive_dir": null,
"restart": "no",
"tty": false
}
idは取り込み時に必ず採番し直されるので、書いてあっても使われない(新しく作るときは書かない)。working_dirが空なのでホームで動く。env_textの 2 行は#で始まるコメントで、使うときに外す。archive_dirはnull(未設定)。アプリ全体の退避先(Log Archive Home)があればそこのOllamaフォルダへ、無ければあふれたログは削除される。空文字列""ではなくnullにするか、項目ごと省く。
環境変数は次の順に重ね、後のものが勝つ: ログインシェルの環境 → env_files(書いた順)→ env_text。子プロセスには RETAIN_PROCESS=1 も渡る。
6. コマンドの起動のされ方(つまずきやすい点)
- シェルを通さない。
argsは分割して直接execveに渡す。パイプ|、&&、リダイレクト>、$VARの展開、グロブ*、argsの中の~はどれも効かない。必要ならexecutable_pathを/bin/zsh、argsを-lc 'cd sub && exec npm run dev'のようにする(最後のコマンドはexecで置き換えると、停止と監視が素直になる)。 - 前景で動き続けるコマンドにする。 自分でバックグラウンドに回るもの(
docker compose up -d、&、nohup、--daemon、brew services start)は、すぐ終わったように見えて管理できない。-dなどを外した前景版を使う。open(open -a X、open X.app)も同じで、アプリを開いてすぐ終わる。Mac アプリは 7 章の形にする。 - 入力は送れない。 パスワードの入力や確認(
y/n)を待つコマンドは止まったままになる。sudoは使わない。 - PATH はログインシェルの環境(
$SHELL -licで取得)。nvm・pyenv・Homebrew などで入れたコマンドは、ターミナルと同じように名前で見つかる。仮想環境の Python はexecutable_pathに.venv/bin/pythonのような作業ディレクトリからのパスを書くと確実。 - 出力のバッファ。 パイプにつながると出力をためて遅れて出すツールがある(Python など)。
PYTHONUNBUFFERED=1を足すか、"tty": trueにする。 - 色。 端末でないと色を出さないツールが多い。色が欲しければ
"tty": true、またはFORCE_COLOR=1/CLICOLOR_FORCE=1。 - 停止に時間がかかるもの(DB、
docker compose up、JVM など)はstop_grace_secsを 15〜30 に増やす。 - ポートの衝突。 同じポートを使うものが既に動いていると、Run は失敗する。
- 引き取り。 同じ実行ファイル・同じ引数のプロセスが RetainProcess の外で既に動いていると、それを引き取って監視・停止の対象にする(その場合、出力は表示できない)。
- 秘密。
env_textの値は設定ファイルに平文で保存される。トークンなどは.envファイルに置き、env_filesで参照するほうがよい。
7. 自分でビルドする Mac アプリ(Xcode・Swift Package)
RetainProcess には GUI アプリ(.app)を扱う専用の機能は無い。open で開くと open 自体がすぐ終わり、アプリは RetainProcess の外で動く(監視も停止もできない)。ただし、.app の中の実行ファイル(X.app/Contents/MacOS/X)を直接起動すれば、普通のプロセスとして管理できる。利用者が自分でビルドしているアプリ(Xcode のプロジェクト、XcodeGen、Swift Package の実行ターゲット)なら、ビルドから起動までを 1 つの構成にできる。
利用者に確かめること
プロジェクトのフォルダを読めるなら、先に読んで分かるものは聞かない。
- どのスキーム・ターゲットを動かすか(
xcodebuild -listで一覧が出る)。複数あるときは推測しない - 署名に何が要るか: 手元で動かすだけなら ad-hoc 署名でよいか、開発者の署名(チーム ID)が要る機能を使っているか(下の「署名」)。チーム ID は推測しない
/Applicationsに置かないと動かない機能(システム拡張・DriverKit など)を使っているか- 同じアプリを Finder などから開いて使うことがあるか(下の「二重起動」)
形
executable_pathは/bin/zsh、argsは-lc 'ビルド && exec 実行ファイル'。&&でつなぎ、ビルドに失敗したら起動しない。最後は必ずexecにする(zsh を残さず、アプリを RetainProcess の子にする)。working_dirはプロジェクトのフォルダ(.xcodeproj・project.yml・Package.swiftがある場所)。- Run のたびにビルドが走るので、起動までに時間がかかる。ビルドの出力も Logs に出る。ビルド済みのものを起動するだけでよければ、
executable_pathに実行ファイル(…/X.app/Contents/MacOS/X)を直接書く。 "tty": trueにする(下の「ログ」)。restartは既定の"no"のまま(alwaysだと、利用者が ⌘Q で閉じても起動し直す)。
ビルドの書き方
| プロジェクト | -lc '…' の中身 | 備考 |
|---|---|---|
Xcode(.xcodeproj) | xcodebuild -project X.xcodeproj -scheme X -configuration Debug -derivedDataPath build/xcode <署名> build && exec build/xcode/Build/Products/Debug/X.app/Contents/MacOS/X | -derivedDataPath を付けると出力の場所が決まる。.app の名前はスキームでなく製品名(PRODUCT_NAME)、MacOS/ の下の名前は CFBundleExecutable。違うことがあるので、ビルドの設定(xcodebuild -showBuildSettings)か一度ビルドした結果で確かめる |
Xcode のワークスペース(.xcworkspace、CocoaPods など) | -project X.xcodeproj の代わりに -workspace X.xcworkspace | CocoaPods なら Pods/ が揃っていること |
XcodeGen(project.yml) | 先頭に xcodegen generate && を足し、あとは Xcode と同じ | .xcodeproj を git に入れていないプロジェクトでは必須 |
| Swift Package の実行ターゲット | swift build --product X && exec .build/debug/X | AppKit / SwiftUI のメニューバーアプリなどを .app にせず動かしているもの |
ビルドと起動をまとめたスクリプト(scripts/run.sh、make run など) | 最後が open なら、スクリプトの中身をたどって「ビルド && exec 実行ファイル」に組み直す | スクリプトをそのまま登録すると、open の時点で終わってしまう。リポジトリのスクリプトを書き換えるかは利用者に聞く |
署名
- 手元で動かすだけなら ad-hoc 署名:
<署名>をCODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES CODE_SIGNING_REQUIRED=NOにする。コマンドラインからの自動署名は、Xcode にチームのアカウントが無いとNo signing certificate "Mac Development"などで失敗するため。 - 開発者の署名が要るもの: プロビジョニングプロファイルが要る機能(システム拡張・DriverKit、iCloud、プッシュ通知、App Groups など)を使うアプリは ad-hoc では動かない。
CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=<チーム ID> -allowProvisioningUpdatesにする(Xcode にそのチームのアカウントでサインインしていること)。チーム ID をリポジトリに入れていないプロジェクト(git 管理外の.xcconfigなどから読むもの)では、構成にも直接書かず、同じところから読む。 - ad-hoc 署名はビルドのたびに署名が変わる。キーチェーンに保存するアプリでは、ビルドのたびにアクセスの確認が出ることがある。
/Applicationsに置く必要があるもの(システム拡張・DriverKit を有効化するアプリなど): ビルドした.appを/Applicationsにコピーしてから、/Applicationsの中の実行ファイルをexecする。コピーの前に、動いている同じアプリを終了させる(下の例の 4 件目)。
動き方の違い(利用者に伝える)
- Stop はアプリの通常の終了ではない。 Stop は SIGTERM を送る。AppKit / SwiftUI のアプリは SIGTERM ですぐ終わり、通常の終了処理(
applicationWillTerminate、未保存の確認、状態の保存)を通らない。きれいに終わらせたいときは、アプリの側で ⌘Q する。⌘Q やウィンドウを閉じてアプリが終わると、RetainProcess には終了として表示される。 - 二重起動。 Launch Services(
openや Finder の起動の仕組み)を通らないので、同じアプリが既に開いていても、もう 1 つ起動する。/bin/zsh -lcで包んだ構成では、6 章の「引き取り」も効かない(構成の実行ファイルが zsh のため)。外で開いているものは先に終了させるか、ビルドの後にosascript -e "quit app \"X\""で終了させてから起動する。 - ログ。 stderr はすぐ出る。
printなどの stdout は、パイプにつながると出力がためられて遅れて出るうえ、Stop(SIGTERM)で止めるとためた分が出ないまま失われる。"tty": trueにすると行ごとに出る。os_log/Loggerの出力は Logs に出ない(統合ログに行く。/usr/bin/log stream --predicate 'process == "X"'を別の構成にすると見られる)。 - CPU・メモリはアプリのプロセスグループの合算。XPC サービスやヘルパーのうち launchd が起動するものは含まれない。
- 許可(プライバシーとセキュリティ)。 カメラ・マイク・画面収録・音声認識などの許可は、macOS が起動元のアプリの分として扱うことがある。この形で起動したアプリでは、許可を RetainProcess の名前で求められたり、Finder から開いたときと別扱いになったりする可能性がある。許可が要るアプリなら、そのことを利用者に伝える。
- Dock にはアイコンが出る(
LSUIElementのアプリは出ない)。
対象外
- 既製のアプリ(Safari・ミュージック・Slack など、利用者がビルドしていないもの)は登録しない。中の実行ファイルを直接起動しても、二重起動になる、自動アップデートで終了して
openで開き直すと RetainProcess の外に出る、などで管理し続けられない。 - iOS・iPadOS・watchOS などのアプリ(シミュレータや実機で動かすもの)。Mac Catalyst のアプリは Mac アプリとして扱える。
- デバッガをつないで動かすこと(Xcode の Run)。
例
上から順に、Xcode のプロジェクト(ad-hoc 署名)、XcodeGen、Swift Package、開発者の署名が要り /Applications に置くアプリ。DEVELOPMENT_TEAM の値は例で、実際は利用者のチーム ID を使う。
[
{
"name": "MyApp (Debug)",
"executable_path": "/bin/zsh",
"args": "-lc 'xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Debug -derivedDataPath build/xcode CODE_SIGN_IDENTITY=\"-\" CODE_SIGNING_ALLOWED=YES CODE_SIGNING_REQUIRED=NO build && exec build/xcode/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp'",
"working_dir": "~/src/MyApp",
"tty": true
},
{
"name": "Notes (XcodeGen)",
"executable_path": "/bin/zsh",
"args": "-lc 'xcodegen generate && xcodebuild -project Notes.xcodeproj -scheme Notes -configuration Debug -derivedDataPath build/xcode CODE_SIGN_IDENTITY=\"-\" CODE_SIGNING_ALLOWED=YES CODE_SIGNING_REQUIRED=NO build && exec build/xcode/Build/Products/Debug/Notes.app/Contents/MacOS/Notes'",
"working_dir": "~/src/notes/apps/Notes",
"tty": true
},
{
"name": "MenuTool",
"executable_path": "/bin/zsh",
"args": "-lc 'swift build --product MenuTool && exec .build/debug/MenuTool'",
"working_dir": "~/src/MenuTool",
"tty": true
},
{
"name": "AudioDriverApp",
"executable_path": "/bin/zsh",
"args": "-lc 'xcodebuild -project AudioDriverApp.xcodeproj -scheme AudioDriverApp -configuration Debug -derivedDataPath build/xcode CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=ABCDE12345 -allowProvisioningUpdates build && { osascript -e \"quit app \\\"AudioDriverApp\\\"\" >/dev/null 2>&1 || true; sleep 1; } && rm -rf /Applications/AudioDriverApp.app && cp -R build/xcode/Build/Products/Debug/AudioDriverApp.app /Applications/ && exec /Applications/AudioDriverApp.app/Contents/MacOS/AudioDriverApp'",
"working_dir": "~/src/AudioDriverApp",
"tty": true
}
]
8. JSON の例
次の配列をそのまま取り込める(1 件だけでも配列にする)。
[
{
"name": "Blog Dev",
"executable_path": "npm",
"args": "run dev -- --port 4321",
"working_dir": "~/src/blog",
"env_text": "NODE_ENV=development\nFORCE_COLOR=1\n",
"workspace": "Blog"
},
{
"name": "API (uvicorn)",
"executable_path": ".venv/bin/python",
"args": "-m uvicorn app.main:app --reload --port 8000",
"working_dir": "~/src/shop/api",
"env_text": "PYTHONUNBUFFERED=1\n",
"env_files": [".env"],
"restart": "on-failure"
},
{
"name": "Ollama",
"executable_path": "ollama",
"args": "serve",
"env_text": "OLLAMA_HOST=127.0.0.1:11434\n",
"stop_grace_secs": 10
},
{
"name": "Docker Stack",
"executable_path": "docker",
"args": "compose up",
"working_dir": "~/src/shop",
"stop_grace_secs": 30
},
{
"name": "Worker (shell)",
"executable_path": "/bin/zsh",
"args": "-lc 'source ./env.sh && exec bundle exec sidekiq'",
"working_dir": "~/src/shop/backend",
"tty": true,
"restart": "always"
}
]
補足: docker compose up の場合、CPU・メモリに出るのは docker の CLI の分だけで、コンテナ自体(Docker の VM の中)は含まれない。
9. スタック(retainprocess.yml)の書式
docker compose のファイルの部分集合。プロジェクトのディレクトリに retainprocess.yml(.yaml も可)として置く。ファイルが正で、アプリ側では編集しない(変えたらファイルを直す)。
トップレベル
name: スタック名(省略時はディレクトリ名)services: サービス名をキーにしたマップ。サービス名は[a-zA-Z0-9][a-zA-Z0-9_.-]*
サービスのキー(これ以外は読み飛ばされ、警告が出る)
| キー | 書き方 |
|---|---|
command | 必須。文字列(JSON の args と同じ分割)か配列。先頭の語が実行ファイル。シェルは通さない |
working_dir | このファイルのディレクトリからの相対パス、または絶対パス。省略時はファイルのディレクトリ |
environment | マップ(KEY: value)か配列(- KEY=value) |
env_file | 文字列か配列。相対パスは このファイルのディレクトリ基準(working_dir 基準ではない) |
depends_on | 配列(= 起動したら次へ)、またはマップで condition: service_started / service_healthy(準備完了を待つ) |
healthcheck | port: 5432(そのポートで待ち受けたら準備完了。any なら何かのポート)か http: http://localhost:8080/health(2xx で準備完了。ホストは localhost / 127.0.0.1 / [::1] だけ、リダイレクトは追わない)。interval(既定 1s)、timeout(準備完了を待つ上限、既定 60s) |
stop_grace_period | 既定 5s |
restart | no / on-failure / always(unless-stopped は always と同じ) |
tty | true / false |
x-retainprocess | RetainProcess 独自の設定: log_enabled / log_max_size_mb / log_max_files / log_file_per_run / archive_dir / workspace(意味は JSON と同じ) |
- 時間は
500ms/10s/1m30s/2h、または数だけ(秒)。 image・ports・volumes・build・profilesなどは使えない(読み飛ばされる)。コンテナが要るものは、docker compose upを 1 つのサービスのcommandにする。${VAR}は展開しない。:や#を含む値、数字だけの値(ポート番号など)をcommandの配列やenvironmentに書くときは"…"でくくる。- 依存の循環、存在しないサービスへの依存、空の
commandはエラーになり、そのスタックは操作できない。
例
name: shop
services:
db:
command: postgres -D ./data -p 5432
working_dir: backend
healthcheck:
port: 5432
timeout: 1m30s
stop_grace_period: 15s
api:
command: [.venv/bin/python, -m, uvicorn, "app.main:app", --port, "8000"]
working_dir: api
env_file: api/.env
environment:
PYTHONUNBUFFERED: "1"
depends_on:
db:
condition: service_healthy
healthcheck:
http: http://localhost:8000/health
restart: on-failure
web:
command: npm run dev
working_dir: web
tty: true
depends_on: [api]
x-retainprocess:
workspace: Shop
Up は依存の順に起動し(service_healthy なら準備完了を待つ)、Down は逆順に止める。
10. Mac に最初から入っているもので試す例
利用者が「まず試したい」「何を登録すればよいか分からない」ときは、インストール不要の macOS 標準のコマンドで例を作る。どれも前景で動き続け、Stop で止まる。
| やりたいこと | コマンド | 見どころ |
|---|---|---|
| Mac をスリープさせない(Run 中だけ) | /usr/bin/caffeinate -d -i | Run でスリープ防止、Stop で元に戻る。トグルのように使える |
| アプリのログを流し見る | /usr/bin/log stream --style compact --predicate 'process == "Finder"' | Logs タブと検索の練習。出力が多いのでログの世代交代も確かめられる |
| ネットワークの疎通を見張る | /sbin/ping -i 10 www.apple.com | 10 秒ごとに 1 行。途切れたら Logs で分かる |
| メモリの状況を記録する | /usr/bin/vm_stat 60 | 1 分ごとの記録がログファイルに残る |
| ポートで待ち受ける | /usr/bin/nc -lk 127.0.0.1 9000 | ヘッダーに :9000 が出る。別のターミナルで echo hello | nc 127.0.0.1 9000 とすると Logs に出る |
| 定期的に何かする(シェルのループ) | /bin/zsh -c 'while true; do date; sleep 60; done' | シェルの構文が要るときの書き方の例 |
JSON の例
[
{
"name": "Keep Awake",
"executable_path": "/usr/bin/caffeinate",
"args": "-d -i",
"log_enabled": false
},
{
"name": "Finder Log",
"executable_path": "/usr/bin/log",
"args": "stream --style compact --predicate 'process == \"Finder\"'",
"log_max_size_mb": 2,
"log_max_files": 2
},
{
"name": "Ping Watch",
"executable_path": "/sbin/ping",
"args": "-i 10 www.apple.com"
},
{
"name": "Memory Stats",
"executable_path": "/usr/bin/vm_stat",
"args": "60",
"workspace": "Monitor"
},
{
"name": "Port 9000",
"executable_path": "/usr/bin/nc",
"args": "-lk 127.0.0.1 9000"
},
{
"name": "Clock",
"executable_path": "/bin/zsh",
"args": "-c 'while true; do date; sleep 60; done'"
}
]
スタックの例(依存と準備完了を試す)
listener がポート 9000 で待ち受けてから、sender が 5 秒ごとに時刻を送る。retain-process up で順に起動し、listener の Logs に時刻が流れる。
name: mac-demo
services:
listener:
command: /usr/bin/nc -lk 127.0.0.1 9000
healthcheck:
port: 9000
sender:
command: [/bin/zsh, -c, "while true; do date | /usr/bin/nc -w 1 127.0.0.1 9000; sleep 5; done"]
depends_on:
listener:
condition: service_healthy
restart: on-failure
できないこと: Safari・ミュージック・メモなどの既製の GUI アプリは管理できない(open -a … はすぐ終わり、監視も停止もできない。中の実行ファイルを直接起動しても管理し続けられない)。利用者が自分でビルドしている Mac アプリは 7 章の形で登録できる。sudo が要るもの(powermetrics、fs_usage など)も使えない。python3 / ruby / git は Command Line Tools が無い Mac ではインストールを促すダイアログが出るだけなので、「標準のコマンド」の例には使わない。
log は zsh の組み込みコマンドと名前が重なる。RetainProcess はシェルを通さないので log でも /usr/bin/log が動くが、例では絶対パスで書くと誤解が無い。
11. 出す前のチェックリスト
- JSON は構成の配列で、構文として正しい(末尾のカンマなし、
env_textの改行は\n)。 - 各構成に
nameとexecutable_pathがある。idは書いていない。 argsにパイプ・&&・$VAR・~が無い(あれば/bin/zsh -lc '…'に)。- 前景で動き続けるコマンドになっている(
-dや&、openが無い)。入力を待たない。sudoが無い。 - Mac アプリなら: ビルドと起動を
&&でつなぎ、最後が.app/Contents/MacOS/…のexecになっている。署名の方法と/Applicationsに置く必要があるかを確かめた。チーム ID を推測で書いていない。"tty": true、restartは"no"。Stop・二重起動・許可の違い(7 章)を利用者に伝えている。 env_textの値を引用符でくくっていない。秘密はenv_files側にある。- 既定値と同じフィールドを省いている。
archive_dirを勝手に決めていない。 - YAML なら、使えないキー(
imageなど)を使っていない。依存が循環していない。 - 推測で埋めた値(パス、ポート)があれば、その旨を利用者に伝えている。
12. 利用者に伝える反映のしかた
JSON の場合
- JSON をコピーする。
- メニューバーの RetainProcess のアイコンをクリックし、ポップアップの左ペインの
⤓→Import from Clipboard。ファイルに保存したならImport from File…。 - 構成は一覧の末尾に追加される(既存の構成は変わらない。同じものを 2 回取り込むと 2 件になる)。自動では起動しないので、選んで
▶ Run。 - 直すときは Configuration タブで編集する(自動で保存される。動いている間の変更は次の Run から効く)。
YAML の場合
retainprocess.ymlをプロジェクトのディレクトリに保存する。- そのディレクトリで
retain-process up(初回はスタックの登録も行う)。状態はretain-process ps、停止はretain-process down。 - ファイルを直したら、もう一度
retain-process up(または、ポップアップのスタックの表示でReload)。
CLI の retain-process は、アプリが起動していないと終了コード 2 で「RetainProcess is not running」と返す。その場合は open -a RetainProcess で起動してもらう。