silverpoint
フック: 保存や書き出しのあとにコマンドを実行
フックは、Silverpoint がドキュメントの保存や書き出しを終えたときに Mac 上でコマンドを実行します。tokens.json をリポジトリにコピーしたり、書き出した Swift を整形したり、書き出したファイルをステージしたりでき、保存や書き出しを止めたり失敗させたりすることはありません。

フックは、Silverpoint がドキュメントの保存や書き出しを終えたときに Mac 上でコマンドを実行します。tokens.json をリポジトリにコピーする、書き出した Swift を整形する、書き出したファイルをステージする、といった使い方ができます。フックはイベントが終わったあとにメインアクターの外で実行され、きっかけになった保存や書き出しを止めたり失敗させたりすることはありません。フックが出力した内容と終了の仕方は、開いて確認できるログに残ります。

フックはあなた(またはあなたのチーム)が書いたコマンドです。ドキュメントのフックはほかの人から届いたかもしれないファイルに入っているので、あなたが許可するまで実行されません。信頼を参照してください。

フックの置き場所

ソース場所信頼
ユーザ~/Library/Application Support/Silverpoint/hooks.json、または $SILVERPOINT_HOOKS_FILE が指すファイル信頼済み: あなたが書いたもの
書類.silverpoint ドキュメントの hooks キー。チームのフックがファイルと一緒に持ち運ばれますそのドキュメントのパスに対して許可するまで実行されません

ほかの 2 つの場所も同じルールに従い、環境変数で移動できます(名前変更前の VELLUM_ の名前もフォールバックとして使えます)。

項目デフォルト上書き
ユーザのフックファイル~/Library/Application Support/Silverpoint/hooks.jsonSILVERPOINT_HOOKS_FILE
承認~/Library/Application Support/Silverpoint/hook-approvals.jsonSILVERPOINT_HOOK_APPROVALS_FILE
ログ~/Library/Application Support/Silverpoint/HookLogs/SILVERPOINT_HOOK_LOGS_DIR

ユーザファイルの形式は {"hooks": [hook, ...]} です。ドキュメントはリストそのものを "children" の隣に "hooks": [hook, ...] として保存します。フックのないドキュメントには hooks キーそのものがないので、これまでとバイト単位で同じに保存されます。

1 つのイベントが両方のソースのフックに一致したときは、あなたのフックが先、ドキュメントのフックがあとに、書かれた順に 1 つずつ実行されます。無効にしたフック("enabled": false)はスキップされます。

フックの書き方

{
  "id": "sync-tokens",
  "event": "afterExport",
  "filter": ["tokens"],
  "command": ["/bin/cp", "tokens.json", "../my-app/Sources/Design/tokens.json"],
  "cwd": ".",
  "env": { "MODE": "release" },
  "timeout": 30,
  "enabled": true
}
フィールド意味
id必須。ファイル内で一意です。ログ、通知、list_hooks、環境変数でフックを示す名前になります。
event必須: afterSave、afterExport、afterExportCode のいずれか。
filter省略可能な文字列のリスト。afterExport では実行する書き出しの種類(image、pdf、code、tokens、designSystem、handoff、site)。afterExportCode では実行するコードターゲットの id(swiftui、html、compose、react、uikit など)。省略するとすべてが対象です。afterSave には指定できません。
command必須: argv、つまり空でない文字列の配列です。シェルでは実行されません。["/bin/echo", "$HOME"] は文字どおり $HOME と出力します。最初の項目はパスです。git のような名前だけの場合は PATH から探します(Dock から起動したアプリの PATH は短いので、フルパスか /usr/bin/xcrun を使うのがおすすめです)。
shelltrue にすると、コマンドを /bin/sh -c で実行します。command[0] がスクリプトで、残りがその $0、$1... になります。デフォルトは false。
cwd作業ディレクトリ。デフォルトはドキュメントのあるフォルダです。相対パスはそのフォルダからの相対パス、絶対パス(または ~/...)はそのまま使われます。まだ保存していないドキュメントでは、あなたのフックはホームフォルダから実行されます。
env追加の環境変数(文字列)。Silverpoint の起動時の環境に追加されます。
timeout秒数で、0 より大きい値。デフォルトは 60。
enabledデフォルトは true。

