pinglet-cli 0.6.2 → 0.8.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.
@@ -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
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadHudConfig = loadHudConfig;
4
+ exports.renderHudLines = renderHudLines;
5
+ const config_1 = require("./config");
6
+ const git_1 = require("./git");
7
+ const memory_1 = require("./memory");
8
+ const render_1 = require("./render");
9
+ const speed_1 = require("./speed");
10
+ const transcript_1 = require("./transcript");
11
+ /** config.json의 `hud` 섹션을 정규화해 돌려준다 (없으면 기본값 = full 프리셋). */
12
+ function loadHudConfig(config) {
13
+ return (0, config_1.normalizeHudConfig)(config.hud);
14
+ }
15
+ /**
16
+ * statusline tick에서 호출 — payload가 없으면(수동 실행 등) HUD 줄을 그리지 않는다.
17
+ * transcript는 비동기 스트림으로, git은 비동기 서브프로세스로 읽으며 네트워크는 쓰지 않는다.
18
+ * 로컬 잠금 밖에서 실행되므로 다른 훅을 막지 않는다.
19
+ */
20
+ async function renderHudLines(hud, payload, now = Date.now()) {
21
+ if (!hud.enabled)
22
+ return [];
23
+ const hasPayload = !!(payload.model || payload.context_window || payload.cwd || payload.transcript_path);
24
+ if (!hasPayload)
25
+ return [];
26
+ const { display, gitStatus } = hud;
27
+ const needsTranscript = display.showTools || display.showSkills || display.showAgents || display.showTodos ||
28
+ display.showSessionName || display.showDuration || display.showPromptCache || display.showAdvisor ||
29
+ display.showSessionTokens || display.showCompactions || display.showEffortLevel;
30
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : undefined;
31
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : payload.workspace?.current_dir;
32
+ const [transcript, git] = await Promise.all([
33
+ needsTranscript ? (0, transcript_1.parseTranscript)(transcriptPath) : Promise.resolve({ tools: [], agents: [], todos: [], skills: [] }),
34
+ gitStatus.enabled ? (0, git_1.getGitStatus)(cwd) : Promise.resolve(null),
35
+ ]);
36
+ const memory = display.showMemoryUsage && hud.lineLayout === "expanded" && hud.elementOrder.includes("memory")
37
+ ? (0, memory_1.getMemoryUsage)(now)
38
+ : null;
39
+ const speed = display.showSpeed ? (0, speed_1.getOutputSpeed)(payload, now) : null;
40
+ return (0, render_1.renderHud)({ config: hud, payload, transcript, gitStatus: git, memory, speed, now });
41
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hudLabel = hudLabel;
4
+ exports.resetHudLabels = resetHudLabels;
5
+ const render_1 = require("../render");
6
+ /** HUD 고정 문구 — statusline 언어 판정(detectLocale)을 따른다. */
7
+ const TABLE = {
8
+ en: {
9
+ context: "Context",
10
+ usage: "Usage",
11
+ weekly: "Weekly",
12
+ resetsIn: "resets in",
13
+ resets: "resets",
14
+ at: "at",
15
+ limitReached: "Limit reached",
16
+ allTodosComplete: "All todos complete",
17
+ in: "in",
18
+ cache: "cache",
19
+ cost: "Cost",
20
+ more: "more",
21
+ skills: "Skills",
22
+ approxRam: "Approx RAM",
23
+ promptCache: "Cache",
24
+ until: "until",
25
+ expired: "expired",
26
+ hit: "hit",
27
+ advisor: "Advisor",
28
+ tokens: "Tokens",
29
+ tok: "tok",
30
+ out: "out",
31
+ tokPerSec: "tok/s",
32
+ compactions: "Compactions",
33
+ },
34
+ ko: {
35
+ context: "컨텍스트",
36
+ usage: "사용량",
37
+ weekly: "주간",
38
+ resetsIn: "리셋까지",
39
+ resets: "리셋",
40
+ at: "",
41
+ limitReached: "한도 도달",
42
+ allTodosComplete: "할 일 모두 완료",
43
+ in: "입력",
44
+ cache: "캐시",
45
+ cost: "비용",
46
+ more: "개 더",
47
+ skills: "스킬",
48
+ approxRam: "RAM",
49
+ promptCache: "캐시",
50
+ until: "만료",
51
+ expired: "만료됨",
52
+ hit: "히트",
53
+ advisor: "자문 모델",
54
+ tokens: "토큰",
55
+ tok: "토큰",
56
+ out: "출력",
57
+ tokPerSec: "tok/s",
58
+ compactions: "압축",
59
+ },
60
+ ja: {
61
+ context: "コンテキスト",
62
+ usage: "使用量",
63
+ weekly: "週間",
64
+ resetsIn: "リセットまで",
65
+ resets: "リセット",
66
+ at: "",
67
+ limitReached: "上限到達",
68
+ allTodosComplete: "Todo すべて完了",
69
+ in: "入力",
70
+ cache: "キャッシュ",
71
+ cost: "コスト",
72
+ more: "件",
73
+ skills: "スキル",
74
+ approxRam: "RAM",
75
+ promptCache: "キャッシュ",
76
+ until: "期限",
77
+ expired: "期限切れ",
78
+ hit: "ヒット",
79
+ advisor: "アドバイザー",
80
+ tokens: "トークン",
81
+ tok: "トークン",
82
+ out: "出力",
83
+ tokPerSec: "tok/s",
84
+ compactions: "圧縮",
85
+ },
86
+ };
87
+ let cached = null;
88
+ function hudLabel(key) {
89
+ cached ??= TABLE[(0, render_1.detectLocale)()];
90
+ return cached[key];
91
+ }
92
+ /** 테스트용 — 언어 판정 캐시를 비운다. */
93
+ function resetHudLabels() {
94
+ cached = null;
95
+ }
@@ -0,0 +1,123 @@
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.parseVmStat = parseVmStat;
37
+ exports.parseLinuxMeminfo = parseLinuxMeminfo;
38
+ exports.toMemoryInfo = toMemoryInfo;
39
+ exports.getMemoryUsage = getMemoryUsage;
40
+ exports.formatBytes = formatBytes;
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const config_1 = require("../config");
46
+ const storage_1 = require("../storage");
47
+ const CACHE_PATH = path.join(config_1.PINGLET_DIR, "hud-cache", "memory.json");
48
+ const MEMORY_CACHE_MS = 5_000;
49
+ function parseVmStat(output) {
50
+ const pageSize = output.match(/page size of (\d+) bytes/);
51
+ const active = output.match(/Pages active:\s+(\d+)/);
52
+ const wired = output.match(/Pages wired down:\s+(\d+)/);
53
+ if (!pageSize || !active || !wired)
54
+ return null;
55
+ return { pageSize: Number(pageSize[1]), active: Number(active[1]), wired: Number(wired[1]) };
56
+ }
57
+ function parseLinuxMeminfo(output) {
58
+ const total = output.match(/^MemTotal:\s+(\d+)\s+kB/m);
59
+ const available = output.match(/^MemAvailable:\s+(\d+)\s+kB/m);
60
+ if (!total || !available)
61
+ return null;
62
+ return { totalBytes: Number(total[1]) * 1024, freeBytes: Number(available[1]) * 1024 };
63
+ }
64
+ function readRaw() {
65
+ const fallback = () => ({ totalBytes: os.totalmem(), freeBytes: os.freemem() });
66
+ try {
67
+ if (process.platform === "darwin") {
68
+ const parsed = parseVmStat((0, child_process_1.execFileSync)("/usr/bin/vm_stat", { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] }));
69
+ if (!parsed)
70
+ return fallback();
71
+ const totalBytes = os.totalmem();
72
+ return { totalBytes, freeBytes: totalBytes - (parsed.active + parsed.wired) * parsed.pageSize };
73
+ }
74
+ if (process.platform === "linux") {
75
+ return parseLinuxMeminfo(fs.readFileSync("/proc/meminfo", "utf8")) ?? fallback();
76
+ }
77
+ }
78
+ catch {
79
+ // 플랫폼 도구 실패 — Node 값으로.
80
+ }
81
+ return fallback();
82
+ }
83
+ function toMemoryInfo(raw) {
84
+ if (!Number.isFinite(raw.totalBytes) || raw.totalBytes <= 0)
85
+ return null;
86
+ const free = Number.isFinite(raw.freeBytes) ? Math.min(Math.max(raw.freeBytes, 0), raw.totalBytes) : 0;
87
+ const usedBytes = raw.totalBytes - free;
88
+ return {
89
+ totalBytes: raw.totalBytes,
90
+ usedBytes,
91
+ usedPercent: Math.min(100, Math.max(0, Math.round((usedBytes / raw.totalBytes) * 100))),
92
+ };
93
+ }
94
+ function getMemoryUsage(now = Date.now()) {
95
+ try {
96
+ const cached = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
97
+ if (cached && typeof cached.checkedAt === "number" && now - cached.checkedAt < MEMORY_CACHE_MS)
98
+ return cached.info;
99
+ }
100
+ catch {
101
+ // 캐시 없음
102
+ }
103
+ const info = toMemoryInfo(readRaw());
104
+ try {
105
+ (0, storage_1.atomicWrite)(CACHE_PATH, JSON.stringify({ checkedAt: now, info }));
106
+ }
107
+ catch {
108
+ // 캐시 실패는 표시에 영향을 주면 안 된다.
109
+ }
110
+ return info;
111
+ }
112
+ function formatBytes(bytes) {
113
+ if (!Number.isFinite(bytes) || bytes <= 0)
114
+ return "0 B";
115
+ const units = ["B", "KB", "MB", "GB", "TB"];
116
+ let value = bytes;
117
+ let unit = 0;
118
+ while (value >= 1024 && unit < units.length - 1) {
119
+ value /= 1024;
120
+ unit++;
121
+ }
122
+ return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
123
+ }