pinglet-cli 0.6.2 → 0.7.0

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.
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runStatusline = runStatusline;
37
+ exports.writeStatusline = writeStatusline;
37
38
  const fs = __importStar(require("fs"));
38
39
  const config_1 = require("../config");
39
40
  const cache_1 = require("../cache");
@@ -42,6 +43,7 @@ const picker_1 = require("../picker");
42
43
  const queue_1 = require("../queue");
43
44
  const claude_1 = require("../adapters/claude");
44
45
  const runtime_1 = require("../runtime");
46
+ const hud_1 = require("../hud");
45
47
  /**
46
48
  * online 캐시 유효 기간 — refresh(≈5분) 2주기. 서버의 온라인 판정 창
47
49
  * (ONLINE_WINDOW_MS 10분)과 정렬해, 서버가 이미 오프라인으로 세는 시점에
@@ -61,7 +63,8 @@ function onlineOthers(now) {
61
63
  }
62
64
  function readPayload() {
63
65
  try {
64
- return JSON.parse(fs.readFileSync(0, "utf8"));
66
+ const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
67
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
65
68
  }
66
69
  catch {
67
70
  return {};
@@ -138,15 +141,18 @@ function rotateSpinner(state, now) {
138
141
  };
139
142
  }
140
143
  /**
141
- * Claude Code statusLine hook 진입점.
144
+ * Claude Code statusLine hook 진입점 (로컬 잠금 안, 동기).
142
145
  * - spinner 회전(pool=1): ROTATE_MS마다 다음 메시지를 armed (설정 핫리로드로 즉시 반영)
143
146
  * - 노출 측정: stdin payload의 cost 델타로 armed 메시지의 실제 표시 시간을 누적
144
- * - statusline 표시: 지금 함께 켜져 있는 터미널 수
145
147
  * - flush/refresh 백그라운드 트리거 (Claude 사용 중 항상 호출되므로 이 경로에서)
148
+ * 출력할 줄과 payload를 돌려주면 호출부가 잠금을 놓고 `writeStatusline`으로 HUD까지 그린다.
146
149
  */
147
150
  function runStatusline() {
148
- if (!fs.existsSync(config_1.CONFIG_PATH) || !(0, config_1.loadConfig)().adapters.claude)
149
- return;
151
+ if (!fs.existsSync(config_1.CONFIG_PATH))
152
+ return undefined;
153
+ const config = (0, config_1.loadConfig)();
154
+ if (!config.adapters.claude)
155
+ return undefined;
150
156
  const payload = readPayload();
151
157
  const state = (0, cache_1.loadState)();
152
158
  const now = Date.now();
@@ -165,6 +171,23 @@ function runStatusline() {
165
171
  state.lastTickAt = now;
166
172
  (0, cache_1.saveState)(state);
167
173
  }
174
+ const lines = [];
168
175
  const others = onlineOthers(now);
169
- process.stdout.write(others !== null ? (0, render_1.formatOnlineNow)(others) : "");
176
+ if (others !== null)
177
+ lines.push((0, render_1.formatOnlineNow)(others));
178
+ return { payload, lines, hud: (0, hud_1.loadHudConfig)(config) };
179
+ }
180
+ /**
181
+ * 잠금 밖에서 HUD(모델·컨텍스트·사용량·활동)를 그려 "함께 코딩 중" 줄 아래에 출력한다.
182
+ * HUD는 실패해도 위 줄에 영향을 주면 안 된다.
183
+ */
184
+ async function writeStatusline(tick, now = Date.now(), write = (text) => { process.stdout.write(text); }) {
185
+ const lines = [...tick.lines];
186
+ try {
187
+ lines.push(...(await (0, hud_1.renderHudLines)(tick.hud, tick.payload, now)));
188
+ }
189
+ catch {
190
+ // 손상된 transcript·git 오류 등 — 이번 tick은 HUD 없이 그린다.
191
+ }
192
+ write(lines.join("\n"));
170
193
  }
