Edit Pencil Design
Pencil CLI(pencilコマンド)のみで .pen デザインファイルをAI編集し、編集Nodeだけのスクリーンショットを残すスキル。MCPサーバーには依存しません。公式ドキュメント: docs.pencil.dev/for-developers/pencil-cli
設計思想
Pencil CLI の2つの実行モードを使い分けます:
| モード | 起動方法 | できること |
|---|---|---|
| エージェントモード | pencil --in --out --prompt | AIプロンプトで .pen を編集(プロンプト編集はこのモードのみ) |
| インタラクティブモード | pencil interactive -i -o | get_editor_state() / get_screenshot() / export_nodes() / save() / exit() などのツール呼び出し(AIプロンプト編集は不可) |
.pen は暗号化バイナリで Read / Grep では読めないため、Node構造の確認・Node ID取得・Node単位スクリーンショットはすべて pencil interactive 経由で行います。
重要な前提
- 既存ファイルをその場で上書き更新する(別名出力は二重管理を生むため避ける)
- スクリーンショットはファイル全体ではなく編集対象のNodeだけ(差分レビューが容易になる)
前提条件の確認
pencil version— 未インストールならnpm install -g @pencil.dev/cliを案内(Node.js 18以上必要)pencil status— 未認証ならpencil login、またはPENCIL_CLI_KEY環境変数の設定を案内- 対象の
.penファイルが存在するか(編集対象なので先に存在している必要がある)
実行ルール
ルール1: 編集モードの選択(エージェント / interactive どちらも可)
- エージェントモード
pencil --in path/to/design.pen --out path/to/design.pen --prompt "<修正内容>"(短縮形-i/-o/-p)— 自然言語で任せたい編集。大きめのリファイン、レイアウト調整、複数Nodeにまたがる修正向き - インタラクティブモードの
batch_design({...})— 「特定NodeのプロパティをこのJSONに置換」のような決定論的な編集向き。結果が予測可能で差分も追いやすい。ただし heredoc/シェルの改行展開を誤るとサイレントに失敗するため、ルール2の安全規則を必ず守る
どちらのモードでも既存ファイルの更新なので、--in と --out には同じ .pen パスを指定します。新規作成を依頼された場合のみ例外で、--in を省略して --out に新しいパスを指定します。
ルール2: インタラクティブモードを heredoc で非対話的に呼び出す
pencil interactive は標準入力からコマンドを流せば非対話的に実行できます。
pencil interactive -i path/to/design.pen -o path/to/design.pen <<'EOF'
get_editor_state()
exit()
EOF
-iと-oには編集対象と同じ.penパスを指定(ヘッドレスモードでは-oが必須)save()を呼ばなければファイルへの変更は永続化されない(読み取りのみならsave()不要)- 最後に必ず
exit()を呼ぶ
未確定な仕様: 各ツールの完全な引数仕様(出力先パラメータ名など)は公式ドキュメント未記載。pencil interactive --help / pencil --help でローカル実装を確認し、引数名が想定と異なれば調整してください。
heredoc / シェルの改行展開を正しく扱う(重要)
長いJSON引数(特に batch_design({...}))を heredoc で流すとき、シェルが文字列内の \n を実改行に展開するとJSONが壊れ、Pencil側がパースエラーをサイレントに無視して save() だけが走り、小数点正規化(13.995000000000001 → 13.995)のような無害な差分だけがディスクに残ります。過去に実害ありの事故パターンなので必読。
| シェル / コマンド | "a\nb" の扱い |
|---|---|
| zsh の組み込み echo | \n を実改行に展開(デフォルト挙動) |
| bash の組み込み echo | デフォルトでは展開しない(-e で展開) |
| printf '%s' "..." | 移植性ありで \n を2文字のまま出力 |
| print -r -- "..." (zsh) | エスケープ解釈なし |
| heredoc <<'EOF'(クォート付) | 本文をリテラルのまま渡す(\n は2文字のまま、変数展開も無し) |
| heredoc <<EOF(クォート無) | 変数展開・コマンド置換は行うが、リテラル \n は2文字のまま |
原則は「JSON文字列リテラル内の \n は2文字(バックスラッシュ + n)のままPencilに届けること」。シェル側で改行に化けるとJSONが構文エラーになります。
改行を確実に2文字のまま渡すための4原則
-
heredoc は最優先で
<<'EOF'(シングルクォート付き)を使う — 変数展開もエスケープ解釈も止まり、本文のJSONがそのままPencilに届く。 -
動的な値は
jqでJSONエンコードしてから heredoc に差し込む。echo "{\"text\": \"$user_input\"}"のような自前組み立ては禁止(改行・ダブルクォート・バックスラッシュが含まれた瞬間に壊れる)。TEXT_JSON=$(jq -Rs . <<< "Hello World") # → "Hello\nWorld" という、正しくエスケープされたJSON文字列リテラルになる pencil interactive -i path/to/design.pen -o path/to/design.pen <<EOF batch_design({ ops: [ { type: "update", id: "title-01", props: { text: ${TEXT_JSON} } } ] }) save() exit() EOF<<EOF(クォート無し)で変数展開しても、jq -Rs .がJSONエスケープ済み文字列(前後にダブルクォート付き)に変換しているため構文が壊れません。 -
echoを使わない。printf '%s'またはprint -r --(zsh)を使う。# NG (zshで\nが実改行に化けてJSONが壊れる) ARGS=$(echo '{ "text": "Hello\nWorld" }') # OK ARGS=$(printf '%s' '{ "text": "Hello\nWorld" }') -
JSON値として改行が必要なら、リテラル
\nの2文字で書く(heredoc本文に実改行を含むテキストを直接書かない)。
失敗を早く検出するセルフチェック
Pencilに流す前に「シェルが解釈した最終文字列」を cat で目視します。
cat > "${WORK_DIR}/cmds.txt" <<'EOF'
batch_design({ ops: [{ type: "update", id: "t1", props: { text: "line1\nline2" } }] })
save()
exit()
EOF
cat "${WORK_DIR}/cmds.txt" # JSON文字列リテラル内の \n が2文字のまま残っていることを目視
pencil interactive -i path/to/design.pen -o path/to/design.pen < "${WORK_DIR}/cmds.txt"
\n が実改行に化けていたら即失敗。<<'EOF' に修正してやり直します。
ルール3: 同時実行で競合しない一時ディレクトリを毎回確保する
中間ファイルの保存先を固定パスにすると、同じ .pen の同時編集で上書き衝突が起きます。開始時に mktemp -d で実行ごとに一意なディレクトリを確保します(ディレクトリ名の一意性がカーネル側で保証され、trap で途中失敗時も自動後始末される)。
WORK_DIR="$(mktemp -d -t pencil-edit-XXXXXX)"
trap 'rm -rf "$WORK_DIR"' EXIT
before.json / after.json などの中間ファイルは必ず ${WORK_DIR} 配下に置きます(/tmp/before.json のような固定パスは使わない)。
ルール4: 編集の前後でNodeツリーを取得し、編集されたNodeを特定する
-
編集前のスナップショット取得
pencil interactive -i path/to/design.pen -o path/to/design.pen <<'EOF' > "${WORK_DIR}/before.json" get_editor_state() exit() EOF -
編集(エージェントモード) — 標準出力・標準エラーも
${WORK_DIR}に流し、同時実行時のログ取り違えを防ぐpencil --in path/to/design.pen --out path/to/design.pen --prompt "<具体的な指示>" \ > "${WORK_DIR}/edit.log" 2>&1 -
編集後のスナップショット取得 — 同様に
${WORK_DIR}/after.jsonへ保存 -
編集Nodeの特定
afterにあってbeforeに無いid→ 新規追加Node- 双方にあるが属性差分のある
id→ 変更Node - 判定が難しい場合(idの再採番、大規模な再構成など)は推定できる範囲で抽出し、残りはユーザーに確認。フォールバックとして影響を受けた最上位フレーム/コンポーネントのNode IDを1つ選んでスクリーンショットを取る
-
「実質的な編集が無い」ケースの検出 → 編集失敗扱いにする
差分が「Node IDの追加・削除なし、type / name / children の構造変化なし、数値フォーマットの正規化のみ(例:
13.995000000000001→13.995、100.0→100)」なら、JSONパースエラーで構造変更が適用されずsave()だけ走った可能性が極めて高い(ルール2のトラブルの典型的な観測像)。編集失敗として報告し、再実行します。チェックはjqで数値表現を正規化してから diff:jq -S 'walk(if type == "number" then tonumber|tostring|tonumber else . end)' \ "${WORK_DIR}/before.json" > "${WORK_DIR}/before.norm.json" jq -S 'walk(if type == "number" then tonumber|tostring|tonumber else . end)' \ "${WORK_DIR}/after.json" > "${WORK_DIR}/after.norm.json" if diff -q "${WORK_DIR}/before.norm.json" "${WORK_DIR}/after.norm.json" >/dev/null; then echo "編集失敗の疑い: 構造に有意な差分なし。heredocのJSON引数が壊れていないかルール2を再確認してください" >&2 exit 1 fiこの検証は編集Node特定の直前に必ず通します。失敗が出たらルール2に戻って heredoc / echo の改行展開を確認します。
ルール5: 編集したNodeだけをスクリーンショットし snapshots/ に保存する
.pen と同階層の snapshots/ にNode単位でPNG出力します。単一Nodeは get_screenshot、複数Nodeは export_nodes。引数名がエラーになったら pencil interactive --help で正しい名前に置き換えます。
mkdir -p "$(dirname path/to/design.pen)/snapshots"
# 複数Node
pencil interactive -i path/to/design.pen -o path/to/design.pen <<'EOF'
export_nodes({
nodes: [
{ id: "<編集Node1 ID>", out: "path/to/snapshots/design-<node1-name>-<timestamp>.png", format: "png", scale: 2 },
{ id: "<編集Node2 ID>", out: "path/to/snapshots/design-<node2-name>-<timestamp>.png", format: "png", scale: 2 }
]
})
exit()
EOF
# 単一Node
pencil interactive -i path/to/design.pen -o path/to/design.pen <<'EOF'
get_screenshot({ nodeId: "<編集Node ID>", out: "path/to/snapshots/design-<node>-<timestamp>.png", scale: 2 })
exit()
EOF
- ファイル命名規則:
<.penファイル名のステム>-<Node名 or Node ID短縮>-<YYYYMMDD-HHMMSS>.png(例:login.penのheaderNode →snapshots/login-header-20260627-153045.png)。タイムスタンプ込みにすることでsnapshots/内も同時実行で衝突しない - 新規Nodeが親コンテナ内に追加された場合、親Node IDも対象に加えると配置確認しやすい
- ファイル全体のエクスポート(エージェントモードの
--export)は原則使わない。ユーザーが明示的に全体画像を要求した場合のみpencil --in <path> --export <全体画像のpath> --export-scale 2を補助的に使う
ルール6: 実行結果をユーザーに伝える
.pen の中身は直接確認できないため、最終報告に必ず含めます:
- 実行したコマンド(エージェントモードのCLIと、インタラクティブモードのheredoc)
- 編集したと判定したNode(IDと、可能なら名前・type)
- 更新した
.penファイルの絶対パス - 出力したNode単位スクリーンショット画像の絶対パス(編集Nodeごと)
標準ワークフロー
- 前提確認:
pencil version、pencil status - 対象ファイル確認: 指定された
.penが存在するか - 作業ディレクトリ確保:
WORK_DIR="$(mktemp -d -t pencil-edit-XXXXXX)"とtrap 'rm -rf "$WORK_DIR"' EXIT snapshots/準備:mkdir -p <.penと同じディレクトリ>/snapshots- 編集前スナップショット:
get_editor_state()→${WORK_DIR}/before.json - 編集実行:
pencil --in <path> --out <path> --prompt "<指示>" > "${WORK_DIR}/edit.log" 2>&1(--in/--outは同一パス) - 編集後スナップショット:
get_editor_state()→${WORK_DIR}/after.json - 「実質的編集が無い」検出: ルール4-5 の
jq正規化 diff で確認。該当すれば編集失敗としてルール2に戻る - 編集Node特定: before/after の差分から新規/変更Node IDを抽出
- Node単位スクリーンショット:
export_nodes/get_screenshotでsnapshots/にPNG出力(ファイル名はタイムスタンプ込み) - 報告: 編集Nodeと出力画像パスを提示(
${WORK_DIR}は trap で自動削除)
使用例: ログインページに「Forgot password?」リンクを追加
pencil status
mkdir -p designs/snapshots
WORK_DIR="$(mktemp -d -t pencil-edit-XXXXXX)"
trap 'rm -rf "$WORK_DIR"' EXIT
# 1) 編集前 Nodeツリー
pencil interactive -i designs/login.pen -o designs/login.pen <<'EOF' > "${WORK_DIR}/before.json"
get_editor_state()
exit()
EOF
# 2) 編集(--in と --out は同じファイル)
pencil \
--in designs/login.pen \
--out designs/login.pen \
--prompt "Add a 'Forgot password?' link below the password input, aligned to the right" \
> "${WORK_DIR}/edit.log" 2>&1
# 3) 編集後 Nodeツリー
pencil interactive -i designs/login.pen -o designs/login.pen <<'EOF' > "${WORK_DIR}/after.json"
get_editor_state()
exit()
EOF
before/after を比較して新規Node forgot-link-01 を特定したら:
TS="$(date +%Y%m%d-%H%M%S)"
pencil interactive -i designs/login.pen -o designs/login.pen <<EOF
get_screenshot({ nodeId: "forgot-link-01", out: "designs/snapshots/login-forgot-link-${TS}.png", scale: 2 })
exit()
EOF
主要オプション/コマンド早見表
エージェントモード(編集用)
| オプション | 短縮 | 用途 |
|---|---|---|
| --in <path> | -i | 入力 .pen ファイル(編集元) |
| --out <path> | -o | 出力 .pen ファイル(--in と同じパスを指定する) |
| --prompt <text> | -p | AIエージェントへの編集指示 |
| --model <id> | - | 使用モデル指定(claude-opus-4-6 / claude-sonnet-4-6 / claude-haiku-4-5) |
| --export <path> | - | ファイル全体の画像出力(本スキルでは原則使わない) |
| --export-scale <n> | - | エクスポート時のスケール(同上) |
インタラクティブモード(Node取得・スクショ用)
起動オプション: --in / -i <path>、--out / -o <path>(ヘッドレス時必須)、--help / -h
シェル内ツール:
get_editor_state()— Nodeツリーやメタデータの取得get_screenshot(...)— 単一NodeをPNGレンダリングexport_nodes(...)— 複数NodeをPNG/JPEG/WEBP/PDFへエクスポートsnapshot_layout(...)— レイアウトのスナップショットbatch_get(...)/batch_design(...)— 複雑な取得/編集の一括処理get_variables()/get_guidelines()— 変数・ガイドラインの取得save()— 編集結果を.penに書き出す(読み取り目的なら省略)exit()— シェル終了
トラブルシューティング
pencil: command not found:npm install -g @pencil.dev/cliを案内(Node.js 18以上必要)- 認証エラー:
pencil login、またはPENCIL_CLI_KEY環境変数を設定 -oが必須エラー: ヘッドレス実行では-o必須。-iと同じパスを指定し、save()を呼ばなければ変更は永続化されないget_screenshot/export_nodesの引数名エラー: 出力先パラメータ名(out/path/output等)やscale/formatはドキュメント未記載。pencil interactive --helpで確認して合わせる- 編集Nodeが特定できない(idが再採番される/大規模変更): 影響を受けた最上位フレーム/コンポーネントを代表として1つエクスポートし、ユーザーに確認を求める
.penファイルが見つからない: パスを再確認。新規作成希望なら--inを省略- 想定と違う編集結果:
--promptをより具体的に書き直して再実行。.penは上書きされるため、重要な編集前にはユーザーに git コミット等のバックアップを促す - 編集したはずなのに小数点正規化(例:
13.995000000000001→13.995)だけが残っている: heredoc経由のbatch_designでJSON引数が壊れ、save()だけ走った典型的な事故。順にチェック:<<EOFで開いていないか →<<'EOF'に切り替えるechoで組み立てた値を埋め込んでいないか →jq -Rs .かprintf '%s'に置き換える- 実改行を含むテキストを直接書いていないか → リテラル
\nの2文字で書く - ルール2のセルフチェックで最終文字列を
catで目視する - ルール4-5 の正規化 diff で「数値正規化だけ」でないことを確認してから報告する