malanjp が使う Claude Code の mod 集です。作業中のブランチの PR と CI 状態をパネルに表示する pr-status と、セッション内で起動した開発サーバを一覧表示する dev-servers を含みます。

疲れ切った同僚が、読み返さずに理解して次の行動を選べる技術文書を目指すスキル集です。 日本語の Issue・PR・レビュー指摘・調査報告を書くための tech-writing と、PR と CI の状態や開発サーバの一覧を表示する mod 集 dev-mods を収録しています。
新しい文書の作成にも、既存の原稿の推敲にも使えます。 規約とテンプレート、記入例で、文章の組み立て方を確認できます。
文章の規約は共通です。 規約を読み込ませる仕組みと、PR の投稿前検査は環境によって異なります。
| 環境 | 導入するもの | 利用できる機能 |
|---|---|---|
| Claude Code | tired-dev プラグイン | スキル、文章作成の依頼に応じた規約の提示、PR と Linear の投稿前検査、読み手の検査 |
| OpenCode V2 | tired-dev のローカルプラグイン | スキルの登録、文章作成の依頼に応じた規約の提示、PR の投稿前検査 |
| Cursor | tired-dev-cursor プラグイン | スキルと規約の要約 |
Claude Code で、マーケットプレイスとプラグインを追加します。
/plugin marketplace add malanjp/skills
/plugin install tired-dev@malanjp
明示的に使う場合は、/tired-dev:tech-writing を呼び出して原稿や依頼を渡します。 スキルだけの導入や動作の設定も選べます。
このリポジトリをローカルに置き、plugins/tired-dev/opencode の絶対パスを設定します。 プラグインはリポジトリの規約を直接読むため、配置したディレクトリを残して使います。
任意の作業ディレクトリで実行します。 取得済みの場合は、そのリポジトリを使えます。
git clone https://github.com/malanjp/skills.git
cd skills
pwd
~/.config/opencode/opencode.json の plugins 配列にパスを追加します。 次の /absolute/path/to/skills は、pwd が表示したパスに置き換えてください。 既存の設定やプラグインは残します。
{
"plugins": ["/absolute/path/to/skills/plugins/tired-dev/opencode"]
}
V1 形式の設定を引き継ぎ、plugin(単数形)を使っている場合は、その配列に追加します。 追加後に OpenCode を再起動するか、次を実行します。
opencode reload
opencode plugin list
一覧に tired-dev が表示されることを確認してください。 プラグインが tech-writing を登録するため、スキルの手動コピーは不要です。 動作と環境変数の説明に、Claude Code 版との違いを記載しています。
Cursor には tired-dev-cursor を追加します。
https://github.com/malanjp/skills を指定します。tired-dev-cursor を追加します。導入後は /tech-writing で呼び出せます。 Customize から追加する方法や Cloud Agents への配布は、Cursor 版の READMEを参照してください。
たとえば、次のように依頼します。
この調査報告を、担当者が次の確認に着手できる形に推敲してください。
確認済みの事実と仮説を分け、判断に必要な根拠を近くに置いてください。
[原稿を貼る]
対象は、仕事で共有する技術文書です。 短いチャット返答や、記事・エッセイには文書の形式を強制しません。
dev-mods の pr-status は、現在のブランチの PR、CI の結果、関連する GitHub Issue と Linear Issue を Claude Code のパネルに表示する mod です。 Claude Code のターミナルで次を実行して導入します。
/plugin marketplace add malanjp/skills
/plugin install dev-mods@malanjp
パネルは 10 秒ごとに gh pr view の結果で更新されるため、gh へのログインが必要です。 /pr-status を実行すると、パネルを開いて即座に更新します。 claude.ai Linear の MCP サーバーが接続されている場合は、Linear Issue のタイトルと状態も表示します。
dev-mods の dev-servers は、Claude Code の Bash ツールが起動したプロセスのうち、TCP ポートで待ち受けているものをパネルに表示します。 ポートは http://localhost:<port> へのリンクになり、ステータス行にも dev :3000 :5173 の形で表示します。 一覧は 10 秒ごとに ps と lsof の結果で更新し、/dev-servers を実行すると即座に更新します。
MCP サーバーなど Bash ツール以外の子プロセスと、別のターミナルで起動したサーバは表示しません。 nohup や setsid で親プロセスから切り離したサーバも、親子関係をたどれないため表示しません。
Node.js と pnpm を用意し、リポジトリルートで実行します。
pnpm install
pnpm test
pnpm lint
ルートのコマンドは、全パッケージのテストと規約チェックを実行します。 dev-mods のテストと検査には claude コマンドを使います。 個別に確認する場合は、対象の plugins/ 配下へ移動して同じコマンドを使います。
文章の読み心地は自動チェックだけでは判定できません。 記入例を読み、結論・根拠・確認方法を探して戻る箇所がないか確認してください。 生成結果を比較する手順も用意しています。
MIT
hooks/register.tsx 194 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { DevServerInfo, LinearIssue, PrInfo } from '../types'
5import { clean, extractLinear, parseChecks, parseGithubIssues, parseLinearIssue } from './parse'
6import { bashDescendants, parseLsof, parsePs, statusText, summarize, toServers } from './servers'
7import { devServersView, prStatusView } from './views'
8
9const PANE = 'pr-status'
10const POLL_MS = 10_000
11const info = atom({ plugin: 'dev-mods', key: 'info' } as const, null)
12
13const LINEAR_SERVER = 'claude.ai Linear'
14// Linear の状態は頻繁に変わらないので、10 秒ごとの更新で毎回 MCP を呼ばない
15const LINEAR_TTL_MS = 5 * 60_000
16const linearCache = new Map<string, { at: number; issue: LinearIssue }>()
17
18// Linear MCP が使えればタイトルと状態を補う。未接続や失敗時は ID だけで表示する
19async function enrichLinear($: EngineInterface, issue: LinearIssue): Promise<LinearIssue> {
20 const hit = linearCache.get(issue.id)
21 if (hit && Date.now() - hit.at < LINEAR_TTL_MS) return { ...hit.issue, url: hit.issue.url ?? issue.url }
22 try {
23 const r = await $.mcp.call(LINEAR_SERVER, 'get_issue', { id: issue.id, fields: ['title', 'url', 'status'] })
24 const got = r.isError ? null : parseLinearIssue(r.content)
25 const merged = got ? { ...issue, ...got, url: got.url ?? issue.url } : issue
26 linearCache.set(issue.id, { at: Date.now(), issue: merged })
27 return merged
28 } catch {
29 return issue
30 }
31}
32
33// gh が 10 秒以上かかったとき、前回の取得と重ならないようにする
34let inFlight: Promise<PrInfo> | null = null
35
36function refresh($: EngineInterface): Promise<PrInfo> {
37 inFlight ??= fetchInfo($).finally(() => {
38 inFlight = null
39 })
40 return inFlight
41}
42
43async function fetchInfo($: EngineInterface): Promise<PrInfo> {
44 const now = new Date().toLocaleTimeString('ja-JP')
45 const next: PrInfo = { branch: '', pr: null, checks: [], githubIssues: [], linearIssues: [], error: null, updatedAt: now }
46 try {
47 const b = await $.process.run(['git', 'branch', '--show-current'])
48 next.branch = b.exitCode === 0 ? clean(b.stdout.trim()) : '(git 外)'
49 let body = ''
50 const r = await $.process.run(
51 ['gh', 'pr', 'view', '--json', 'number,title,state,url,body,statusCheckRollup,closingIssuesReferences'],
52 { timeoutMs: 20_000 },
53 )
54 if (r.exitCode !== 0) {
55 // PR が無いブランチは gh が非 0 で終わる。エラーではなく「PR なし」として表示する
56 if (!/no pull requests found/i.test(r.stderr)) next.error = clean(r.stderr.trim().split('\n')[0] ?? 'gh 失敗')
57 } else {
58 const j = JSON.parse(r.stdout) as {
59 number: number
60 title: string
61 state: string
62 url: string
63 body: string
64 statusCheckRollup: unknown
65 closingIssuesReferences: unknown
66 }
67 next.pr = { number: j.number, title: clean(j.title), state: clean(j.state), url: clean(j.url) }
68 next.checks = parseChecks(j.statusCheckRollup)
69 next.githubIssues = parseGithubIssues(j.closingIssuesReferences)
70 body = j.body ?? ''
71 }
72 next.linearIssues = await Promise.all(extractLinear(next.branch, next.pr?.title ?? '', body).map(i => enrichLinear($, i)))
73 } catch (err) {
74 next.error = clean(err instanceof Error ? err.message : String(err))
75 }
76 await update($, info, () => next)
77 return next
78}
79
80const SERVERS_PANE = 'dev-servers'
81const SERVERS_TITLE = '開発サーバ'
82// バックグラウンド起動の直後はまだ待受していないことが多いので、少し待ってから取り直す
83const SETTLE_MS = 3_000
84const servers = atom({ plugin: 'dev-mods', key: 'servers' } as const, null)
85
86// エンジンの PID はセッション中に変わらない。モジュールの再読み込みで消えてよい
87let enginePid: number | null = null
88let serversInFlight: Promise<DevServerInfo> | null = null
89
90async function getEnginePid($: EngineInterface): Promise<number> {
91 if (enginePid !== null) return enginePid
92 // $.process.run が起動した子の親がエンジン本体になる
93 const r = await $.process.run(['sh', '-c', 'echo $PPID'])
94 const pid = Number(r.stdout.trim())
95 if (r.exitCode !== 0 || !Number.isInteger(pid) || pid <= 1) throw new Error('エンジンの PID を取得できません')
96 enginePid = pid
97 return pid
98}
99
100async function fetchServers($: EngineInterface): Promise<DevServerInfo> {
101 const next: DevServerInfo = { list: [], error: null, updatedAt: new Date().toLocaleTimeString('ja-JP') }
102 try {
103 const pid = await getEnginePid($)
104 const ps = await $.process.run(['ps', '-axo', 'pid=,ppid=,command='])
105 if (ps.exitCode !== 0) throw new Error(ps.stderr.trim() || 'ps 失敗')
106 const commands = bashDescendants(parsePs(ps.stdout), pid)
107 if (commands.size > 0) {
108 // -p で絞らないと全プロセスを走査して数秒かかる
109 const pids = [...commands.keys()].join(',')
110 const ls = await $.process.run(['lsof', '-nP', '-iTCP', '-sTCP:LISTEN', '-a', '-p', pids, '-Fpcn'])
111 // 待受が 1 件も無いときも lsof は 1 で終わるので、stderr があるときだけ失敗とする
112 if (ls.exitCode !== 0 && ls.stderr.trim()) throw new Error(ls.stderr.trim().split('\n')[0])
113 next.list = toServers(commands, parseLsof(ls.stdout))
114 }
115 } catch (err) {
116 next.error = clean(err instanceof Error ? err.message : String(err))
117 }
118 let before = 0
119 await update($, servers, old => {
120 before = old?.list.length ?? 0
121 return next
122 })
123 $.ui.status(statusText(next))
124 // 0 件から増えたときだけ開く。毎回開くと、閉じたパネルが何度も戻ってくる
125 if (before === 0 && next.list.length > 0) void $.ui.open({ id: SERVERS_PANE, title: SERVERS_TITLE })
126 return next
127}
128
129function refreshServers($: EngineInterface): Promise<DevServerInfo> {
130 serversInFlight ??= fetchServers($).finally(() => {
131 serversInFlight = null
132 })
133 return serversInFlight
134}
135
136export const register: Register = on => {
137 on('session.start', async ($, e, next) => {
138 await $.command.register({ name: 'pr-status', description: 'PR と CI 状態のパネルを開いて更新する' })
139 void $.ui.open({ id: PANE, title: 'PR / CI' })
140 void refresh($).catch(() => undefined)
141 $.clock.every(POLL_MS, () => void refresh($).catch(() => undefined))
142 await $.command.register({ name: 'dev-servers', description: 'このセッションで起動した開発サーバの一覧を開く' })
143 void refreshServers($).catch(() => undefined)
144 $.clock.every(POLL_MS, () => void refreshServers($).catch(() => undefined))
145 return next(e)
146 })
147
148 on('command.run', { command: 'pr-status' }, async $ => {
149 await $.ui.open({ id: PANE, title: 'PR / CI' })
150 const r = await refresh($)
151 if (r.error) return { text: `取得失敗: ${r.error}` }
152 return { text: r.pr ? `#${r.pr.number} を更新しました` : `${r.branch} に PR はありません` }
153 })
154
155 // push や gh 操作の直後は CI 状態が変わるので即時更新する。バックグラウンド起動ではサーバ一覧も取り直す
156 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
157 const ran = await next(e)
158 try {
159 const cmd = e.command
160 if (/\bgit (push|checkout|switch)\b|\bgh pr\b/.test(cmd)) void refresh($).catch(() => undefined)
161 if (e.run_in_background) $.clock.after(SETTLE_MS, () => void refreshServers($).catch(() => undefined))
162 } catch {
163 // パネル更新の失敗でツール結果を壊さない
164 }
165 return ran
166 })
167
168 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
169 const ui = $.ui.resolve(e)
170 const v = await read($, info)
171 if (!v) return <ui.Text dimColor>読み込み中…</ui.Text>
172 return prStatusView(ui, v, e.viewport?.rows ?? 24)
173 })
174
175 on('command.run', { command: 'dev-servers' }, async $ => {
176 await $.ui.open({ id: SERVERS_PANE, title: SERVERS_TITLE })
177 return { text: summarize(await refreshServers($)) }
178 })
179
180 // サーバを止めたら一覧からすぐ消す
181 on('tool.call', { tool: 'TaskStop' }, async ($, e, next) => {
182 const ran = await next(e)
183 void refreshServers($).catch(() => undefined)
184 return ran
185 })
186
187 on('ui.render', { component: 'Pane', requestId: SERVERS_PANE }, async ($, e) => {
188 const ui = $.ui.resolve(e)
189 const v = await read($, servers)
190 if (!v) return <ui.Text dimColor>読み込み中…</ui.Text>
191 return devServersView(ui, v)
192 })
193}
194hooks/parse.ts 88 lines1import type { Check, GithubIssue, LinearIssue } from '../types'
2
3type RawCheck = {
4 __typename?: string
5 name?: string
6 context?: string
7 status?: string
8 conclusion?: string
9 state?: string
10}
11
12const CONTROL = /[\u0000-\u001f\u007f-\u009f]/g
13
14// PR タイトルやチェック名は他人が書けるので、端末の表示を書き換える ESC などの制御文字を除く。
15// 改行とタブは 1 行の表示を崩さないよう空白にする
16export function clean(s: string): string {
17 return s.replace(/[\t\n\r]/g, ' ').replace(CONTROL, '')
18}
19
20// gh pr view --json statusCheckRollup の 1 件を pass / fail / pending / skip に分類する
21export function classify(raw: RawCheck): Check {
22 const name = clean(raw.name ?? raw.context ?? '(unknown)')
23 // StatusContext (外部 CI) は state だけを持つ
24 if (raw.__typename === 'StatusContext') {
25 const s = raw.state ?? ''
26 if (s === 'SUCCESS') return { name, result: 'pass' }
27 if (s === 'PENDING' || s === 'EXPECTED') return { name, result: 'pending' }
28 return { name, result: 'fail' }
29 }
30 if (raw.status !== 'COMPLETED') return { name, result: 'pending' }
31 const c = raw.conclusion ?? ''
32 if (c === 'SUCCESS') return { name, result: 'pass' }
33 if (c === 'SKIPPED' || c === 'NEUTRAL') return { name, result: 'skip' }
34 return { name, result: 'fail' }
35}
36
37export function parseChecks(rollup: unknown): Check[] {
38 if (!Array.isArray(rollup)) return []
39 return rollup.map(r => classify(r as RawCheck))
40}
41
42// Linear の Issue ID (例: ABC-123)。ブランチ名は小文字 (feature/abc-123-...) が慣例なので大文字小文字を区別しない
43const LINEAR_ID = /\b([a-z]{2,6})-(\d+)\b/gi
44const LINEAR_URL = /https:\/\/linear\.app\/[\w-]+\/issue\/([A-Za-z]{2,6}-\d+)/g
45
46// ブランチ名・PR タイトル・本文の Linear URL から Linear Issue を集める。
47// 本文に URL があればそれを使い、無い ID は URL なしで表示する
48export function extractLinear(branch: string, title: string, body: string): LinearIssue[] {
49 const found = new Map<string, string | null>()
50 for (const m of body.matchAll(LINEAR_URL)) {
51 const id = m[1]!.toUpperCase()
52 found.set(id, `${m[0].slice(0, m[0].length - m[1]!.length)}${id}`)
53 }
54 for (const text of [branch, title]) {
55 for (const m of text.matchAll(LINEAR_ID)) {
56 const id = `${m[1]!.toUpperCase()}-${m[2]}`
57 if (!found.has(id)) found.set(id, null)
58 }
59 }
60 return [...found].map(([id, url]) => ({ id, url, title: null, status: null }))
61}
62
63export function parseGithubIssues(refs: unknown): GithubIssue[] {
64 if (!Array.isArray(refs)) return []
65 return refs.flatMap(r => {
66 const { number, url } = r as { number?: unknown; url?: unknown }
67 return typeof number === 'number' && typeof url === 'string' ? [{ number, url: clean(url) }] : []
68 })
69}
70
71// Linear MCP get_issue の結果 (text ブロックの JSON) を読む。読めなければ null
72export function parseLinearIssue(content: unknown): { title: string; url: string | null; status: string | null } | null {
73 if (!Array.isArray(content)) return null
74 const text = content.find(b => (b as { type?: unknown }).type === 'text') as { text?: unknown } | undefined
75 if (typeof text?.text !== 'string') return null
76 try {
77 const j = JSON.parse(text.text) as { title?: unknown; url?: unknown; status?: unknown }
78 if (typeof j.title !== 'string') return null
79 return {
80 title: clean(j.title),
81 url: typeof j.url === 'string' ? clean(j.url) : null,
82 status: typeof j.status === 'string' ? clean(j.status) : null,
83 }
84 } catch {
85 return null
86 }
87}
88hooks/servers.ts 87 lines1import type { DevServer, DevServerInfo } from '../types'
2import { clean } from './parse'
3
4export type Proc = { pid: number; ppid: number; command: string }
5export type Listen = { name: string; addrs: string[] }
6
7// Bash ツールのコマンドはエンジン直下の `<shell> -c ...` で動く。
8// MCP サーバなどエンジンの他の子プロセスも待受することがあるので、シェルの配下だけを対象にする
9const SHELL = /^(\S*\/)?(zsh|bash|sh) -c /
10const MAX_COMMAND = 200
11
12// `ps -axo pid=,ppid=,command=` の出力を読む
13export function parsePs(text: string): Proc[] {
14 const out: Proc[] = []
15 for (const line of text.split('\n')) {
16 const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line)
17 if (m) out.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] ?? '' })
18 }
19 return out
20}
21
22// エンジン直下のシェルとその子孫を pid からコマンドへの対応で返す
23export function bashDescendants(procs: Proc[], enginePid: number): Map<number, string> {
24 const children = new Map<number, Proc[]>()
25 for (const p of procs) children.set(p.ppid, [...(children.get(p.ppid) ?? []), p])
26 const found = new Map<number, string>()
27 const queue = (children.get(enginePid) ?? []).filter(p => SHELL.test(p.command))
28 while (queue.length > 0) {
29 const p = queue.shift()!
30 if (found.has(p.pid)) continue
31 found.set(p.pid, p.command)
32 queue.push(...(children.get(p.pid) ?? []))
33 }
34 return found
35}
36
37// `lsof -Fpcn` の出力を pid ごとのプロセス名と待受アドレスにまとめる
38export function parseLsof(text: string): Map<number, Listen> {
39 const out = new Map<number, Listen>()
40 let cur: Listen | null = null
41 for (const line of text.split('\n')) {
42 const tag = line[0]
43 const val = line.slice(1)
44 if (tag === 'p') {
45 const pid = Number(val)
46 cur = out.get(pid) ?? { name: '', addrs: [] }
47 out.set(pid, cur)
48 } else if (tag === 'c' && cur) {
49 cur.name = val
50 } else if (tag === 'n' && cur && !cur.addrs.includes(val)) {
51 cur.addrs.push(val)
52 }
53 }
54 return out
55}
56
57// `127.0.0.1:3000` や `[::1]:3000` や `*:3000` からポート番号を取る
58export function portOf(addr: string): number | null {
59 const port = Number(addr.slice(addr.lastIndexOf(':') + 1))
60 return Number.isInteger(port) && port > 0 && port < 65536 ? port : null
61}
62
63export function toServers(commands: Map<number, string>, listens: Map<number, Listen>): DevServer[] {
64 const out: DevServer[] = []
65 for (const [pid, l] of listens) {
66 const command = commands.get(pid)
67 if (command === undefined) continue
68 const ports = [...new Set(l.addrs.map(portOf).filter((p): p is number => p !== null))].sort((a, b) => a - b)
69 if (ports.length === 0) continue
70 out.push({ pid, name: clean(l.name), command: clean(command).slice(0, MAX_COMMAND), ports })
71 }
72 return out.sort((a, b) => (a.ports[0] ?? 0) - (b.ports[0] ?? 0))
73}
74
75export const NO_SERVERS = 'このセッションで起動した開発サーバはありません'
76
77export function summarize(v: DevServerInfo): string {
78 if (v.error) return `取得失敗: ${v.error}`
79 if (v.list.length === 0) return NO_SERVERS
80 return v.list.map(s => `${s.ports.map(p => `:${p}`).join(' ')} ${s.name} (pid ${s.pid})`).join('\n')
81}
82
83// ステータス行に出す文字列。サーバが無ければ undefined で消す
84export function statusText(v: DevServerInfo): string | undefined {
85 return v.list.length > 0 ? `dev ${v.list.flatMap(s => s.ports.map(p => `:${p}`)).join(' ')}` : undefined
86}
87hooks/views.tsx 84 lines1import type { EngineInterface } from 'claude-code'
2
3import type { Check, DevServerInfo, PrInfo } from '../types'
4import { NO_SERVERS } from './servers'
5
6// $ はインポート先へ渡せないので、描画は $.ui.resolve の結果だけを受け取る
7type Ui = ReturnType<EngineInterface['ui']['resolve']>
8
9const ICON: Record<Check['result'], string> = { pass: '✓', fail: '✗', pending: '…', skip: '-' }
10
11export function prStatusView({ Box, Text, Link }: Ui, v: PrInfo, rows: number) {
12 const count = (k: Check['result']) => v.checks.filter(c => c.result === k).length
13 const failed = v.checks.filter(c => c.result === 'fail')
14 const pending = v.checks.filter(c => c.result === 'pending')
15 const room = Math.max(1, rows - 8)
16
17 return (
18 <Box flexDirection="column">
19 <Text dimColor>branch: {v.branch}</Text>
20 {v.linearIssues.length > 0 && (
21 <Box flexDirection="column">
22 {v.linearIssues.map(i => (
23 <Text>
24 {i.url ? <Link href={i.url} label={i.id} /> : i.id}
25 {i.status && <Text color="cyan"> [{i.status}]</Text>}
26 {i.title && <Text dimColor> {i.title}</Text>}
27 </Text>
28 ))}
29 </Box>
30 )}
31 {v.githubIssues.length > 0 && (
32 <Text>
33 Issue: {v.githubIssues.map(i => <Link href={i.url} label={`#${i.number} `} />)}
34 </Text>
35 )}
36 {v.error && <Text color="red">エラー: {v.error}</Text>}
37 {!v.error && !v.pr && <Text dimColor>PR なし</Text>}
38 {v.pr && (
39 <Box flexDirection="column">
40 <Text bold>
41 #{v.pr.number} [{v.pr.state}]
42 </Text>
43 <Text>{v.pr.title}</Text>
44 <Text>
45 <Text color="green">✓{count('pass')}</Text> <Text color="red">✗{count('fail')}</Text>{' '}
46 <Text color="yellow">…{count('pending')}</Text> <Text dimColor>-{count('skip')}</Text>
47 </Text>
48 {[...failed, ...pending].slice(0, room).map(c => (
49 <Text color={c.result === 'fail' ? 'red' : 'yellow'}>
50 {ICON[c.result]} {c.name}
51 </Text>
52 ))}
53 </Box>
54 )}
55 <Text dimColor>更新 {v.updatedAt}</Text>
56 </Box>
57 )
58}
59
60export function devServersView({ Box, Text, Link }: Ui, v: DevServerInfo) {
61 return (
62 <Box flexDirection="column">
63 {v.error && <Text color="red">エラー: {v.error}</Text>}
64 {!v.error && v.list.length === 0 && <Text dimColor>{NO_SERVERS}</Text>}
65 {v.list.map(s => (
66 <Box flexDirection="column">
67 <Text>
68 {s.ports.map(p => (
69 <Link href={`http://localhost:${p}`} label={`:${p} `} />
70 ))}
71 <Text bold>{s.name}</Text>
72 <Text dimColor> pid {s.pid}</Text>
73 </Text>
74 <Text dimColor wrap="truncate-end">
75 {' '}
76 {s.command}
77 </Text>
78 </Box>
79 ))}
80 <Text dimColor>更新 {v.updatedAt}</Text>
81 </Box>
82 )
83}
84types/index.d.ts 21 lines1export type Check = { name: string; result: 'pass' | 'fail' | 'pending' | 'skip' }
2export type GithubIssue = { number: number; url: string }
3export type LinearIssue = { id: string; url: string | null; title: string | null; status: string | null }
4export type PrInfo = {
5 branch: string
6 pr: { number: number; title: string; state: string; url: string } | null
7 checks: Check[]
8 githubIssues: GithubIssue[]
9 linearIssues: LinearIssue[]
10 error: string | null
11 updatedAt: string
12}
13export type DevServer = { pid: number; name: string; command: string; ports: number[] }
14export type DevServerInfo = { list: DevServer[]; error: string | null; updatedAt: string }
15
16declare module 'claude-code' {
17 interface PluginState {
18 'dev-mods': { info: PrInfo | null; servers: DevServerInfo | null }
19 }
20}
21