package/dist/config.js CHANGED
@@ -93,6 +93,7 @@ function loadConfig() {
93
93
  const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
94
94
  if (typeof existing.apiBaseUrl !== "string" || typeof existing.createdAt !== "string" ||
95
95
  (existing.userToken !== undefined && typeof existing.userToken !== "string") ||
96
+ (existing.hud !== undefined && !object(existing.hud)) ||
96
97
  (existing.installations !== undefined && !object(existing.installations)) ||
97
98
  (existing.adapters !== undefined && !object(existing.adapters))) {
98
99
  throw new Error(`Invalid ${exports.CONFIG_PATH}; original file preserved.`);
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.quotaBar = exports.contextBar = exports.critical = exports.warning = exports.label = exports.gitBranch = exports.git = exports.project = exports.model = exports.dim = exports.magenta = exports.cyan = exports.red = exports.yellow = exports.green = exports.RESET = void 0;
4
+ exports.contextColor = contextColor;
5
+ exports.quotaColor = quotaColor;
6
+ exports.RESET = "\x1b[0m";
7
+ const DIM = "\x1b[2m";
8
+ const ANSI_BY_NAME = {
9
+ dim: DIM,
10
+ red: "\x1b[31m",
11
+ green: "\x1b[32m",
12
+ yellow: "\x1b[33m",
13
+ magenta: "\x1b[35m",
14
+ cyan: "\x1b[36m",
15
+ brightBlue: "\x1b[94m",
16
+ brightMagenta: "\x1b[95m",
17
+ };
18
+ function resolveAnsi(value, fallback) {
19
+ if (value === undefined || value === null)
20
+ return fallback;
21
+ if (typeof value === "number")
22
+ return `\x1b[38;5;${value}m`;
23
+ if (typeof value === "string" && value.startsWith("#") && value.length === 7) {
24
+ const r = parseInt(value.slice(1, 3), 16);
25
+ const g = parseInt(value.slice(3, 5), 16);
26
+ const b = parseInt(value.slice(5, 7), 16);
27
+ return `\x1b[38;2;${r};${g};${b}m`;
28
+ }
29
+ return ANSI_BY_NAME[value] ?? fallback;
30
+ }
31
+ const colorize = (text, ansi) => `${ansi}${text}${exports.RESET}`;
32
+ const green = (t) => colorize(t, ANSI_BY_NAME.green);
33
+ exports.green = green;
34
+ const yellow = (t) => colorize(t, ANSI_BY_NAME.yellow);
35
+ exports.yellow = yellow;
36
+ const red = (t) => colorize(t, ANSI_BY_NAME.red);
37
+ exports.red = red;
38
+ const cyan = (t) => colorize(t, ANSI_BY_NAME.cyan);
39
+ exports.cyan = cyan;
40
+ const magenta = (t) => colorize(t, ANSI_BY_NAME.magenta);
41
+ exports.magenta = magenta;
42
+ const dim = (t) => colorize(t, DIM);
43
+ exports.dim = dim;
44
+ const model = (t, c) => colorize(t, resolveAnsi(c.model, ANSI_BY_NAME.cyan));
45
+ exports.model = model;
46
+ const project = (t, c) => colorize(t, resolveAnsi(c.project, ANSI_BY_NAME.yellow));
47
+ exports.project = project;
48
+ const git = (t, c) => colorize(t, resolveAnsi(c.git, ANSI_BY_NAME.magenta));
49
+ exports.git = git;
50
+ const gitBranch = (t, c) => colorize(t, resolveAnsi(c.gitBranch, ANSI_BY_NAME.cyan));
51
+ exports.gitBranch = gitBranch;
52
+ const label = (t, c) => colorize(t, resolveAnsi(c.label, DIM));
53
+ exports.label = label;
54
+ const warning = (t, c) => colorize(t, resolveAnsi(c.warning, ANSI_BY_NAME.yellow));
55
+ exports.warning = warning;
56
+ const critical = (t, c) => colorize(t, resolveAnsi(c.critical, ANSI_BY_NAME.red));
57
+ exports.critical = critical;
58
+ function contextColor(percent, c, thresholds) {
59
+ if (percent >= thresholds.critical)
60
+ return resolveAnsi(c.critical, ANSI_BY_NAME.red);
61
+ if (percent >= thresholds.warning)
62
+ return resolveAnsi(c.warning, ANSI_BY_NAME.yellow);
63
+ return resolveAnsi(c.context, ANSI_BY_NAME.green);
64
+ }
65
+ function quotaColor(percent, c) {
66
+ if (percent >= 90)
67
+ return resolveAnsi(c.critical, ANSI_BY_NAME.red);
68
+ if (percent >= 75)
69
+ return resolveAnsi(c.usageWarning, ANSI_BY_NAME.brightMagenta);
70
+ return resolveAnsi(c.usage, ANSI_BY_NAME.brightBlue);
71
+ }
72
+ function bar(percent, width, ansi, c) {
73
+ const safeWidth = Math.max(0, Math.round(width));
74
+ const safePercent = Number.isFinite(percent) ? Math.min(100, Math.max(0, percent)) : 0;
75
+ const filled = Math.round((safePercent / 100) * safeWidth);
76
+ return `${ansi}${c.barFilled.repeat(filled)}${DIM}${c.barEmpty.repeat(safeWidth - filled)}${exports.RESET}`;
77
+ }
78
+ const contextBar = (percent, width, c, thresholds) => bar(percent, width, contextColor(percent, c, thresholds), c);
79
+ exports.contextBar = contextBar;
80
+ const quotaBar = (percent, width, c) => bar(percent, width, quotaColor(percent, c), c);
81
+ exports.quotaBar = quotaBar;
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ /**
3
+ * HUD 설정 — claude-hud(jarrodwatts/claude-hud)와 같은 키 체계를 쓴다.
4
+ * `~/.pinglet/config.json`의 `hud` 섹션에 저장되며, 값이 없거나 잘못되면
5
+ * 기본값으로 되돌린다 (statusline 렌더 경로에서 예외를 던지지 않기 위해).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.PRESETS = exports.TOGGLE_KEYS = exports.DEFAULT_HUD_CONFIG = exports.DEFAULT_MERGE_GROUPS = exports.KNOWN_FIRST_LINE_SEGMENTS = exports.KNOWN_ELEMENTS = exports.DEFAULT_ELEMENT_ORDER = void 0;
9
+ exports.setToggle = setToggle;
10
+ exports.applyPreset = applyPreset;
11
+ exports.normalizeHudConfig = normalizeHudConfig;
12
+ exports.DEFAULT_ELEMENT_ORDER = [
13
+ "project",
14
+ "context",
15
+ "usage",
16
+ "promptCache",
17
+ "memory",
18
+ "tools",
19
+ "skills",
20
+ "agents",
21
+ "todos",
22
+ ];
23
+ exports.KNOWN_ELEMENTS = new Set(exports.DEFAULT_ELEMENT_ORDER);
24
+ exports.KNOWN_FIRST_LINE_SEGMENTS = new Set([
25
+ "model",
26
+ "project",
27
+ "advisor",
28
+ "sessionName",
29
+ "duration",
30
+ "cost",
31
+ "speed",
32
+ ]);
33
+ /** 컨텍스트·사용량·캐시는 한 줄로 묶는다 (폭이 모자라면 렌더러가 다시 나눈다). */
34
+ exports.DEFAULT_MERGE_GROUPS = [["context", "usage", "promptCache"]];
35
+ /** 설정 파일에 없을 때 쓰는 기본값 — claude-hud의 "full" 프리셋과 같다. */
36
+ exports.DEFAULT_HUD_CONFIG = {
37
+ enabled: true,
38
+ lineLayout: "expanded",
39
+ showSeparators: false,
40
+ pathLevels: 1,
41
+ maxWidth: null,
42
+ forceMaxWidth: false,
43
+ elementOrder: [...exports.DEFAULT_ELEMENT_ORDER],
44
+ projectLineOrder: [],
45
+ gitStatus: {
46
+ enabled: true,
47
+ showDirty: true,
48
+ showAheadBehind: true,
49
+ showFileStats: true,
50
+ branchOverflow: "truncate",
51
+ pushWarningThreshold: 0,
52
+ pushCriticalThreshold: 0,
53
+ },
54
+ display: {
55
+ showModel: true,
56
+ modelFormat: "full",
57
+ showProject: true,
58
+ showContextBar: true,
59
+ contextValue: "percent",
60
+ showTokenBreakdown: true,
61
+ showUsage: true,
62
+ usageValue: "percent",
63
+ usageBarEnabled: true,
64
+ showResetLabel: true,
65
+ usageCompact: false,
66
+ showModelScopedUsage: true,
67
+ timeFormat: "relative",
68
+ showTools: true,
69
+ toolNameMaxLength: 0,
70
+ toolsMaxVisible: 4,
71
+ showSkills: true,
72
+ showAgents: true,
73
+ showTodos: true,
74
+ showSessionName: true,
75
+ showDuration: true,
76
+ showCost: true,
77
+ showSpeed: true,
78
+ showEffortLevel: true,
79
+ effortFormat: "full",
80
+ showSessionTokens: true,
81
+ showCompactions: true,
82
+ showMemoryUsage: true,
83
+ showPromptCache: true,
84
+ promptCacheTtlSeconds: 300,
85
+ showCacheHitRate: true,
86
+ showAdvisor: true,
87
+ advisorOverride: "",
88
+ mergeGroups: exports.DEFAULT_MERGE_GROUPS.map((group) => [...group]),
89
+ contextWarningThreshold: 70,
90
+ contextCriticalThreshold: 85,
91
+ usageThreshold: 0,
92
+ sevenDayThreshold: 80,
93
+ },
94
+ colors: {
95
+ context: "green",
96
+ usage: "brightBlue",
97
+ warning: "yellow",
98
+ usageWarning: "brightMagenta",
99
+ critical: "red",
100
+ model: "cyan",
101
+ project: "yellow",
102
+ git: "magenta",
103
+ gitBranch: "cyan",
104
+ label: "dim",
105
+ barFilled: "█",
106
+ barEmpty: "░",
107
+ },
108
+ };
109
+ /** `pinglet hud --show/--hide`에서 받는 토글 이름 → 설정 경로. */
110
+ exports.TOGGLE_KEYS = {
111
+ model: "display.showModel",
112
+ project: "display.showProject",
113
+ context: "display.showContextBar",
114
+ "context-bar": "display.showContextBar",
115
+ usage: "display.showUsage",
116
+ "model-scoped-usage": "display.showModelScopedUsage",
117
+ tools: "display.showTools",
118
+ skills: "display.showSkills",
119
+ agents: "display.showAgents",
120
+ todos: "display.showTodos",
121
+ memory: "display.showMemoryUsage",
122
+ "prompt-cache": "display.showPromptCache",
123
+ "cache-hit": "display.showCacheHitRate",
124
+ advisor: "display.showAdvisor",
125
+ "session-name": "display.showSessionName",
126
+ duration: "display.showDuration",
127
+ cost: "display.showCost",
128
+ speed: "display.showSpeed",
129
+ effort: "display.showEffortLevel",
130
+ "session-tokens": "display.showSessionTokens",
131
+ compactions: "display.showCompactions",
132
+ "token-breakdown": "display.showTokenBreakdown",
133
+ "usage-bar": "display.usageBarEnabled",
134
+ "reset-label": "display.showResetLabel",
135
+ "usage-compact": "display.usageCompact",
136
+ git: "gitStatus.enabled",
137
+ "git-dirty": "gitStatus.showDirty",
138
+ "git-ahead-behind": "gitStatus.showAheadBehind",
139
+ "git-files": "gitStatus.showFileStats",
140
+ };
141
+ /** 프리셋이 손대는 토글 — 나머지 키는 프리셋을 바꿔도 그대로 둔다. */
142
+ const PRESET_TOGGLES = [
143
+ "model", "project", "context", "usage", "model-scoped-usage", "tools", "skills", "agents", "todos",
144
+ "memory", "prompt-cache", "cache-hit", "advisor", "session-name", "duration", "cost", "speed", "effort",
145
+ "session-tokens", "compactions", "git", "git-ahead-behind", "git-files",
146
+ ];
147
+ /**
148
+ * 프리셋 — claude-hud의 setup/configure 흐름과 같은 세 단계.
149
+ * full: 모두 켜기
150
+ * essential: 활동 줄(tools/skills/agents/todos) + 모델·프로젝트·컨텍스트·git 브랜치, 정보 최소
151
+ * minimal: 모델명 + context bar만
152
+ */
153
+ exports.PRESETS = {
154
+ full: new Set(PRESET_TOGGLES),
155
+ essential: new Set([
156
+ "model", "project", "context", "usage", "model-scoped-usage", "prompt-cache", "cache-hit",
157
+ "tools", "skills", "agents", "todos", "git",
158
+ ]),
159
+ minimal: new Set(["model", "context"]),
160
+ };
161
+ function setToggle(config, name, value) {
162
+ const target = exports.TOGGLE_KEYS[name];
163
+ if (!target)
164
+ return;
165
+ const [section, key] = target.split(".");
166
+ config[section][key] = value;
167
+ }
168
+ function applyPreset(config, preset) {
169
+ const next = structuredClone(config);
170
+ const on = exports.PRESETS[preset];
171
+ for (const name of PRESET_TOGGLES)
172
+ setToggle(next, name, on.has(name));
173
+ // 프리셋은 배치까지 정하므로 병합 그룹도 기본값(컨텍스트·사용량·캐시 한 줄)으로 되돌린다.
174
+ next.display.mergeGroups = exports.DEFAULT_MERGE_GROUPS.map((group) => [...group]);
175
+ return next;
176
+ }
177
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
178
+ const bool = (v, fallback) => (typeof v === "boolean" ? v : fallback);
179
+ const oneOf = (v, allowed, fallback) => allowed.includes(v) ? v : fallback;
180
+ const int = (v, fallback, min = 0, max = 100_000) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? Math.trunc(v) : fallback;
181
+ function uniqueKnown(value, known) {
182
+ if (!Array.isArray(value))
183
+ return [];
184
+ const out = [];
185
+ for (const item of value) {
186
+ if (typeof item === "string" && known.has(item) && !out.includes(item))
187
+ out.push(item);
188
+ }
189
+ return out;
190
+ }
191
+ const COLOR_NAMES = [
192
+ "dim", "red", "green", "yellow", "magenta", "cyan", "brightBlue", "brightMagenta",
193
+ ];
194
+ function colorValue(value, fallback) {
195
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255)
196
+ return value;
197
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value))
198
+ return value;
199
+ if (typeof value === "string" && COLOR_NAMES.includes(value))
200
+ return value;
201
+ return fallback;
202
+ }
203
+ /** 바 문자: 제어·서식 문자를 제외한 단일 grapheme만 허용한다 (터미널 방어). */
204
+ function barChar(value, fallback) {
205
+ if (typeof value !== "string" || value.length === 0 || value.length > 4)
206
+ return fallback;
207
+ if (/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value))
208
+ return fallback;
209
+ return Array.from(value).length === 1 ? value : fallback;
210
+ }
211
+ /** 어떤 입력이 와도 유효한 HudConfig를 돌려준다. 알 수 없는 키는 버린다. */
212
+ function normalizeHudConfig(raw) {
213
+ const d = exports.DEFAULT_HUD_CONFIG;
214
+ if (!isObject(raw))
215
+ return structuredClone(d);
216
+ const display = isObject(raw.display) ? raw.display : {};
217
+ const git = isObject(raw.gitStatus) ? raw.gitStatus : {};
218
+ const colors = isObject(raw.colors) ? raw.colors : {};
219
+ const elementOrder = uniqueKnown(raw.elementOrder, exports.KNOWN_ELEMENTS);
220
+ const mergeGroups = Array.isArray(display.mergeGroups)
221
+ ? display.mergeGroups
222
+ .map((group) => uniqueKnown(group, exports.KNOWN_ELEMENTS))
223
+ .filter((group) => group.length > 1)
224
+ : d.display.mergeGroups.map((group) => [...group]);
225
+ const warning = int(display.contextWarningThreshold, d.display.contextWarningThreshold, 0, 100);
226
+ const critical = int(display.contextCriticalThreshold, d.display.contextCriticalThreshold, 0, 100);
227
+ return {
228
+ enabled: bool(raw.enabled, d.enabled),
229
+ lineLayout: oneOf(raw.lineLayout, ["compact", "expanded"], d.lineLayout),
230
+ showSeparators: bool(raw.showSeparators, d.showSeparators),
231
+ pathLevels: oneOf(raw.pathLevels, [1, 2, 3, "full"], d.pathLevels),
232
+ maxWidth: typeof raw.maxWidth === "number" && raw.maxWidth > 0 ? Math.min(1000, Math.trunc(raw.maxWidth)) : null,
233
+ forceMaxWidth: bool(raw.forceMaxWidth, d.forceMaxWidth),
234
+ elementOrder: elementOrder.length > 0 ? elementOrder : [...d.elementOrder],
235
+ projectLineOrder: uniqueKnown(raw.projectLineOrder, exports.KNOWN_FIRST_LINE_SEGMENTS),
236
+ gitStatus: {
237
+ enabled: bool(git.enabled, d.gitStatus.enabled),
238
+ showDirty: bool(git.showDirty, d.gitStatus.showDirty),
239
+ showAheadBehind: bool(git.showAheadBehind, d.gitStatus.showAheadBehind),
240
+ showFileStats: bool(git.showFileStats, d.gitStatus.showFileStats),
241
+ branchOverflow: oneOf(git.branchOverflow, ["truncate", "wrap"], d.gitStatus.branchOverflow),
242
+ pushWarningThreshold: int(git.pushWarningThreshold, d.gitStatus.pushWarningThreshold, 0, 10_000),
243
+ pushCriticalThreshold: int(git.pushCriticalThreshold, d.gitStatus.pushCriticalThreshold, 0, 10_000),
244
+ },
245
+ display: {
246
+ showModel: bool(display.showModel, d.display.showModel),
247
+ modelFormat: oneOf(display.modelFormat, ["full", "compact", "short"], d.display.modelFormat),
248
+ showProject: bool(display.showProject, d.display.showProject),
249
+ showContextBar: bool(display.showContextBar, d.display.showContextBar),
250
+ contextValue: oneOf(display.contextValue, ["percent", "tokens", "remaining", "both"], d.display.contextValue),
251
+ showTokenBreakdown: bool(display.showTokenBreakdown, d.display.showTokenBreakdown),
252
+ showUsage: bool(display.showUsage, d.display.showUsage),
253
+ usageValue: oneOf(display.usageValue, ["percent", "remaining"], d.display.usageValue),
254
+ usageBarEnabled: bool(display.usageBarEnabled, d.display.usageBarEnabled),
255
+ showResetLabel: bool(display.showResetLabel, d.display.showResetLabel),
256
+ usageCompact: bool(display.usageCompact, d.display.usageCompact),
257
+ showModelScopedUsage: bool(display.showModelScopedUsage, d.display.showModelScopedUsage),
258
+ timeFormat: oneOf(display.timeFormat, ["relative", "absolute", "both"], d.display.timeFormat),
259
+ showTools: bool(display.showTools, d.display.showTools),
260
+ toolNameMaxLength: int(display.toolNameMaxLength, d.display.toolNameMaxLength, 0, 64),
261
+ toolsMaxVisible: int(display.toolsMaxVisible, d.display.toolsMaxVisible, 0, 20),
262
+ showSkills: bool(display.showSkills, d.display.showSkills),
263
+ showAgents: bool(display.showAgents, d.display.showAgents),
264
+ showTodos: bool(display.showTodos, d.display.showTodos),
265
+ showSessionName: bool(display.showSessionName, d.display.showSessionName),
266
+ showDuration: bool(display.showDuration, d.display.showDuration),
267
+ showCost: bool(display.showCost, d.display.showCost),
268
+ showSpeed: bool(display.showSpeed, d.display.showSpeed),
269
+ showEffortLevel: bool(display.showEffortLevel, d.display.showEffortLevel),
270
+ effortFormat: oneOf(display.effortFormat, ["full", "symbol", "text"], d.display.effortFormat),
271
+ showSessionTokens: bool(display.showSessionTokens, d.display.showSessionTokens),
272
+ showCompactions: bool(display.showCompactions, d.display.showCompactions),
273
+ showMemoryUsage: bool(display.showMemoryUsage, d.display.showMemoryUsage),
274
+ showPromptCache: bool(display.showPromptCache, d.display.showPromptCache),
275
+ promptCacheTtlSeconds: int(display.promptCacheTtlSeconds, d.display.promptCacheTtlSeconds, 1, 86_400),
276
+ showCacheHitRate: bool(display.showCacheHitRate, d.display.showCacheHitRate),
277
+ showAdvisor: bool(display.showAdvisor, d.display.showAdvisor),
278
+ advisorOverride: typeof display.advisorOverride === "string" ? display.advisorOverride.slice(0, 64) : "",
279
+ mergeGroups,
280
+ contextWarningThreshold: Math.min(warning, critical),
281
+ contextCriticalThreshold: critical,
282
+ usageThreshold: int(display.usageThreshold, d.display.usageThreshold, 0, 100),
283
+ sevenDayThreshold: int(display.sevenDayThreshold, d.display.sevenDayThreshold, 0, 100),
284
+ },
285
+ colors: {
286
+ context: colorValue(colors.context, d.colors.context),
287
+ usage: colorValue(colors.usage, d.colors.usage),
288
+ warning: colorValue(colors.warning, d.colors.warning),
289
+ usageWarning: colorValue(colors.usageWarning, d.colors.usageWarning),
290
+ critical: colorValue(colors.critical, d.colors.critical),
291
+ model: colorValue(colors.model, d.colors.model),
292
+ project: colorValue(colors.project, d.colors.project),
293
+ git: colorValue(colors.git, d.colors.git),
294
+ gitBranch: colorValue(colors.gitBranch, d.colors.gitBranch),
295
+ label: colorValue(colors.label, d.colors.label),
296
+ barFilled: barChar(colors.barFilled, d.colors.barFilled),
297
+ barEmpty: barChar(colors.barEmpty, d.colors.barEmpty),
298
+ },
299
+ };
300
+ }
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseFileStats = parseFileStats;
37
+ exports.parseNumstat = parseNumstat;
38
+ exports.getGitStatus = getGitStatus;
39
+ const child_process_1 = require("child_process");
40
+ const path = __importStar(require("path"));
41
+ const GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
42
+ const BRANCH_MAX_LEN = 64;
43
+ const GIT_ENV = {
44
+ ...process.env,
45
+ GIT_OPTIONAL_LOCKS: "0",
46
+ GIT_TERMINAL_PROMPT: "0",
47
+ GCM_INTERACTIVE: "Never",
48
+ };
49
+ function git(cwd, args, timeout) {
50
+ return new Promise((resolve, reject) => {
51
+ (0, child_process_1.execFile)("git", args, { cwd, env: GIT_ENV, encoding: "utf8", timeout, maxBuffer: GIT_MAX_OUTPUT_BYTES, windowsHide: true }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
52
+ });
53
+ }
54
+ function sanitizeBranch(value) {
55
+ const cleaned = value.replace(/[\p{Cc}\p{Cf}]/gu, "").trim();
56
+ return cleaned.length > BRANCH_MAX_LEN ? `${cleaned.slice(0, BRANCH_MAX_LEN - 1)}…` : cleaned;
57
+ }
58
+ async function resolveRef(cwd) {
59
+ const branch = (await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], 1000)).trim();
60
+ if (branch && branch !== "HEAD")
61
+ return branch;
62
+ try {
63
+ const tag = (await git(cwd, ["describe", "--tags", "--exact-match", "HEAD"], 1000)).trim();
64
+ if (tag)
65
+ return tag;
66
+ }
67
+ catch {
68
+ // 태그가 없는 detached HEAD — 짧은 SHA로.
69
+ }
70
+ const sha = (await git(cwd, ["rev-parse", "--short", "HEAD"], 1000)).trim();
71
+ return sha ? `detached:${sha}` : null;
72
+ }
73
+ function parsePorcelainPath(field) {
74
+ if (field.startsWith('"') && field.endsWith('"')) {
75
+ try {
76
+ return JSON.parse(field);
77
+ }
78
+ catch {
79
+ return field.slice(1, -1);
80
+ }
81
+ }
82
+ return field;
83
+ }
84
+ /** `git status --porcelain` → 파일 개수와 추적 파일 목록 (Starship과 같은 분류). */
85
+ function parseFileStats(porcelain) {
86
+ const stats = { modified: 0, added: 0, deleted: 0, untracked: 0, trackedFiles: [] };
87
+ for (const line of porcelain.split("\n").filter(Boolean)) {
88
+ if (line.length < 2)
89
+ continue;
90
+ const index = line[0];
91
+ const worktree = line[1];
92
+ const pathField = line.slice(2).trimStart();
93
+ if (line.startsWith("??")) {
94
+ stats.untracked++;
95
+ }
96
+ else if (index === "A") {
97
+ stats.added++;
98
+ const fullPath = parsePorcelainPath(pathField);
99
+ stats.trackedFiles.push({ basename: fullPath.split("/").pop() ?? fullPath, fullPath, type: "added" });
100
+ }
101
+ else if (index === "D" || worktree === "D") {
102
+ stats.deleted++;
103
+ const fullPath = parsePorcelainPath(pathField);
104
+ stats.trackedFiles.push({ basename: fullPath.split("/").pop() ?? fullPath, fullPath, type: "deleted" });
105
+ }
106
+ else if (index === "M" || worktree === "M" || index === "R" || index === "C") {
107
+ stats.modified++;
108
+ // 이름 변경은 "old -> new"로 오므로 목적지 경로를 쓴다.
109
+ const fullPath = parsePorcelainPath(pathField.split(" -> ").pop() ?? pathField);
110
+ stats.trackedFiles.push({ basename: fullPath.split("/").pop() ?? fullPath, fullPath, type: "modified" });
111
+ }
112
+ }
113
+ return stats;
114
+ }
115
+ /** numstat의 이름 변경 표기(`old => new`, `dir/{old => new}`)를 목적지 경로로 바꾼다. */
116
+ function numstatDestination(filePath) {
117
+ const brace = filePath.match(/^(.*)\{(.*) => (.*)\}(.*)$/);
118
+ if (brace)
119
+ return `${brace[1]}${brace[3]}${brace[4]}`.replace(/\/{2,}/g, "/");
120
+ const arrow = filePath.indexOf(" => ");
121
+ return arrow === -1 ? filePath : filePath.slice(arrow + 4);
122
+ }
123
+ function parseNumstat(output, trackedPaths) {
124
+ const total = { added: 0, deleted: 0 };
125
+ const perFile = new Map();
126
+ for (const line of output.trim().split("\n").filter(Boolean)) {
127
+ const parts = line.split("\t");
128
+ if (parts.length < 3)
129
+ continue;
130
+ const added = parseInt(parts[0], 10);
131
+ const deleted = parseInt(parts[1], 10);
132
+ if (Number.isNaN(added) || Number.isNaN(deleted))
133
+ continue; // binary
134
+ let filePath = parts[2];
135
+ if (!trackedPaths.has(filePath)) {
136
+ const destination = numstatDestination(filePath);
137
+ if (trackedPaths.has(destination))
138
+ filePath = destination;
139
+ }
140
+ total.added += added;
141
+ total.deleted += deleted;
142
+ perFile.set(filePath, { added, deleted });
143
+ }
144
+ return { total, perFile };
145
+ }
146
+ function buildGitHubRefUrl(remote, ref) {
147
+ const base = remote
148
+ .replace(/^git@github\.com:/, "https://github.com/")
149
+ .replace(/^ssh:\/\/git@github\.com\//, "https://github.com/")
150
+ .replace(/\.git$/, "");
151
+ if (!base.startsWith("https://github.com/"))
152
+ return undefined;
153
+ const detached = ref.match(/^detached:([0-9a-f]+)$/);
154
+ if (detached)
155
+ return `${base}/commit/${detached[1]}`;
156
+ return `${base}/tree/${ref.split("/").map(encodeURIComponent).join("/")}`;
157
+ }
158
+ async function getGitStatus(cwd) {
159
+ if (!cwd || !path.isAbsolute(cwd))
160
+ return null;
161
+ let branch;
162
+ try {
163
+ branch = await resolveRef(cwd);
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ if (!branch)
169
+ return null;
170
+ let isDirty = false;
171
+ let fileStats;
172
+ let lineDiff;
173
+ try {
174
+ const out = (await git(cwd, ["-c", "core.quotePath=false", "--no-optional-locks", "status", "--porcelain"], 1000)).trim();
175
+ isDirty = out.length > 0;
176
+ if (isDirty)
177
+ fileStats = parseFileStats(out);
178
+ }
179
+ catch {
180
+ // status 실패는 배지에서 dirty 표시만 빠진다.
181
+ }
182
+ if (isDirty) {
183
+ try {
184
+ const out = await git(cwd, ["-c", "core.quotePath=false", "--no-optional-locks", "diff", "--numstat", "HEAD"], 2000);
185
+ const tracked = new Set(fileStats?.trackedFiles.map((f) => f.fullPath) ?? []);
186
+ const { total, perFile } = parseNumstat(out, tracked);
187
+ lineDiff = total;
188
+ for (const file of fileStats?.trackedFiles ?? []) {
189
+ const diff = perFile.get(file.fullPath);
190
+ if (diff)
191
+ file.lineDiff = diff;
192
+ }
193
+ }
194
+ catch {
195
+ // 줄 수 차이는 선택 정보.
196
+ }
197
+ }
198
+ let ahead = 0;
199
+ let behind = 0;
200
+ try {
201
+ const parts = (await git(cwd, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], 1000)).trim().split(/\s+/);
202
+ if (parts.length === 2) {
203
+ behind = parseInt(parts[0], 10) || 0;
204
+ ahead = parseInt(parts[1], 10) || 0;
205
+ }
206
+ }
207
+ catch {
208
+ // upstream 없음
209
+ }
210
+ let branchUrl;
211
+ try {
212
+ branchUrl = buildGitHubRefUrl((await git(cwd, ["remote", "get-url", "origin"], 1000)).trim(), branch);
213
+ }
214
+ catch {
215
+ // origin 없음
216
+ }
217
+ return { branch: sanitizeBranch(branch), isDirty, ahead, behind, fileStats, lineDiff, branchUrl };
218
+ }