検証

フックは 1 つずつ検証されます。検証を通らないフックは報告されてスキップされ、ほかのフックはそのまま実行されます。理由は次のとおりです。id がない、または重複している、event が不明、command が空または文字列でない、timeout が 0 より大きくない、filter の種類が存在しない(または afterSave に filter がある)、shell、cwd、env、enabled の型が違う。それぞれの問題には、ソース、フックの id または位置、理由が示されます: user hook #2 "typo": unknown event "afterSav" (use afterSave, ...)。ユーザファイルが有効な JSON でない(または {"hooks": [...]} でない)場合は 1 つの問題としてフックはなしになり、ドキュメントのフックには影響しません。問題は設定 ▸ 一般 ▸ フックと、list_hooks の errors に表示されます。

イベント

イベント発生するときペイロードの files
afterSaveファイルが書き込まれたあと。あなたが求めた保存、つまり保存、別名で保存、複製の最初の保存のとき。自動保存では発生しません(アプリはその場で絶えず自動保存するので、自動保存ごとにフックを実行すると常に動き続けることになります)。silverpoint call と mcp は、--run-hooks を付けると保存された編集ごとに発生させます。保存されたパッケージ: [documentPath]
afterExportファイルが書き込まれたあと。種類は filter で指定します: image(ファイル ▸ 書き出す… で PNG、JPEG、SVG として書き出したとき、フレームをPNGとして書き出す、export ツール、インスペクタの <名前>を書き出す… と、レイヤーの PNG、JPG、SVG のプリセットを書き出す export_presets。まとめて 1 回の書き出しです)、pdf(書き出す… で PDF として書き出したとき、デッキをPDFとして書き出す、PDF のプリセットを書き出す <名前>を書き出す… と export_presets)、code(コードを書き出す)、tokens(トークンの形式を選んだコードまたはトークンを書き出す、export_tokens)、designSystem(デザインシステムパッケージとして書き出す、export_design_system)、handoff(開発者向けハンドオフを書き出す、export_handoff と hand_off)、site(ウェブサイトのテーマとそのコンテンツ。ファイル ▸ ウェブサイトを公開… の公開で、ビルドの前に 1 回。mode: apply を付けた export_site。silverpoint publish はフックを実行しません)書き込まれたファイル
afterExportCode種類が code の afterExport でも、あわせて発生します。コードターゲットを持ち、それで絞り込めます書き込まれたファイル

知っておきたい点:

  • 書き出しでイベントが発生するのは、ファイルを書き込んだときだけです。path を指定しない export_tokens と export_code はテキストを返すだけで何も書き込まないので、何も発生しません。render と score コマンドがフックを発生させることはありません。
  • 複数のファイルになるコードの書き出し(Compose、React、UIKit)では、そのフォルダのすべてのファイルが files に入ります。
  • hand_off の files は、バンドルの written リストを絶対パスにしたものです(ツール自身のリストはバンドルからの相対パス)。tokensPath はバンドルの tokens.json です。export_handoff の tokensPath はその tokens.json です。どちらも handoff の書き出しです。
  • 各イベントは共通のツールカタログ(DesignTools)が通知するので、アプリ内のエージェントや CLI も、メニューコマンドと同じイベントを発生させます。

コマンドが受け取るもの

stdin には JSON オブジェクトが 1 つ渡されます(スラッシュはエスケープされず、キーはソートされます)。

{
  "event": "afterExportCode",
  "kind": "code",
  "documentPath": "/Users/me/Design/Pulse.silverpoint",
  "files": ["/Users/me/app/Design.swift"],
  "codeTarget": "swiftui",
  "tokensPath": null,
  "hookId": "format-swift"
}

