入力欄の上にgit状態、ディレクトリ、モデル、使用率、言語のバージョン、reviewボタン等を常時表示するmod

metalmental (ユーザ名) の claude code の plugins 等をまとめたリポジトリ
| plugin名 | 説明 |
|---|---|
| ask-user-question-enforce | Skillのユーザー確認をAskUserQuestionに統一させるSKILL |
| aws-best-practice | AWSリソースの推奨設定SKILL |
| career-transition-support | ITエンジニア転職SKILL |
| code-review-by-book | 読んだ技術書をもとにしたコードレビューWorkflow |
| git-github | commit、worktree/branch、pr作成Skill |
| local-doc-search | ローカルPCのファイルを全文検索やRAG検索するMCP |
| sandbox-enforce | 全claude設定ファイルでsandbox.enabledにするSkill |
| session-daily-report | 全プロジェクトのセッション履歴から日報を作成するWorkflow |
| statusline | ModelやBranch名、Python等のversionを表示するstatusline Mod |
| tool-failure-log | Toolの実行エラーを調査可能にするSkill |
| ui-plus | UIに表のコピーや会話の折りたたみボタン追加するMod |
| verified-web-search | 検索したURLの完全なページを取得し、正確な1件のみ返すWorkflow |
# Launch Claude
claude
# Add this marketplace
/plugin marketplace add https://github.com/Flupinochan/metalmental-claude
# Reload plugins
/reload-plugins
/plugin install git-github@metalmental-plugins-official
/git-github:commit-enforce
claude --plugin-dir "$(pwd)/plugins/session-daily-report"
plugin.json の version を上げることplugin-dev:plugin-validator や claude plugin validate, test, eval 実行して動作確認するかユーザに確認すること/help skillを作成することhooks/register.tsx 262 lines1import type { Register } from "claude-code";
2
3import { formatBunVersion, readBunVersion } from "./bun";
4import { COLOR } from "./colors";
5import { ctxColor, formatCtx, readCtx, syncCtx } from "./ctx";
6import { gitStatusColor, readBranchName, readGitStatus } from "./gitStatus";
7import { formatGoVersion, readGoVersion } from "./go";
8import { formatModel, modelColor, readModelId, syncModelId } from "./model";
9import { formatNodeVersion, readNodeVersion } from "./nodejs";
10import { formatPythonVersion, readPythonVersion } from "./python";
11import { formatRustVersion, readRustVersion } from "./rust";
12import { formatTerraformVersion, readTerraformVersion } from "./terraform";
13import { readOutputStyle, syncOutputStyle } from "./outputStyle";
14import {
15 openPullRequest,
16 readPullRequests,
17 summarizePullRequests,
18} from "./pullRequest";
19import {
20 readRateLimits,
21 summarizeRateLimits,
22 syncRateLimits,
23} from "./rateLimits";
24import { readReloadStatus, reloadManually, summarizeReload } from "./reload";
25import { pressReview, readReviewedCommands, summarizeReviews } from "./review";
26import { syncStatusline } from "./syncStatusline";
27import {
28 copyWorkingDirectory,
29 directoryName,
30 readWorkingDirectory,
31 workingDirectoryColor,
32} from "./workingDirectory";
33
34const COMMANDS_TO_RESYNC = [
35 "cd",
36 "clear",
37 "resume",
38 "branch",
39 "rewind",
40] as const;
41
42/**
43 * statusline の各イベントのフックと、入力欄の上の描画を登録する
44 * @param on フックを登録する関数
45 */
46export const register: Register = (on) => {
47 on("session.start", async ($, event, next) => {
48 await syncStatusline($);
49
50 return next(event);
51 });
52
53 on("turn.complete", async ($, event, next) => {
54 if (event.agentId === undefined) {
55 await syncStatusline($);
56 }
57
58 return next(event);
59 });
60
61 on("session.measure", async ($, event, next) => {
62 if (event.changed.includes("context")) {
63 await syncCtx($);
64 }
65
66 if (event.changed.includes("rateLimits")) {
67 await syncRateLimits($);
68 }
69
70 return next(event);
71 });
72
73 on("classic.PostCompact", async ($, event, next) => {
74 await syncCtx($);
75
76 return next(event);
77 });
78
79 on("config.set", async ($, event, next) => {
80 const result = await next(event);
81 await syncOutputStyle($);
82
83 return result;
84 });
85
86 on("command.run", { command: "model" }, async ($, event, next) => {
87 const ran = await next(event);
88 await syncModelId($);
89
90 return ran;
91 });
92
93 on("command.run", { command: "output-style" }, async ($, event, next) => {
94 const ran = await next(event);
95 await syncOutputStyle($);
96
97 return ran;
98 });
99
100 for (const command of COMMANDS_TO_RESYNC) {
101 on("command.run", { command }, async ($, event, next) => {
102 const ran = await next(event);
103 $.clock.after(0, () => syncStatusline($));
104
105 return ran;
106 });
107 }
108
109 on("ui.render", { component: "AbovePrompt" }, async ($, event, next) => {
110 const [
111 reloadStatus,
112 gitStatus,
113 workingDirectory,
114 branchName,
115 nodeVersion,
116 pythonVersion,
117 rustVersion,
118 goVersion,
119 bunVersion,
120 terraformVersion,
121 pullRequests,
122 modelId,
123 outputStyle,
124 ctxPercent,
125 rateLimits,
126 reviewedCommands,
127 ] = await Promise.all([
128 readReloadStatus($),
129 readGitStatus($),
130 readWorkingDirectory($),
131 readBranchName($),
132 readNodeVersion($),
133 readPythonVersion($),
134 readRustVersion($),
135 readGoVersion($),
136 readBunVersion($),
137 readTerraformVersion($),
138 readPullRequests($),
139 readModelId($),
140 readOutputStyle($),
141 readCtx($),
142 readRateLimits($),
143 readReviewedCommands($),
144 ]);
145
146 if (event.props.hasSurvey || event.surface === "mobile") {
147 return next(event);
148 }
149
150 const { Box, Button, Text } = $.ui.resolve(event);
151 const { icon: reloadIcon, color: reloadColor } =
152 summarizeReload(reloadStatus);
153
154 return (
155 <Box
156 flexDirection="row"
157 flexWrap="wrap"
158 columnGap={1}
159 position="relative"
160 >
161 <Button key="reload" plain onPress={() => void reloadManually($)}>
162 <Text color={reloadColor}>{reloadIcon}</Text>
163 </Button>
164 {gitStatus !== undefined && (
165 <Text color={gitStatusColor(gitStatus)}>{gitStatus}</Text>
166 )}
167 {workingDirectory !== "" && (
168 <Button
169 key="working-directory"
170 plain
171 hover={{ scope: "working-directory" }}
172 onPress={(press) =>
173 void copyWorkingDirectory($, workingDirectory, press.surface)
174 }
175 >
176 <Text color={workingDirectoryColor(workingDirectory)}>
177 {directoryName(workingDirectory)}
178 </Text>
179 </Button>
180 )}
181 {branchName !== "" && <Text color={COLOR.indigo}>{branchName}</Text>}
182 {summarizePullRequests(pullRequests).map(
183 ({ number, url, text, color }) => (
184 <Button
185 key={`pull-request-${number}`}
186 plain
187 onPress={() => void openPullRequest($, url)}
188 >
189 <Text color={color}>{text}</Text>
190 </Button>
191 ),
192 )}
193 {modelId !== "" && (
194 <Text color={modelColor(modelId)}>{formatModel(modelId)}</Text>
195 )}
196 {outputStyle !== undefined && (
197 <Text color={COLOR.orange}>{outputStyle}</Text>
198 )}
199 <Text color={ctxColor(ctxPercent)}>{formatCtx(ctxPercent)}</Text>
200 {summarizeRateLimits(rateLimits).map(({ kind, text, color }) => (
201 <Text key={kind} color={color}>
202 {text}
203 </Text>
204 ))}
205 {summarizeReviews(reviewedCommands).map(({ command, label, color }) => (
206 <Button
207 key={label}
208 plain
209 onPress={() => void pressReview($, command)}
210 >
211 <Text color={color}>{label}</Text>
212 </Button>
213 ))}
214 {nodeVersion !== "" && (
215 <Text color={COLOR.green}>{formatNodeVersion(nodeVersion)}</Text>
216 )}
217 {pythonVersion !== "" && (
218 <Text color={COLOR.yellow}>{formatPythonVersion(pythonVersion)}</Text>
219 )}
220 {rustVersion !== "" && (
221 <Text color={COLOR.orange}>{formatRustVersion(rustVersion)}</Text>
222 )}
223 {goVersion !== "" && (
224 <Text color={COLOR.blue}>{formatGoVersion(goVersion)}</Text>
225 )}
226 {bunVersion !== "" && (
227 <Text color={COLOR.purple}>{formatBunVersion(bunVersion)}</Text>
228 )}
229 {terraformVersion !== "" && (
230 <Text color={COLOR.indigo}>
231 {formatTerraformVersion(terraformVersion)}
232 </Text>
233 )}
234 {workingDirectory !== "" && (
235 <Box
236 position="absolute"
237 top={0}
238 bottom={0}
239 left={0}
240 right={0}
241 display="none"
242 backgroundColor={COLOR.blue}
243 hover={{ scope: "working-directory", display: "flex" }}
244 >
245 <Button
246 key="working-directory-path"
247 plain
248 onPress={(press) =>
249 void copyWorkingDirectory($, workingDirectory, press.surface)
250 }
251 >
252 <Text color={workingDirectoryColor(workingDirectory)}>
253 {workingDirectory}
254 </Text>
255 </Button>
256 </Box>
257 )}
258 </Box>
259 );
260 });
261};
262hooks/bun.ts 107 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/bun.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasFile } from "./directoryEntries";
9
10const bunVersionState = atom(
11 { plugin: "statusline", key: "bunVersion" } as const,
12 "",
13);
14
15const DETECT_FILES = ["bun.lock", "bun.lockb", "bunfig.toml"];
16
17// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
18let versionCache = { cwd: "", version: "" };
19
20/**
21 * ディレクトリの直下の内容が、bun のプロジェクトかを判定する
22 * @param entries ディレクトリの直下の項目
23 * @returns bun のプロジェクトの場合は true
24 */
25export function isBunProject(entries: readonly Entry[]) {
26 return DETECT_FILES.some((name) => hasFile(entries, name));
27}
28
29/**
30 * bun --version の出力から、版を取り出す
31 * @param stdout bun --version の標準出力
32 * @returns 版
33 */
34export function parseBunVersion(stdout: string) {
35 return stdout.trim();
36}
37
38/**
39 * bun の版を表示用の文字列に整形する
40 * @param version 版
41 * @returns 整形後の文字列
42 */
43export function formatBunVersion(version: string) {
44 return `bun:${version}`;
45}
46
47/**
48 * bun の版のキャッシュを削除する
49 * 次の syncBunVersion では、キャッシュを使わず、bun を実行して取得する
50 */
51export function clearBunVersionCache() {
52 versionCache = { cwd: "", version: "" };
53}
54
55/**
56 * 作業ディレクトリで bun --version を実行し、版を取得する
57 * @param $ engineのインターフェース
58 * @param cwd 作業ディレクトリ
59 * @returns 版。終了コードが0でない場合、起動できない場合、タイムアウトの場合は空文字
60 */
61async function fetchBunVersion($: EngineInterface, cwd: string) {
62 try {
63 const ran = await $.process.run(["bun", "--version"], {
64 cwd,
65 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
66 });
67
68 return ran.exitCode === 0 ? parseBunVersion(ran.stdout) : "";
69 } catch (error) {
70 $.ui.log(`bun --version failed: ${String(error)}`, { to: "debug" });
71
72 return "";
73 }
74}
75
76/**
77 * 保存済みの bun の版を読む
78 * 描画の中で呼ぶと、syncBunVersion による更新時に再描画される
79 * @param $ engineのインターフェース
80 * @returns 保存済みの版。bun のプロジェクトでない間や、取得できない間は空文字
81 */
82export function readBunVersion($: EngineInterface) {
83 return read($, bunVersionState);
84}
85
86/**
87 * 作業ディレクトリの bun の版を取得し、保存済みの値を更新する
88 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は bun を実行しない
89 * @param $ engineのインターフェース
90 */
91export async function syncBunVersion($: EngineInterface) {
92 const cwd = await $.session.cwd();
93 const entries = await $.fs.list(cwd);
94
95 if (!isBunProject(entries)) {
96 await update($, bunVersionState, () => "");
97
98 return;
99 }
100
101 if (versionCache.cwd !== cwd) {
102 versionCache = { cwd, version: await fetchBunVersion($, cwd) };
103 }
104
105 await update($, bunVersionState, () => versionCache.version);
106}
107hooks/colors.ts 32 lines1// claude codeのtheme keyに別名を付けて再利用することで既存の配色と一致させる
2// orangeを除き、基本的には新しいcolor codeは定義しない
3// mod側で利用されていないtheme keyもあるが、後でcolorを重複して定義しないために残しておく
4export const COLOR = {
5 green: "success",
6 blue: "planMode",
7 indigo: "suggestion",
8 yellow: "warning",
9 red: "error",
10 purple: "autoAccept",
11 white: "inactive",
12 grey: "text",
13 black: "inverseText",
14 orange: "#ff8700",
15} as const;
16
17const WARNING_PERCENT = 50;
18const DANGER_PERCENT = 80;
19
20/**
21 * 使用率に対応する色を返す
22 * @param percent 使用率 0から100
23 * @returns テーマキーで表した色
24 */
25export function levelColor(percent: number) {
26 if (percent >= DANGER_PERCENT) {
27 return COLOR.red;
28 }
29
30 return percent >= WARNING_PERCENT ? COLOR.yellow : COLOR.green;
31}
32hooks/ctx.ts 48 lines1import type { EngineInterface } from "claude-code";
2import { atom, read, update } from "claude-code";
3
4import { COLOR, levelColor } from "./colors";
5
6const ctxState = atom({ plugin: "statusline", key: "ctx" } as const, {});
7
8/**
9 * ctx使用率を表示用の文字列に整形する
10 * @param percent ctx使用率 0から100。無い場合は undefined
11 * @returns 整形後の文字列
12 */
13export function formatCtx(percent: number | undefined) {
14 return percent === undefined ? "ctx:--" : `ctx:${Math.round(percent)}%`;
15}
16
17/**
18 * ctx使用率に対応する色を返す
19 * @param percent ctx使用率 0から100。無い場合は undefined
20 * @returns テーマキーで表した色
21 */
22export function ctxColor(percent: number | undefined) {
23 return percent === undefined ? COLOR.blue : levelColor(percent);
24}
25
26/**
27 * 保存済みのctx使用率を読む
28 * 描画の中で呼ぶと、syncCtx による更新時に再描画される
29 * @param $ engineのインターフェース
30 * @returns 保存済みの使用率。最初の応答より前は undefined
31 */
32export async function readCtx($: EngineInterface) {
33 const { percent } = await read($, ctxState);
34
35 return percent;
36}
37
38/**
39 * セッションの現在のctx使用率を取得し、保存済みの値を更新する
40 * 最初の応答より前や compact 直後は使用率が無いため、使用率の無いオブジェクトを保存する
41 * undefined は直接保存できないため、オブジェクトのプロパティとして持つ
42 * @param $ engineのインターフェース
43 */
44export async function syncCtx($: EngineInterface) {
45 const usage = await $.session.usage();
46 await update($, ctxState, () => ({ percent: usage.context.percent }));
47}
48hooks/gitStatus.ts 150 lines1import type { EngineInterface } from "claude-code";
2import { atom, read, update } from "claude-code";
3
4import type { GitStatus } from "../types";
5import { COLOR } from "./colors";
6
7const gitStatusState = atom(
8 { plugin: "statusline", key: "gitStatus" } as const,
9 undefined,
10);
11
12const branchNameState = atom(
13 { plugin: "statusline", key: "branchName" } as const,
14 "",
15);
16
17const GIT_STATUS_COLORS = {
18 unpushed: COLOR.red,
19 uncommitted: COLOR.red,
20 clean: COLOR.green,
21 "no-git": COLOR.blue,
22} as const satisfies Record<GitStatus, string>;
23
24const AHEAD_OF_UPSTREAM = /^\+[1-9]/;
25const INITIAL_COMMIT = "(initial)";
26const DETACHED_HEAD = "(detached)";
27
28/**
29 * git status --porcelain=v2 --branch が出力する、ブランチの情報の項目名
30 */
31type BranchHeaderKey = "oid" | "head" | "upstream" | "ab";
32
33/**
34 * gitの状態に対応する色を返す
35 * @param gitStatus gitの状態
36 * @returns テーマキーで表した色
37 */
38export function gitStatusColor(gitStatus: GitStatus) {
39 return GIT_STATUS_COLORS[gitStatus];
40}
41
42/**
43 * git status --porcelain=v2 --branch の出力から、ブランチの情報の行の値を取り出す
44 * @param lines 出力の各行
45 * @param key 項目名。例: head、upstream、ab
46 * @returns 値。該当する行がない場合は undefined
47 */
48function branchHeaderValue(lines: readonly string[], key: BranchHeaderKey) {
49 const prefix = `# branch.${key} `;
50
51 return lines.find((line) => line.startsWith(prefix))?.slice(prefix.length);
52}
53
54/**
55 * 未プッシュのコミットがあるかを、ブランチの情報から判定する
56 * プッシュ対象があり、上流がないか、上流より先行している場合に該当する
57 * @param lines git status --porcelain=v2 --branch の出力の各行
58 * @returns 未プッシュのコミットがある場合は true
59 */
60function hasUnpushedCommits(lines: readonly string[]) {
61 const hasNothingToPush =
62 branchHeaderValue(lines, "oid") === INITIAL_COMMIT ||
63 branchHeaderValue(lines, "head") === DETACHED_HEAD;
64 const hasUpstream = branchHeaderValue(lines, "upstream") !== undefined;
65 const isAhead = AHEAD_OF_UPSTREAM.test(branchHeaderValue(lines, "ab") ?? "");
66
67 return !hasNothingToPush && (!hasUpstream || isAhead);
68}
69
70/**
71 * git status の結果から、gitの状態を1つに決める
72 * @param exitCode git status の終了コード
73 * @param stdout git status --porcelain=v2 --branch の出力
74 * @returns gitの状態
75 */
76function classifyGitStatus(exitCode: number, stdout: string): GitStatus {
77 if (exitCode !== 0) {
78 return "no-git";
79 }
80
81 const lines = stdout.split("\n").filter((line) => line.length > 0);
82
83 if (hasUnpushedCommits(lines)) {
84 return "unpushed";
85 }
86
87 const hasChanges = lines.some((line) => !line.startsWith("#"));
88
89 return hasChanges ? "uncommitted" : "clean";
90}
91
92/**
93 * git status の結果から、ブランチ名を取り出す
94 * @param exitCode git status の終了コード
95 * @param stdout git status --porcelain=v2 --branch の出力
96 * @returns ブランチ名。git でない場合、detached HEAD の場合は空文字
97 */
98function parseBranchName(exitCode: number, stdout: string) {
99 if (exitCode !== 0) {
100 return "";
101 }
102
103 const head = branchHeaderValue(stdout.split("\n"), "head");
104
105 return head === undefined || head === DETACHED_HEAD ? "" : head;
106}
107
108/**
109 * 保存済みのブランチ名を読む
110 * 描画の中で呼ぶと、syncGitStatus による更新時に再描画される
111 * @param $ engineのインターフェース
112 * @returns 保存済みのブランチ名。取得できない間は空文字
113 */
114export function readBranchName($: EngineInterface) {
115 return read($, branchNameState);
116}
117
118/**
119 * 保存済みのgitの状態を読む
120 * 描画の中で呼ぶと、syncGitStatus による更新時に再描画される
121 * @param $ engineのインターフェース
122 * @returns 保存済みのgitの状態。未取得の間は undefined
123 */
124export function readGitStatus($: EngineInterface) {
125 return read($, gitStatusState);
126}
127
128/**
129 * 作業ディレクトリのgitの状態とブランチ名を取得し、保存済みの値を更新する
130 * @param $ engineのインターフェース
131 */
132export async function syncGitStatus($: EngineInterface) {
133 const cwd = await $.session.cwd();
134 const ran = await $.process.run([
135 "git",
136 "--no-optional-locks",
137 "-C",
138 cwd,
139 "status",
140 "--porcelain=v2",
141 "--branch",
142 ]);
143 await update($, gitStatusState, () =>
144 classifyGitStatus(ran.exitCode, ran.stdout),
145 );
146 await update($, branchNameState, () =>
147 parseBranchName(ran.exitCode, ran.stdout),
148 );
149}
150hooks/go.ts 123 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/golang.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasExtension, hasFile, hasFolder } from "./directoryEntries";
9
10const goVersionState = atom(
11 { plugin: "statusline", key: "goVersion" } as const,
12 "",
13);
14
15const DETECT_FILES = [
16 "go.mod",
17 "go.sum",
18 "go.work",
19 "glide.yaml",
20 "Gopkg.yml",
21 "Gopkg.lock",
22 ".go-version",
23];
24const DETECT_FOLDERS = ["Godeps"];
25const DETECT_EXTENSIONS = ["go"];
26
27// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
28let versionCache = { cwd: "", version: "" };
29
30/**
31 * ディレクトリの直下の内容が、Go のプロジェクトかを判定する
32 * @param entries ディレクトリの直下の項目
33 * @returns Go のプロジェクトの場合は true
34 */
35export function isGoProject(entries: readonly Entry[]) {
36 return (
37 DETECT_FILES.some((name) => hasFile(entries, name)) ||
38 DETECT_FOLDERS.some((name) => hasFolder(entries, name)) ||
39 DETECT_EXTENSIONS.some((extension) => hasExtension(entries, extension))
40 );
41}
42
43/**
44 * go version の出力から、版を取り出す
45 * @param stdout go version の標準出力 例: go version go1.25.0 linux/amd64
46 * @returns 版。取り出せない場合は空文字
47 */
48export function parseGoVersion(stdout: string) {
49 return /^go version go(\S+)/.exec(stdout.trim())?.[1] ?? "";
50}
51
52/**
53 * Go の版を表示用の文字列に整形する
54 * @param version 版
55 * @returns 整形後の文字列
56 */
57export function formatGoVersion(version: string) {
58 return `go:${version}`;
59}
60
61/**
62 * Go の版のキャッシュを削除する
63 * 次の syncGoVersion では、キャッシュを使わず、go を実行して取得する
64 */
65export function clearGoVersionCache() {
66 versionCache = { cwd: "", version: "" };
67}
68
69/**
70 * 作業ディレクトリで go version を実行し、版を取得する
71 * @param $ engineのインターフェース
72 * @param cwd 作業ディレクトリ
73 * @returns 版。終了コードが0でない場合、起動できない場合、タイムアウトの場合は空文字
74 */
75async function fetchGoVersion($: EngineInterface, cwd: string) {
76 try {
77 // go.mod が指す新しい版を、版の取得のたびにダウンロードしないようにする
78 const ran = await $.process.run(["go", "version"], {
79 cwd,
80 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
81 env: { GOTOOLCHAIN: "local" },
82 });
83
84 return ran.exitCode === 0 ? parseGoVersion(ran.stdout) : "";
85 } catch (error) {
86 $.ui.log(`go version failed: ${String(error)}`, { to: "debug" });
87
88 return "";
89 }
90}
91
92/**
93 * 保存済みの Go の版を読む
94 * 描画の中で呼ぶと、syncGoVersion による更新時に再描画される
95 * @param $ engineのインターフェース
96 * @returns 保存済みの版。Go のプロジェクトでない間や、取得できない間は空文字
97 */
98export function readGoVersion($: EngineInterface) {
99 return read($, goVersionState);
100}
101
102/**
103 * 作業ディレクトリの Go の版を取得し、保存済みの値を更新する
104 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は go を実行しない
105 * @param $ engineのインターフェース
106 */
107export async function syncGoVersion($: EngineInterface) {
108 const cwd = await $.session.cwd();
109 const entries = await $.fs.list(cwd);
110
111 if (!isGoProject(entries)) {
112 await update($, goVersionState, () => "");
113
114 return;
115 }
116
117 if (versionCache.cwd !== cwd) {
118 versionCache = { cwd, version: await fetchGoVersion($, cwd) };
119 }
120
121 await update($, goVersionState, () => versionCache.version);
122}
123hooks/model.ts 61 lines1import type { EngineInterface } from "claude-code";
2import { atom, read, update } from "claude-code";
3
4import { COLOR } from "./colors";
5
6const modelIdState = atom(
7 { plugin: "statusline", key: "modelId" } as const,
8 "",
9);
10
11const MODEL_COLORS: readonly (readonly [RegExp, string])[] = [
12 [/sonnet/i, COLOR.green],
13 [/opus/i, COLOR.yellow],
14 [/fable/i, COLOR.red],
15];
16
17/**
18 * モデルIDを表示用の短い文字列に整形する
19 * 表示名を返すAPIが無いため、IDから組み立てる
20 * @param modelId モデルID
21 * @returns 整形後の文字列
22 */
23export function formatModel(modelId: string) {
24 return modelId
25 .replace(/^claude-/, "")
26 .replace(/\[[^[\]]*\]$/, "")
27 .replace(/-\d{8}$/, "")
28 .replace(/-(\d+)-(\d+)$/, "$1.$2")
29 .replace(/-(\d+)$/, "$1");
30}
31
32/**
33 * モデルIDに対応する色を返す
34 * @param modelId モデルID
35 * @returns テーマキーで表した色
36 */
37export function modelColor(modelId: string) {
38 return (
39 MODEL_COLORS.find(([pattern]) => pattern.test(modelId))?.[1] ?? COLOR.blue
40 );
41}
42
43/**
44 * 保存済みのモデルIDを読む
45 * 描画の中で呼ぶと、syncModelId による更新時に再描画される
46 * @param $ engineのインターフェース
47 * @returns 保存済みのモデルID。未取得の間は空文字
48 */
49export function readModelId($: EngineInterface) {
50 return read($, modelIdState);
51}
52
53/**
54 * セッションの現在のモデルIDを取得し、保存済みの値を更新する
55 * @param $ engineのインターフェース
56 */
57export async function syncModelId($: EngineInterface) {
58 const modelId = await $.session.model();
59 await update($, modelIdState, () => modelId);
60}
61hooks/nodejs.ts 131 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/nodejs.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasExtension, hasFile, hasFolder } from "./directoryEntries";
9
10const nodeVersionState = atom(
11 { plugin: "statusline", key: "nodeVersion" } as const,
12 "",
13);
14
15const DETECT_FILES = ["package.json", ".node-version", ".nvmrc"];
16const DETECT_FOLDERS = ["node_modules"];
17const DETECT_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
18const EXCLUDE_FILES = [
19 "bunfig.toml",
20 "bun.lock",
21 "bun.lockb",
22 "deno.json",
23 "deno.jsonc",
24 "deno.lock",
25];
26const EXCLUDE_FOLDERS = ["esy.lock"];
27
28// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
29let versionCache = { cwd: "", version: "" };
30
31/**
32 * ディレクトリの直下の内容が、Node.js のプロジェクトかを判定する
33 * 除外の対象があれば、他の条件によらずプロジェクトではない
34 * @param entries ディレクトリの直下の項目
35 * @returns Node.js のプロジェクトの場合は true
36 */
37export function isNodeProject(entries: readonly Entry[]) {
38 const file = (name: string) => hasFile(entries, name);
39 const folder = (name: string) => hasFolder(entries, name);
40 const extension = (name: string) => hasExtension(entries, name);
41
42 if (EXCLUDE_FILES.some(file) || EXCLUDE_FOLDERS.some(folder)) {
43 return false;
44 }
45
46 return (
47 DETECT_FILES.some(file) ||
48 DETECT_FOLDERS.some(folder) ||
49 DETECT_EXTENSIONS.some(extension)
50 );
51}
52
53/**
54 * node --version の出力から、先頭の v と空白を除いた版を取り出す
55 * @param stdout node --version の標準出力
56 * @returns 版
57 */
58export function parseNodeVersion(stdout: string) {
59 return stdout.trim().replace(/^v/, "");
60}
61
62/**
63 * Node.js の版を表示用の文字列に整形する
64 * @param version 版
65 * @returns 整形後の文字列
66 */
67export function formatNodeVersion(version: string) {
68 return `node:${version}`;
69}
70
71/**
72 * Node.js の版のキャッシュを削除する
73 * 次の syncNodeVersion では、キャッシュを使わず、node を実行して取得する
74 */
75export function clearNodeVersionCache() {
76 versionCache = { cwd: "", version: "" };
77}
78
79/**
80 * 作業ディレクトリで node --version を実行し、版を取得する
81 * @param $ engineのインターフェース
82 * @param cwd 作業ディレクトリ
83 * @returns 版。終了コードが0でない場合、起動できない場合、タイムアウトの場合は空文字
84 */
85async function fetchNodeVersion($: EngineInterface, cwd: string) {
86 try {
87 const ran = await $.process.run(["node", "--version"], {
88 cwd,
89 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
90 });
91
92 return ran.exitCode === 0 ? parseNodeVersion(ran.stdout) : "";
93 } catch (error) {
94 $.ui.log(`node --version failed: ${String(error)}`, { to: "debug" });
95
96 return "";
97 }
98}
99
100/**
101 * 保存済みの Node.js の版を読む
102 * 描画の中で呼ぶと、syncNodeVersion による更新時に再描画される
103 * @param $ engineのインターフェース
104 * @returns 保存済みの版。Node.js のプロジェクトでない間や、取得できない間は空文字
105 */
106export function readNodeVersion($: EngineInterface) {
107 return read($, nodeVersionState);
108}
109
110/**
111 * 作業ディレクトリの Node.js の版を取得し、保存済みの値を更新する
112 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は node を実行しない
113 * @param $ engineのインターフェース
114 */
115export async function syncNodeVersion($: EngineInterface) {
116 const cwd = await $.session.cwd();
117 const entries = await $.fs.list(cwd);
118
119 if (!isNodeProject(entries)) {
120 await update($, nodeVersionState, () => "");
121
122 return;
123 }
124
125 if (versionCache.cwd !== cwd) {
126 versionCache = { cwd, version: await fetchNodeVersion($, cwd) };
127 }
128
129 await update($, nodeVersionState, () => versionCache.version);
130}
131hooks/python.ts 138 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/python.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasExtension, hasFile } from "./directoryEntries";
9
10const pythonVersionState = atom(
11 { plugin: "statusline", key: "pythonVersion" } as const,
12 "",
13);
14
15const PYTHON_COMMANDS = ["python", "python3", "python2"];
16const DETECT_FILES = [
17 "requirements.txt",
18 ".python-version",
19 "pyproject.toml",
20 "Pipfile",
21 "tox.ini",
22 "setup.py",
23 "__init__.py",
24];
25const DETECT_EXTENSIONS = ["py", "ipynb"];
26
27// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
28let versionCache = { cwd: "", version: "" };
29
30/**
31 * ディレクトリの直下の内容と環境変数が、Python のプロジェクトかを判定する
32 * 仮想環境の環境変数が設定されていれば、ファイルがなくてもプロジェクトとして扱う
33 * @param entries ディレクトリの直下の項目
34 * @param virtualEnv 環境変数 VIRTUAL_ENV の値。設定されていない場合は undefined
35 * @returns Python のプロジェクトの場合は true
36 */
37export function isPythonProject(
38 entries: readonly Entry[],
39 virtualEnv: string | undefined,
40) {
41 return (
42 virtualEnv !== undefined ||
43 DETECT_FILES.some((name) => hasFile(entries, name)) ||
44 DETECT_EXTENSIONS.some((extension) => hasExtension(entries, extension))
45 );
46}
47
48/**
49 * python --version の出力から、版を取り出す
50 * @param output python --version の出力
51 * @returns 版。取り出せない場合は空文字
52 */
53export function parsePythonVersion(output: string) {
54 return output.trim().split(/\s+/)[1] ?? "";
55}
56
57/**
58 * Python の版を表示用の文字列に整形する
59 * @param version 版
60 * @returns 整形後の文字列
61 */
62export function formatPythonVersion(version: string) {
63 return `python:${version}`;
64}
65
66/**
67 * Python の版のキャッシュを削除する
68 * 次の syncPythonVersion では、キャッシュを使わず、python を実行して取得する
69 */
70export function clearPythonVersionCache() {
71 versionCache = { cwd: "", version: "" };
72}
73
74/**
75 * 作業ディレクトリで --version を実行し、最初に成功したコマンドの版を取得する
76 * python2 は版を標準エラー出力に書くため、標準出力が空なら標準エラー出力を使う
77 * @param $ engineのインターフェース
78 * @param cwd 作業ディレクトリ
79 * @returns 版。すべての候補が失敗した場合は空文字
80 */
81async function fetchPythonVersion($: EngineInterface, cwd: string) {
82 for (const command of PYTHON_COMMANDS) {
83 try {
84 const ran = await $.process.run([command, "--version"], {
85 cwd,
86 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
87 });
88
89 if (ran.exitCode === 0) {
90 return parsePythonVersion(
91 ran.stdout.trim() === "" ? ran.stderr : ran.stdout,
92 );
93 }
94 } catch (error) {
95 $.ui.log(`${command} --version failed: ${String(error)}`, {
96 to: "debug",
97 });
98 }
99 }
100
101 return "";
102}
103
104/**
105 * 保存済みの Python の版を読む
106 * 描画の中で呼ぶと、syncPythonVersion による更新時に再描画される
107 * @param $ engineのインターフェース
108 * @returns 保存済みの版。Python のプロジェクトでない間や、取得できない間は空文字
109 */
110export function readPythonVersion($: EngineInterface) {
111 return read($, pythonVersionState);
112}
113
114/**
115 * 作業ディレクトリの Python の版を取得し、保存済みの値を更新する
116 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は python を実行しない
117 * @param $ engineのインターフェース
118 */
119export async function syncPythonVersion($: EngineInterface) {
120 const cwd = await $.session.cwd();
121 const [entries, virtualEnv] = await Promise.all([
122 $.fs.list(cwd),
123 $.env.get("VIRTUAL_ENV"),
124 ]);
125
126 if (!isPythonProject(entries, virtualEnv)) {
127 await update($, pythonVersionState, () => "");
128
129 return;
130 }
131
132 if (versionCache.cwd !== cwd) {
133 versionCache = { cwd, version: await fetchPythonVersion($, cwd) };
134 }
135
136 await update($, pythonVersionState, () => versionCache.version);
137}
138hooks/rust.ts 113 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/rust.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasExtension, hasFile } from "./directoryEntries";
9
10const rustVersionState = atom(
11 { plugin: "statusline", key: "rustVersion" } as const,
12 "",
13);
14
15const DETECT_FILES = ["Cargo.toml"];
16const DETECT_EXTENSIONS = ["rs"];
17
18// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
19let versionCache = { cwd: "", version: "" };
20
21/**
22 * ディレクトリの直下の内容が、Rust のプロジェクトかを判定する
23 * @param entries ディレクトリの直下の項目
24 * @returns Rust のプロジェクトの場合は true
25 */
26export function isRustProject(entries: readonly Entry[]) {
27 return (
28 DETECT_FILES.some((name) => hasFile(entries, name)) ||
29 DETECT_EXTENSIONS.some((extension) => hasExtension(entries, extension))
30 );
31}
32
33/**
34 * rustc --version の出力から、版を取り出す
35 * @param stdout rustc --version の標準出力
36 * @returns 版。取り出せない場合は空文字
37 */
38export function parseRustVersion(stdout: string) {
39 return stdout.trim().split(/\s+/)[1] ?? "";
40}
41
42/**
43 * Rust の版を表示用の文字列に整形する
44 * @param version 版
45 * @returns 整形後の文字列
46 */
47export function formatRustVersion(version: string) {
48 return `rust:${version}`;
49}
50
51/**
52 * Rust の版のキャッシュを削除する
53 * 次の syncRustVersion では、キャッシュを使わず、rustc を実行して取得する
54 */
55export function clearRustVersionCache() {
56 versionCache = { cwd: "", version: "" };
57}
58
59/**
60 * 作業ディレクトリで rustc --version を実行し、版を取得する
61 * @param $ engineのインターフェース
62 * @param cwd 作業ディレクトリ
63 * @returns 版。終了コードが0でない場合、起動できない場合、タイムアウトの場合は空文字
64 */
65async function fetchRustVersion($: EngineInterface, cwd: string) {
66 try {
67 // 未導入のツールチェーンを、版の取得のたびにインストールしないようにする
68 const ran = await $.process.run(["rustc", "--version"], {
69 cwd,
70 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
71 env: { RUSTUP_AUTO_INSTALL: "0" },
72 });
73
74 return ran.exitCode === 0 ? parseRustVersion(ran.stdout) : "";
75 } catch (error) {
76 $.ui.log(`rustc --version failed: ${String(error)}`, { to: "debug" });
77
78 return "";
79 }
80}
81
82/**
83 * 保存済みの Rust の版を読む
84 * 描画の中で呼ぶと、syncRustVersion による更新時に再描画される
85 * @param $ engineのインターフェース
86 * @returns 保存済みの版。Rust のプロジェクトでない間や、取得できない間は空文字
87 */
88export function readRustVersion($: EngineInterface) {
89 return read($, rustVersionState);
90}
91
92/**
93 * 作業ディレクトリの Rust の版を取得し、保存済みの値を更新する
94 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は rustc を実行しない
95 * @param $ engineのインターフェース
96 */
97export async function syncRustVersion($: EngineInterface) {
98 const cwd = await $.session.cwd();
99 const entries = await $.fs.list(cwd);
100
101 if (!isRustProject(entries)) {
102 await update($, rustVersionState, () => "");
103
104 return;
105 }
106
107 if (versionCache.cwd !== cwd) {
108 versionCache = { cwd, version: await fetchRustVersion($, cwd) };
109 }
110
111 await update($, rustVersionState, () => versionCache.version);
112}
113hooks/terraform.ts 124 lines1// starship参考
2// https://github.com/starship/starship/blob/main/src/modules/terraform.rs
3import type { EngineInterface } from "claude-code";
4import { atom, read, update } from "claude-code";
5
6import { VERSION_COMMAND_TIMEOUT_MS } from "./commandTimeout";
7import type { Entry } from "./directoryEntries";
8import { hasExtension, hasFolder } from "./directoryEntries";
9
10const terraformVersionState = atom(
11 { plugin: "statusline", key: "terraformVersion" } as const,
12 "",
13);
14
15const TERRAFORM_COMMANDS = ["terraform", "tofu"];
16const DETECT_FOLDERS = [".terraform"];
17const DETECT_EXTENSIONS = ["tf", "tofu", "tfplan", "tfstate"];
18
19// 取得の制御に使うだけで描画には使わないため、$.state ではなくモジュールの変数に持つ
20let versionCache = { cwd: "", version: "" };
21
22/**
23 * ディレクトリの直下の内容が、Terraform のプロジェクトかを判定する
24 * @param entries ディレクトリの直下の項目
25 * @returns Terraform のプロジェクトの場合は true
26 */
27export function isTerraformProject(entries: readonly Entry[]) {
28 return (
29 DETECT_FOLDERS.some((name) => hasFolder(entries, name)) ||
30 DETECT_EXTENSIONS.some((name) => hasExtension(entries, name))
31 );
32}
33
34/**
35 * terraform version の出力の1行目から、Terraform または OpenTofu と先頭の v を除いた版を取り出す
36 * 2行目以降には、更新の案内などが付くことがある
37 * @param output terraform version の標準出力
38 * @returns 版。取り出せない場合は空文字
39 */
40export function parseTerraformVersion(output: string) {
41 return (output.split("\n")[0] ?? "")
42 .replace(/^(Terraform|OpenTofu) /, "")
43 .trim()
44 .replace(/^v/, "");
45}
46
47/**
48 * Terraform の版を表示用の文字列に整形する
49 * @param version 版
50 * @returns 整形後の文字列
51 */
52export function formatTerraformVersion(version: string) {
53 return `terraform:${version}`;
54}
55
56/**
57 * Terraform の版のキャッシュを削除する
58 * 次の syncTerraformVersion では、キャッシュを使わず、terraform を実行して取得する
59 */
60export function clearTerraformVersionCache() {
61 versionCache = { cwd: "", version: "" };
62}
63
64/**
65 * 作業ディレクトリで version を実行し、最初に成功したコマンドの版を取得する
66 * @param $ engineのインターフェース
67 * @param cwd 作業ディレクトリ
68 * @returns 版。すべての候補が失敗した場合は空文字
69 */
70async function fetchTerraformVersion($: EngineInterface, cwd: string) {
71 for (const command of TERRAFORM_COMMANDS) {
72 try {
73 // 更新の確認で外部へ通信するため、無効にする
74 const ran = await $.process.run([command, "version"], {
75 cwd,
76 timeoutMs: VERSION_COMMAND_TIMEOUT_MS,
77 env: { CHECKPOINT_DISABLE: "1" },
78 });
79
80 if (ran.exitCode === 0) {
81 return parseTerraformVersion(ran.stdout);
82 }
83 } catch (error) {
84 $.ui.log(`${command} version failed: ${String(error)}`, {
85 to: "debug",
86 });
87 }
88 }
89
90 return "";
91}
92
93/**
94 * 保存済みの Terraform の版を読む
95 * 描画の中で呼ぶと、syncTerraformVersion による更新時に再描画される
96 * @param $ engineのインターフェース
97 * @returns 保存済みの版。Terraform のプロジェクトでない間や、取得できない間は空文字
98 */
99export function readTerraformVersion($: EngineInterface) {
100 return read($, terraformVersionState);
101}
102
103/**
104 * 作業ディレクトリの Terraform の版を取得し、保存済みの値を更新する
105 * 版は作業ディレクトリごとに1回だけ取得し、プロジェクトでない場合は terraform を実行しない
106 * @param $ engineのインターフェース
107 */
108export async function syncTerraformVersion($: EngineInterface) {
109 const cwd = await $.session.cwd();
110 const entries = await $.fs.list(cwd);
111
112 if (!isTerraformProject(entries)) {
113 await update($, terraformVersionState, () => "");
114
115 return;
116 }
117
118 if (versionCache.cwd !== cwd) {
119 versionCache = { cwd, version: await fetchTerraformVersion($, cwd) };
120 }
121
122 await update($, terraformVersionState, () => versionCache.version);
123}
124hooks/outputStyle.ts 33 lines1import type { EngineInterface } from "claude-code";
2import { atom, read, update } from "claude-code";
3
4const outputStyleState = atom(
5 { plugin: "statusline", key: "outputStyle" } as const,
6 {},
7);
8
9/**
10 * 保存済みのoutput style名を読む
11 * 描画の中で呼ぶと、syncOutputStyle による更新時に再描画される
12 * @param $ engineのインターフェース
13 * @returns 保存済みのoutput style名。設定が無い間は undefined
14 */
15export async function readOutputStyle($: EngineInterface) {
16 const { name } = await read($, outputStyleState);
17
18 return name;
19}
20
21/**
22 * 設定のoutputStyleを取得し、保存済みの値を更新する
23 * 設定が文字列でない場合は、名前の無いオブジェクトを保存する
24 * undefined は直接保存できないため、オブジェクトのプロパティとして持つ
25 * @param $ engineのインターフェース
26 */
27export async function syncOutputStyle($: EngineInterface) {
28 const outputStyle = await $.settings.read({ key: "outputStyle" });
29 await update($, outputStyleState, () => ({
30 name: typeof outputStyle === "string" ? outputStyle : undefined,
31 }));
32}
33