oh-my-customcode 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +54 -0
- package/dist/index.js +54 -0
- package/package.json +1 -1
- package/templates/.claude/statusline.sh +128 -17
package/dist/cli/index.js
CHANGED
|
@@ -12805,6 +12805,11 @@ async function listFiles(dir2, options = {}) {
|
|
|
12805
12805
|
}
|
|
12806
12806
|
return files;
|
|
12807
12807
|
}
|
|
12808
|
+
async function copyFile(src, dest) {
|
|
12809
|
+
const fs = await import("node:fs/promises");
|
|
12810
|
+
await ensureDirectory(dirname2(dest));
|
|
12811
|
+
await fs.copyFile(src, dest);
|
|
12812
|
+
}
|
|
12808
12813
|
function matchesPattern(filename, pattern) {
|
|
12809
12814
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
12810
12815
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
@@ -13984,6 +13989,53 @@ async function installSingleComponent(targetDir, component, options, result) {
|
|
|
13984
13989
|
result.warnings.push(`Failed to install ${component}: ${message}`);
|
|
13985
13990
|
}
|
|
13986
13991
|
}
|
|
13992
|
+
async function installStatusline(targetDir, options, _result) {
|
|
13993
|
+
const layout = getProviderLayout();
|
|
13994
|
+
const srcPath = resolveTemplatePath(join4(layout.rootDir, "statusline.sh"));
|
|
13995
|
+
const destPath = join4(targetDir, layout.rootDir, "statusline.sh");
|
|
13996
|
+
if (!await fileExists(srcPath)) {
|
|
13997
|
+
debug("install.statusline_not_found", { path: srcPath });
|
|
13998
|
+
return;
|
|
13999
|
+
}
|
|
14000
|
+
if (await fileExists(destPath)) {
|
|
14001
|
+
if (!options.force && !options.backup) {
|
|
14002
|
+
debug("install.statusline_skipped", { reason: "exists" });
|
|
14003
|
+
return;
|
|
14004
|
+
}
|
|
14005
|
+
}
|
|
14006
|
+
await copyFile(srcPath, destPath);
|
|
14007
|
+
const fs2 = await import("node:fs/promises");
|
|
14008
|
+
await fs2.chmod(destPath, 493);
|
|
14009
|
+
debug("install.statusline_installed", {});
|
|
14010
|
+
}
|
|
14011
|
+
async function installSettingsLocal(targetDir, result) {
|
|
14012
|
+
const layout = getProviderLayout();
|
|
14013
|
+
const settingsPath = join4(targetDir, layout.rootDir, "settings.local.json");
|
|
14014
|
+
const statusLineConfig = {
|
|
14015
|
+
statusLine: {
|
|
14016
|
+
type: "command",
|
|
14017
|
+
command: ".claude/statusline.sh",
|
|
14018
|
+
padding: 0
|
|
14019
|
+
}
|
|
14020
|
+
};
|
|
14021
|
+
if (await fileExists(settingsPath)) {
|
|
14022
|
+
try {
|
|
14023
|
+
const existing = await readJsonFile(settingsPath);
|
|
14024
|
+
if (!existing.statusLine) {
|
|
14025
|
+
existing.statusLine = statusLineConfig.statusLine;
|
|
14026
|
+
await writeJsonFile(settingsPath, existing);
|
|
14027
|
+
debug("install.settings_local_merged", {});
|
|
14028
|
+
} else {
|
|
14029
|
+
debug("install.settings_local_skipped", { reason: "statusLine exists" });
|
|
14030
|
+
}
|
|
14031
|
+
} catch {
|
|
14032
|
+
result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
|
|
14033
|
+
}
|
|
14034
|
+
return;
|
|
14035
|
+
}
|
|
14036
|
+
await writeJsonFile(settingsPath, statusLineConfig);
|
|
14037
|
+
debug("install.settings_local_created", {});
|
|
14038
|
+
}
|
|
13987
14039
|
async function installEntryDocWithTracking(targetDir, options, result) {
|
|
13988
14040
|
const language = options.language ?? DEFAULT_LANGUAGE2;
|
|
13989
14041
|
const overwrite = !!(options.force || options.backup);
|
|
@@ -14010,6 +14062,8 @@ async function install(options) {
|
|
|
14010
14062
|
await checkAndWarnExisting(options.targetDir, !!options.force, !!options.backup, result);
|
|
14011
14063
|
await verifyTemplateDirectory();
|
|
14012
14064
|
await installAllComponents(options.targetDir, options, result);
|
|
14065
|
+
await installStatusline(options.targetDir, options, result);
|
|
14066
|
+
await installSettingsLocal(options.targetDir, result);
|
|
14013
14067
|
await installEntryDocWithTracking(options.targetDir, options, result);
|
|
14014
14068
|
await updateInstallConfig(options.targetDir, options, result.installedComponents);
|
|
14015
14069
|
result.success = true;
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,11 @@ function resolveTemplatePath(relativePath) {
|
|
|
163
163
|
const packageRoot = getPackageRoot();
|
|
164
164
|
return join(packageRoot, "templates", relativePath);
|
|
165
165
|
}
|
|
166
|
+
async function copyFile(src, dest) {
|
|
167
|
+
const fs = await import("node:fs/promises");
|
|
168
|
+
await ensureDirectory(dirname(dest));
|
|
169
|
+
await fs.copyFile(src, dest);
|
|
170
|
+
}
|
|
166
171
|
function matchesPattern(filename, pattern) {
|
|
167
172
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
168
173
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
@@ -846,6 +851,53 @@ async function installSingleComponent(targetDir, component, options, result) {
|
|
|
846
851
|
result.warnings.push(`Failed to install ${component}: ${message}`);
|
|
847
852
|
}
|
|
848
853
|
}
|
|
854
|
+
async function installStatusline(targetDir, options, _result) {
|
|
855
|
+
const layout = getProviderLayout();
|
|
856
|
+
const srcPath = resolveTemplatePath(join3(layout.rootDir, "statusline.sh"));
|
|
857
|
+
const destPath = join3(targetDir, layout.rootDir, "statusline.sh");
|
|
858
|
+
if (!await fileExists(srcPath)) {
|
|
859
|
+
debug("install.statusline_not_found", { path: srcPath });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (await fileExists(destPath)) {
|
|
863
|
+
if (!options.force && !options.backup) {
|
|
864
|
+
debug("install.statusline_skipped", { reason: "exists" });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
await copyFile(srcPath, destPath);
|
|
869
|
+
const fs = await import("node:fs/promises");
|
|
870
|
+
await fs.chmod(destPath, 493);
|
|
871
|
+
debug("install.statusline_installed", {});
|
|
872
|
+
}
|
|
873
|
+
async function installSettingsLocal(targetDir, result) {
|
|
874
|
+
const layout = getProviderLayout();
|
|
875
|
+
const settingsPath = join3(targetDir, layout.rootDir, "settings.local.json");
|
|
876
|
+
const statusLineConfig = {
|
|
877
|
+
statusLine: {
|
|
878
|
+
type: "command",
|
|
879
|
+
command: ".claude/statusline.sh",
|
|
880
|
+
padding: 0
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
if (await fileExists(settingsPath)) {
|
|
884
|
+
try {
|
|
885
|
+
const existing = await readJsonFile(settingsPath);
|
|
886
|
+
if (!existing.statusLine) {
|
|
887
|
+
existing.statusLine = statusLineConfig.statusLine;
|
|
888
|
+
await writeJsonFile(settingsPath, existing);
|
|
889
|
+
debug("install.settings_local_merged", {});
|
|
890
|
+
} else {
|
|
891
|
+
debug("install.settings_local_skipped", { reason: "statusLine exists" });
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
await writeJsonFile(settingsPath, statusLineConfig);
|
|
899
|
+
debug("install.settings_local_created", {});
|
|
900
|
+
}
|
|
849
901
|
async function installEntryDocWithTracking(targetDir, options, result) {
|
|
850
902
|
const language = options.language ?? DEFAULT_LANGUAGE;
|
|
851
903
|
const overwrite = !!(options.force || options.backup);
|
|
@@ -872,6 +924,8 @@ async function install(options) {
|
|
|
872
924
|
await checkAndWarnExisting(options.targetDir, !!options.force, !!options.backup, result);
|
|
873
925
|
await verifyTemplateDirectory();
|
|
874
926
|
await installAllComponents(options.targetDir, options, result);
|
|
927
|
+
await installStatusline(options.targetDir, options, result);
|
|
928
|
+
await installSettingsLocal(options.targetDir, result);
|
|
875
929
|
await installEntryDocWithTracking(options.targetDir, options, result);
|
|
876
930
|
await updateInstallConfig(options.targetDir, options, result.installedComponents);
|
|
877
931
|
result.success = true;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Reads JSON from stdin (Claude Code statusline API, ~300ms intervals)
|
|
5
5
|
# and outputs a formatted status line, e.g.:
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# $0.05 | my-project | develop | PR #160 | CTX:42%
|
|
8
8
|
#
|
|
9
9
|
# JSON input structure:
|
|
10
10
|
# {
|
|
@@ -71,6 +71,7 @@ IFS=$'\t' read -r model_name project_dir ctx_pct ctx_size cost_usd <<< "$(
|
|
|
71
71
|
|
|
72
72
|
# ---------------------------------------------------------------------------
|
|
73
73
|
# 5. Model display name + color (bash 3.2 compatible case pattern matching)
|
|
74
|
+
# Model detection (kept for internal reference, not displayed in statusline)
|
|
74
75
|
# ---------------------------------------------------------------------------
|
|
75
76
|
case "$model_name" in
|
|
76
77
|
*[Oo]pus*) model_display="Opus"; model_color="${COLOR_OPUS}" ;;
|
|
@@ -79,6 +80,30 @@ case "$model_name" in
|
|
|
79
80
|
*) model_display="$model_name"; model_color="${COLOR_RESET}" ;;
|
|
80
81
|
esac
|
|
81
82
|
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# 5b. Cost display — format and colorize session API cost
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Ensure cost_usd is a valid number (fallback to 0)
|
|
87
|
+
if [[ -z "$cost_usd" ]] || ! printf '%f' "$cost_usd" >/dev/null 2>&1; then
|
|
88
|
+
cost_usd="0"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
cost_display=$(printf '$%.2f' "$cost_usd")
|
|
92
|
+
|
|
93
|
+
# Color by cost threshold (cents for integer comparison)
|
|
94
|
+
cost_cents=$(printf '%.0f' "$(echo "$cost_usd * 100" | bc 2>/dev/null || echo 0)")
|
|
95
|
+
if ! [[ "$cost_cents" =~ ^[0-9]+$ ]]; then
|
|
96
|
+
cost_cents=0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [[ "$cost_cents" -ge 500 ]]; then
|
|
100
|
+
cost_color="${COLOR_CTX_CRIT}" # Red (>= $5.00)
|
|
101
|
+
elif [[ "$cost_cents" -ge 100 ]]; then
|
|
102
|
+
cost_color="${COLOR_CTX_WARN}" # Yellow ($1.00 - $4.99)
|
|
103
|
+
else
|
|
104
|
+
cost_color="${COLOR_CTX_OK}" # Green (< $1.00)
|
|
105
|
+
fi
|
|
106
|
+
|
|
82
107
|
# ---------------------------------------------------------------------------
|
|
83
108
|
# 6. Project name — basename of workspace current_dir
|
|
84
109
|
# ---------------------------------------------------------------------------
|
|
@@ -108,7 +133,85 @@ if [[ -f "$git_head_file" ]]; then
|
|
|
108
133
|
fi
|
|
109
134
|
|
|
110
135
|
# ---------------------------------------------------------------------------
|
|
111
|
-
#
|
|
136
|
+
# 7b. Branch URL — for OSC 8 clickable link
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
branch_url=""
|
|
139
|
+
if [[ -n "$git_branch" && -n "$project_dir" ]]; then
|
|
140
|
+
# Get remote URL from git config
|
|
141
|
+
git_config="${project_dir}/.git/config"
|
|
142
|
+
if [[ -f "$git_config" ]]; then
|
|
143
|
+
# Extract remote origin URL from git config (no subprocess)
|
|
144
|
+
remote_url=""
|
|
145
|
+
in_remote_origin=false
|
|
146
|
+
while IFS= read -r line; do
|
|
147
|
+
case "$line" in
|
|
148
|
+
'[remote "origin"]')
|
|
149
|
+
in_remote_origin=true
|
|
150
|
+
;;
|
|
151
|
+
'['*)
|
|
152
|
+
in_remote_origin=false
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
if $in_remote_origin; then
|
|
156
|
+
case "$line" in
|
|
157
|
+
*url\ =*)
|
|
158
|
+
remote_url="${line#*url = }"
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
esac
|
|
164
|
+
done < "$git_config"
|
|
165
|
+
|
|
166
|
+
# Convert remote URL to HTTPS browse URL
|
|
167
|
+
if [[ -n "$remote_url" ]]; then
|
|
168
|
+
case "$remote_url" in
|
|
169
|
+
git@github.com:*)
|
|
170
|
+
# git@github.com:owner/repo.git → https://github.com/owner/repo
|
|
171
|
+
repo_path="${remote_url#git@github.com:}"
|
|
172
|
+
repo_path="${repo_path%.git}"
|
|
173
|
+
branch_url="https://github.com/${repo_path}/tree/${git_branch}"
|
|
174
|
+
;;
|
|
175
|
+
https://github.com/*)
|
|
176
|
+
# https://github.com/owner/repo.git → https://github.com/owner/repo
|
|
177
|
+
repo_path="${remote_url#https://github.com/}"
|
|
178
|
+
repo_path="${repo_path%.git}"
|
|
179
|
+
branch_url="https://github.com/${repo_path}/tree/${git_branch}"
|
|
180
|
+
;;
|
|
181
|
+
esac
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# 8. PR number — cached by branch to avoid gh call on every refresh
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
pr_display=""
|
|
190
|
+
if [[ -n "$git_branch" ]] && command -v gh >/dev/null 2>&1; then
|
|
191
|
+
cache_file="/tmp/statusline-pr-${project_name}"
|
|
192
|
+
cached_branch=""
|
|
193
|
+
cached_pr=""
|
|
194
|
+
|
|
195
|
+
if [[ -f "$cache_file" ]]; then
|
|
196
|
+
IFS=$'\t' read -r cached_branch cached_pr < "$cache_file"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
if [[ "$cached_branch" == "$git_branch" ]]; then
|
|
200
|
+
# Cache hit — use cached PR number
|
|
201
|
+
pr_number="$cached_pr"
|
|
202
|
+
else
|
|
203
|
+
# Cache miss — query gh and update cache
|
|
204
|
+
pr_number="$(gh pr view --json number -q .number 2>/dev/null || echo "")"
|
|
205
|
+
printf '%s\t%s\n' "$git_branch" "$pr_number" > "$cache_file"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [[ -n "$pr_number" ]]; then
|
|
209
|
+
pr_display="PR #${pr_number}"
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# 9. Context percentage with color
|
|
112
215
|
# ---------------------------------------------------------------------------
|
|
113
216
|
# ctx_pct may arrive as a float (e.g. 42.5); truncate to integer for comparison
|
|
114
217
|
ctx_int="${ctx_pct%%.*}"
|
|
@@ -127,26 +230,34 @@ fi
|
|
|
127
230
|
|
|
128
231
|
ctx_display="CTX:${ctx_int}%"
|
|
129
232
|
|
|
130
|
-
# ---------------------------------------------------------------------------
|
|
131
|
-
# 9. Cost formatting — always two decimal places
|
|
132
|
-
# ---------------------------------------------------------------------------
|
|
133
|
-
cost_display="$(printf '$%.2f' "$cost_usd")"
|
|
134
|
-
|
|
135
233
|
# ---------------------------------------------------------------------------
|
|
136
234
|
# 10. Assemble and output the status line
|
|
137
235
|
# ---------------------------------------------------------------------------
|
|
138
|
-
#
|
|
236
|
+
# Format branch with optional OSC 8 hyperlink
|
|
237
|
+
if [[ -n "$branch_url" && -n "${COLOR_RESET}" ]]; then
|
|
238
|
+
# OSC 8 hyperlink: ESC]8;;URL BEL visible-text ESC]8;; BEL
|
|
239
|
+
branch_display=$'\033]8;;'"${branch_url}"$'\a'"${git_branch}"$'\033]8;;\a'
|
|
240
|
+
else
|
|
241
|
+
branch_display="$git_branch"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Build the PR segment (with separator) if present
|
|
245
|
+
pr_segment=""
|
|
246
|
+
if [[ -n "$pr_display" ]]; then
|
|
247
|
+
pr_segment=" | ${pr_display}"
|
|
248
|
+
fi
|
|
249
|
+
|
|
139
250
|
if [[ -n "$git_branch" ]]; then
|
|
140
|
-
printf "${
|
|
141
|
-
"$
|
|
251
|
+
printf "${cost_color}%s${COLOR_RESET} | %s | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
|
|
252
|
+
"$cost_display" \
|
|
142
253
|
"$project_name" \
|
|
143
|
-
"$
|
|
144
|
-
"$
|
|
145
|
-
"$
|
|
254
|
+
"$branch_display" \
|
|
255
|
+
"$pr_segment" \
|
|
256
|
+
"$ctx_display"
|
|
146
257
|
else
|
|
147
|
-
printf "${
|
|
148
|
-
"$
|
|
258
|
+
printf "${cost_color}%s${COLOR_RESET} | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
|
|
259
|
+
"$cost_display" \
|
|
149
260
|
"$project_name" \
|
|
150
|
-
"$
|
|
151
|
-
"$
|
|
261
|
+
"$pr_segment" \
|
|
262
|
+
"$ctx_display"
|
|
152
263
|
fi
|