kind は書き出しの種類(保存では null)、codeTarget はコードの書き出しで、tokensPath はトークン、デザインシステム、ハンドオフの書き出しで設定されます。documentPath は、まだ保存していないドキュメントでは null です。当てはまらないフィールドは null になります。

同じ値が環境変数にも入ります。常に設定され、当てはまらないときは空です。

変数値
SILVERPOINT_HOOK_EVENTafterSave、afterExport、afterExportCode のいずれか
SILVERPOINT_HOOK_KIND書き出しの種類
SILVERPOINT_HOOK_DOCUMENTドキュメントのパス
SILVERPOINT_HOOK_FILESファイル。1 行に 1 つ
SILVERPOINT_HOOK_CODE_TARGETコードターゲットの id
SILVERPOINT_HOOK_TOKENS_PATHトークンファイル
SILVERPOINT_HOOK_IDフックの id

これらは Silverpoint の起動時の環境とフックの env に追加され、SILVERPOINT_HOOK_* の値が優先されます。

実行、タイムアウト、まとめ実行とログ

  • メインアクターの外で、イベントのあとに。 フックが始まる前に、保存や書き出しは終わっていて、その結果も使える状態です。起動に失敗した、0 以外で終了した、タイムアウトしたフックは記録されますが、保存や書き出しにエラーを投げることはありません。
  • タイムアウト。 timeout より長く動いたフックには SIGTERM が送られ、2 秒後に SIGKILL が送られて、timed out として記録されます。シグナルが送られるのはフック自身のプロセスだけです。shell: true の場合、スクリプトがバックグラウンドで起動したプログラムはスクリプトより長く残ることがあります。
  • まとめ実行。 まだ実行中のフック(ドキュメントとフックの id ごと)は、次の保存や書き出しで再び起動されません。そのきっかけは待機し、2 つ目が来ればそれに置き換わり、最初の実行が終わると最新のペイロードで1 回だけ追加で実行されます。フックの実行中に 10 回続けて保存しても、追加の実行は 10 回ではなく 1 回です。
  • ログ。 ログフォルダに、ドキュメントごとに 1 つのテキストファイルがあります。名前はドキュメント名とフルパスのダイジェストからなります(そのため a/Pulse.silverpoint と b/Pulse.silverpoint には別々のログがあります)。実行のたびにエントリが追記されます。時刻、フックの id、ソースとイベント、argv、終了ステータス(exit 0、exit 1、signal 9、timed out、failed to launch: ...)、stdout と stderr(それぞれ先頭 256 KB)です。設定 ▸ 一般 ▸ フック ▸ ログを表示 で、最前面のドキュメントのログが開きます。
  • 通知。 失敗した、起動に失敗した、タイムアウトしたフックは、画面の隅に ログを表示 付きの小さな通知を表示します。フォーカスは奪わず、自然に消えます。0 で終了したフックは何も表示しません。

信頼

フックは、あなたの権限で Mac 上で実行されるコマンドです。そのため:

  • あなたのフックファイルは信頼されます。 あなたが書いたものだからです。
  • ドキュメントのフックは、そのドキュメントに対して許可するまで実行されません。 フックのあるドキュメントを開いたとき、またはそのフックが変わったとき(設定での編集、取り消し、ディスク上でのファイルの変更)、Silverpoint は各フックの正確なコマンド(とシェルのフラグ、フォルダ、環境変数)を 許可 と 許可しない とともに表示します。許可するとフックが記録されます。許可しないと何も記録されず、フックはオフのままです(同じフックについては、変わるまで再び確認されません)。
  • 承認は、ドキュメントのパスごとに、そのドキュメントのフックリスト全体に対する 1 つのハッシュです。 リストを順番どおりに正規化した(キーをソートした)JSON に対する SHA-256 なので、何が実行されるかを決めるものすべてを含みます。各フックの id、event、filter、command、shell、cwd、env、timeout、そしてどのフックがどの順にあるかです。enabled は含みません。そのため、フックをオフにしてまたオンにしても承認はそのままです。どれか 1 つのフックを編集したり、フックを追加、削除、並べ替えたりすると、そのドキュメントのすべてのフックが未承認になり、もう一度許可するまで実行されません(変更していないフックも止まります)。別のパスにあるコピーや、移動や名前変更のあとの同じフックは承認されていません(新しいパスには新しい承認が必要で、何も引き継がれません)。承認ファイルはドキュメントのパスごとに 1 つのエントリを持ちます: {"approvals": {"/path/to/Doc.silverpoint": "<sha256>"}}。
  • 設定のフックフォームであなたが加えた変更は、保存した時点で承認されます。 ただし、変更前のドキュメントのリストが承認済みだった場合に限ります(設定を参照)。それ以外の変更、取り消し、やり直しでは、もう一度確認されます。
  • 確認 は、設定 ▸ 一般 ▸ フックでいつでもできます。ドキュメントのフックは承認済みか未承認かの 1 つの状態を共有し、確認… でシートをもう一度表示できます。
  • silverpoint call と silverpoint mcp は、--run-hooks を付けない限りフックを実行しません。 付けた場合も、実行するのはあなたのフックと、アプリで承認したドキュメントのフックだけです。フラグがなければ、承認ファイルやログを読み書きすることはありません。
  • アプリ自身の MCP サーバーは、クライアントが何を求めてもフックを実行しません。 それらのツールを動かしているのは別のプログラムだからです。アプリ内のエージェントはあなたがアプリに指示しているものなので、その書き出しはメニューコマンドと同じようにフックを実行します。
  • フックを承認、書き込み、切り替えできるツールはありません(できるのは設定でのあなただけです)。list_hooks は読み取り専用です。各フックの id、event、filter、command、shell、cwd、timeout、enabled、source(user または document)、approval(trusted、approved、notApproved)と、検証を通らなかったフックを返します。エージェントはあなたが追加するフックを提案できますが、自分で追加することはできません。

設定

設定 ▸ 一般 ▸ フックには、最前面のドキュメントで有効なフックが ユーザ または 書類 のバッジ付きで一覧され、ドキュメントのフックにはそのドキュメントのフックが承認済みかどうか(1 つの状態を共有します)が表示されます。フックを追加… でフックフォームが開きます。各行には 編集(同じフォームに値が入った状態)、今すぐ実行、複製、削除 とスイッチがあります。Finderに表示 はあなたのフックファイルを、ログを表示 はドキュメントのログを、確認… は承認シートを表示します。検証を通らなかったフックは行の下に一覧されます。このグループが一般にあるのはそのカードに余裕があったからで、ドキュメントのフックはスキルのカードと同じ最前面のドキュメントを使います。

フックを追加… と編集

フォームにはフックのすべてのフィールドがあります。

  • 保存先: あなたのフックか、このドキュメントのフック(最前面に保存済みのドキュメントがあるときだけ選べます。新しいドキュメントのフックには、その承認を保存するためのパスが必要です)。編集では、フックは元の場所のままです。
  • 実行のタイミング: 保存のあと、書き出しのあと(種類にチェックを付けます。何もチェックしなければすべての種類のあとに実行)、コードの書き出しのあと(コードターゲットの id をカンマ区切りで。空ならすべてのターゲットのあとに実行)。保存のあとに切り替えると filter は消えます。
  • コマンド: 1 行で、下のルールに従って argv に分割されます。シェルで実行(/bin/sh -c) をオンにすると、その行がスクリプトそのものになり、入力したとおりに command の 1 項目として保存され、シェルが展開します。
  • 作業フォルダ(空欄ならドキュメントのフォルダ)、環境変数(名前は英字、数字、_ で、数字で始まらないこと。それぞれ 1 回だけ設定でき、値は空でもかまいません)、タイムアウト(1 から 3600 秒、デフォルトは 60)と オン。
  • id はプログラム名から作られ(/opt/tools/xcrun swift-format なら xcrun)、リストの中で一意になるようにします(echo、echo-2…)。フックを追加している間はコマンドに合わせて変わり、保存したあとは変わりません。

各フィールドには問題がその横に表示され、問題がなくなるまで 保存 は無効のままです。フックが検証を通るまで何も書き込まれません。新しいフックは id、event、command と、デフォルトから変更したフィールドだけを書き込みます。編集は書かれているとおりのフックから始まるので、フォームが知らないキー(note や独自のキー)は残り、変更していないフックを保存しても何も書き込まれません。JSONとして編集… はフォームを閉じ、あなたの hooks.json をデフォルトのアプリで開きます(まだなければ {"hooks": []} として作成されます)。ドキュメントのフックの場合は、ドキュメントのフックを JSON 配列としてシートで開き、1 回の取り消し操作として保存します。フォームで表示できないフック(引数付きのシェルスクリプト、command の項目が 2 つ以上ある shell: true)は、その説明と JSONとして編集… だけで開きます。

コマンド行のルール。 行は、シェルの小さく予測しやすい一部と同じやり方で単語に分割され、何も展開されません。

  • 引用符の外では、空白(スペース、タブ、改行)が単語を区切ります。
  • シングルクォートはリテラルの部分を始めます。次のシングルクォートまでのすべてがそのまま使われます('$HOME' は $HOME)。
  • ダブルクォートは、\"(ダブルクォート)と \\(バックスラッシュ)だけがエスケープになる部分を始めます。それ以外のバックスラッシュは次の文字と一緒に残ります("a\nb" は a\nb の 4 文字)。
  • 引用符の外では、バックスラッシュは次の文字をリテラルにします(x\ y は 1 つの単語 x y)。
  • 隣り合う部分は 1 つの単語につながり(a"b c"d は ab cd)、'' や "" は空の単語です。
  • $ の展開、グロブ、~、パイプはありません。それらには シェルで実行 が必要です。
  • エラーは 4 つで、それぞれフィールドの下に表示されます。閉じていないシングルクォート、閉じていないダブルクォート、末尾のバックスラッシュ(最後にあって何もエスケープしないもの)、空の行です。

フックの argv も同じやり方で表示されます。空白、引用符、バックスラッシュ、$、バッククォートを含まない単語はそのまま、それ以外はシングルクォートで囲み(中の ' は '\'' と書きます)、行が必ず同じ argv に分割し直せるようにします。

複製、削除とスイッチ

複製 は、フックのすぐあとにコピーを置きます。id 以外は書かれているとおりです(<id>-copy、続いて <id>-copy-2…)。削除 は、フックとそのコマンドを示して先に確認します。スイッチは enabled を書き換えます。

あなたのフックは hooks.json に丸ごと書き込まれます({"hooks": [...]}、整形済み、キーはソート、アトミックに置き換え。フォルダやファイルがなければ作成されます)。ほかのフックは、検証を通らないものも含めて書かれているとおりに残ります。設定が何かを失わずに書き戻せないファイルは拒否され、そのまま残ります。コメントがあるファイルや JSON でないファイル、{"hooks": [...]} でないファイル、hooks の隣に別のトップレベルのキーがあるファイル(unknown top-level key "extra" (only "hooks"))です。設定はその理由を示し、JSONとして編集… を提示します。ドキュメントのフックはドキュメントを通じて変更され、それぞれ 1 回の取り消し操作になります(フックを追加、フックを編集、フックを複製、フックを削除、フックの有効化、フックの無効化)。

信頼。 フォームで変更した(追加、編集、複製、削除、切り替えた)ドキュメントのフックリストは、変更がドキュメントに届く前、保存した時点で承認されます。ただし、変更前のリストが承認済みだった場合に限ります(空のリストも承認済みとみなします)。あなたが書いたのは変更部分であって、残りではないからです。承認されていなかったリストは未承認のままで、これまでどおり確認されます。JSONとして編集… のシートは承認せず、フォームでの変更を取り消したりやり直したりすると、確認するまでリストは未承認になります。あなた自身のフックに承認は必要ありません。

今すぐ実行

今すぐ実行 は、イベントがちょうど起きたかのようにフックを 1 回実行し、その結果を表示します。ステータス(exit 0、exit 1、timed out…)、かかった時間、stdout と stderr の最後の 40 行です。何も保存や書き出しはされません。afterSave のフックはドキュメントのパッケージを 1 つのファイルとして受け取り(ドキュメントがなければなし)、afterExport のフックは filter の最初の種類を受け取ってファイルはなし、afterExportCode のフックは kind: "code" と filter の最初のターゲットを受け取ります。あなたのフックはドキュメントなしで実行されます(ドキュメントのパスはなく、cwd で指定しない限りホームフォルダで実行)。ドキュメントのフックには、ドキュメントが保存済みで、そのフックが承認済みである必要があります(今すぐ実行は確認してから実行します)。オフにしたフックも実行できます。実行はログに残り、失敗すればほかと同じように通知が表示されます。

例

それぞれを ~/Library/Application Support/Silverpoint/hooks.json(またはドキュメントの hooks)に入れます。

トークンの書き出しのあとに tokens.json をリポジトリにコピーする(ファイル ▸ コードまたはトークンを書き出す… ▸ トークン: JSON)。$SILVERPOINT_HOOK_TOKENS_PATH は書き込まれたばかりのファイルなので、それを展開するにはシェルが必要です。

{
  "hooks": [
    {
      "id": "sync-tokens",
      "event": "afterExport",
      "filter": ["tokens"],
      "shell": true,
      "command": ["cp \"$SILVERPOINT_HOOK_TOKENS_PATH\" /Users/me/code/my-app/Design/tokens.json"]
    }
  ]
}

ドキュメントの隣にある決まったファイルなら、シェルは必要ありません。{"id": "copy-tokens", "event": "afterExport", "filter": ["tokens"], "command": ["/bin/cp", "-f", "tokens.json", "../my-app/Design/tokens.json"]}はドキュメントのフォルダで cp を実行します。

書き出したばかりの Swift に swift-format をかける(SwiftUI のコードターゲット)。ファイルは $SILVERPOINT_HOOK_FILES に改行区切りで(stdin には配列として)届くので、スペースを含むパスも tr と xargs -0 を通り抜けます。

{
  "hooks": [
    {
      "id": "format-swift",
      "event": "afterExportCode",
      "filter": ["swiftui"],
      "shell": true,
      "command": ["printf '%s' \"$SILVERPOINT_HOOK_FILES\" | tr '\\n' '\\0' | xargs -0 /usr/bin/xcrun swift-format -i"],
      "timeout": 120
    }
  ]
}

書き出したファイルを git add する(コード、トークン、ハンドオフの書き出しを、書き込み先のリポジトリに)。

{
  "hooks": [
    {
      "id": "stage-exports",
      "event": "afterExport",
      "filter": ["code", "tokens", "handoff"],
      "shell": true,
      "command": ["printf '%s' \"$SILVERPOINT_HOOK_FILES\" | tr '\\n' '\\0' | xargs -0 /usr/bin/git -C /Users/me/code/my-app add --"]
    }
  ]
}

代わりにフォルダ内のすべてをステージするなら、シェルは必要ありません: {"id": "stage-all", "event": "afterExportCode", "command": ["/usr/bin/git", "add", "-A", "."], "cwd": "/Users/me/code/my-app/Sources/Design"}。

対象外

  • デフォルトでのシェル: コマンドは argv です。shell: true は明示的な選択です(承認シートにも表示されます)。
  • render と score がフックを実行することはありません。
  • ドキュメントを移動したり名前を変更したりしても、承認は引き継がれません。新しいパスではもう一度確認されます。
  • ほかの種類のプラグインや、フックのマーケットプレイス。