@ipv9/tokentracker-cli 0.39.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.
- package/LICENSE +21 -0
- package/README.ja.md +464 -0
- package/README.ko.md +464 -0
- package/README.md +476 -0
- package/README.zh-CN.md +469 -0
- package/bin/tracker.js +52 -0
- package/dashboard/dist/app-icon.png +0 -0
- package/dashboard/dist/apple-touch-icon.png +0 -0
- package/dashboard/dist/assets/ActivityHeatmap-BxBGHY1z.js +42 -0
- package/dashboard/dist/assets/Card-GROxUc_6.js +1 -0
- package/dashboard/dist/assets/DashboardPage-BZ5VFoBk.js +19 -0
- package/dashboard/dist/assets/DevicePage-CQts7ivB.js +1 -0
- package/dashboard/dist/assets/DialogTitle-DqFsk26U.js +1 -0
- package/dashboard/dist/assets/FadeIn-HBBXW_3q.js +1 -0
- package/dashboard/dist/assets/HeaderGithubStar-COD2HfaF.js +1 -0
- package/dashboard/dist/assets/IpCheckPage-Cn0l8-BQ.js +15 -0
- package/dashboard/dist/assets/LandingPage-CmV1BMU4.js +4356 -0
- package/dashboard/dist/assets/LeaderboardAvatar-CiIpWZwb.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-BU0mlR3g.js +6 -0
- package/dashboard/dist/assets/LeaderboardProfileModal-C49_3Xm9.js +72 -0
- package/dashboard/dist/assets/LeaderboardProfilePage-Bh1n0BAM.js +1 -0
- package/dashboard/dist/assets/LimitsPage-fZAmwLWT.js +2 -0
- package/dashboard/dist/assets/LocalOnlyNotice-D3rQdA-P.js +1 -0
- package/dashboard/dist/assets/LoginPage-enO4XP-c.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-pitJEpN1.js +1 -0
- package/dashboard/dist/assets/Select-tT9x4ntD.js +1 -0
- package/dashboard/dist/assets/SelectItemText-C7F7eIm0.js +1 -0
- package/dashboard/dist/assets/SettingsPage-pDasZNU2.js +1 -0
- package/dashboard/dist/assets/SkillsPage-DEu7FY-2.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-DGcO7rbp.js +1 -0
- package/dashboard/dist/assets/WrappedPage-D_1ID213.js +1 -0
- package/dashboard/dist/assets/agent-logos-DXQVg1xy.js +1 -0
- package/dashboard/dist/assets/arrow-up-right-DNLo92Ba.js +1 -0
- package/dashboard/dist/assets/download-DbBBBpJF.js +1 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-BKC3PZkM.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-DdrQ8KBw.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-900-normal-CmoKXrdK.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-900-normal-Cu5MFKsu.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-Br6WNUGb.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-CrmBTzU2.woff2 +0 -0
- package/dashboard/dist/assets/index-BeoRn2gJ.js +2 -0
- package/dashboard/dist/assets/info-Bh5qTz6k.js +1 -0
- package/dashboard/dist/assets/main-BIY3Nj6I.js +969 -0
- package/dashboard/dist/assets/main-G_F9Iu_x.css +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-B4iJ-bBc.js +1 -0
- package/dashboard/dist/assets/use-native-settings-DHjPFDv8.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-odqe9OY7.js +1 -0
- package/dashboard/dist/assets/useCurrency-BsSZiZMP.js +1 -0
- package/dashboard/dist/assets/useOpenInteractionType-BQhgq4rv.js +12 -0
- package/dashboard/dist/brand-logos/antigravity.svg +1 -0
- package/dashboard/dist/brand-logos/claude-code.svg +1 -0
- package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
- package/dashboard/dist/brand-logos/codex.svg +15 -0
- package/dashboard/dist/brand-logos/copilot.svg +1 -0
- package/dashboard/dist/brand-logos/cursor.svg +1 -0
- package/dashboard/dist/brand-logos/gemini.svg +1 -0
- package/dashboard/dist/brand-logos/grok.svg +1 -0
- package/dashboard/dist/brand-logos/hermes.svg +1 -0
- package/dashboard/dist/brand-logos/kilo.svg +4 -0
- package/dashboard/dist/brand-logos/kimi.svg +1 -0
- package/dashboard/dist/brand-logos/kiro.svg +1 -0
- package/dashboard/dist/brand-logos/openclaw.svg +22 -0
- package/dashboard/dist/brand-logos/opencode.svg +3 -0
- package/dashboard/dist/clawd/happy.svg +161 -0
- package/dashboard/dist/clawd/idle/collapse.svg +101 -0
- package/dashboard/dist/clawd/idle/doze.svg +72 -0
- package/dashboard/dist/clawd/idle/follow.svg +64 -0
- package/dashboard/dist/clawd/idle/living.svg +196 -0
- package/dashboard/dist/clawd/idle/look.svg +115 -0
- package/dashboard/dist/clawd/idle/yawn.svg +158 -0
- package/dashboard/dist/clawd/mini/alert.svg +129 -0
- package/dashboard/dist/clawd/mini/crabwalk.svg +76 -0
- package/dashboard/dist/clawd/mini/enter-sleep.svg +65 -0
- package/dashboard/dist/clawd/mini/enter.svg +115 -0
- package/dashboard/dist/clawd/mini/happy.svg +124 -0
- package/dashboard/dist/clawd/mini/idle-tight.svg +15 -0
- package/dashboard/dist/clawd/mini/idle.svg +83 -0
- package/dashboard/dist/clawd/mini/peek.svg +82 -0
- package/dashboard/dist/clawd/mini/sleep.svg +112 -0
- package/dashboard/dist/clawd/react/double.svg +108 -0
- package/dashboard/dist/clawd/react/drag.svg +102 -0
- package/dashboard/dist/clawd/react/left.svg +102 -0
- package/dashboard/dist/clawd/react/right.svg +102 -0
- package/dashboard/dist/clawd/sleep/collapse-sleep.svg +247 -0
- package/dashboard/dist/clawd/sleep/sleeping.svg +118 -0
- package/dashboard/dist/clawd/sleep/wake.svg +277 -0
- package/dashboard/dist/clawd/static-base.svg +21 -0
- package/dashboard/dist/clawd/status/disconnected.svg +136 -0
- package/dashboard/dist/clawd/status/error.svg +94 -0
- package/dashboard/dist/clawd/status/notification.svg +134 -0
- package/dashboard/dist/clawd/working/building.svg +231 -0
- package/dashboard/dist/clawd/working/carrying.svg +107 -0
- package/dashboard/dist/clawd/working/conducting.svg +118 -0
- package/dashboard/dist/clawd/working/confused.svg +113 -0
- package/dashboard/dist/clawd/working/debugger.svg +91 -0
- package/dashboard/dist/clawd/working/juggling.svg +82 -0
- package/dashboard/dist/clawd/working/overheated.svg +75 -0
- package/dashboard/dist/clawd/working/pushing.svg +125 -0
- package/dashboard/dist/clawd/working/sweeping.svg +79 -0
- package/dashboard/dist/clawd/working/thinking.svg +119 -0
- package/dashboard/dist/clawd/working/typing.svg +153 -0
- package/dashboard/dist/clawd/working/ultrathink.svg +166 -0
- package/dashboard/dist/clawd/working/wizard.svg +98 -0
- package/dashboard/dist/dashboard-dark.png +0 -0
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon.ico +0 -0
- package/dashboard/dist/feed.xml +34 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +355 -0
- package/dashboard/dist/llms.txt +53 -0
- package/dashboard/dist/neon_bg.png +0 -0
- package/dashboard/dist/robots.txt +7 -0
- package/dashboard/dist/share.html +140 -0
- package/dashboard/dist/sitemap.xml +27 -0
- package/dashboard/dist/widgets-overview.png +0 -0
- package/package.json +78 -0
- package/scripts/install-local-service.sh +140 -0
- package/scripts/uninstall-local-service.sh +21 -0
- package/src/cli.js +95 -0
- package/src/commands/device-login.js +161 -0
- package/src/commands/diagnostics.js +38 -0
- package/src/commands/doctor.js +96 -0
- package/src/commands/init.js +1195 -0
- package/src/commands/serve.js +351 -0
- package/src/commands/status.js +609 -0
- package/src/commands/sync.js +2285 -0
- package/src/commands/uninstall.js +189 -0
- package/src/commands/wrapped.js +150 -0
- package/src/lib/antigravity-paths.js +48 -0
- package/src/lib/browser-auth.js +281 -0
- package/src/lib/categorizer-utils.js +232 -0
- package/src/lib/claude-categorizer.js +1319 -0
- package/src/lib/claude-config.js +204 -0
- package/src/lib/cli-ui.js +179 -0
- package/src/lib/codex-config.js +350 -0
- package/src/lib/codex-context-breakdown.js +400 -0
- package/src/lib/codex-rollout-parser.js +623 -0
- package/src/lib/codex-token-refresh.js +112 -0
- package/src/lib/cursor-config.js +425 -0
- package/src/lib/debug-flags.js +7 -0
- package/src/lib/diagnostics.js +272 -0
- package/src/lib/doctor.js +338 -0
- package/src/lib/fs.js +91 -0
- package/src/lib/gemini-config.js +288 -0
- package/src/lib/grok-hook.js +351 -0
- package/src/lib/init-flow.js +42 -0
- package/src/lib/local-api.js +1861 -0
- package/src/lib/openclaw-hook.js +420 -0
- package/src/lib/openclaw-session-plugin.js +520 -0
- package/src/lib/opencode-config.js +99 -0
- package/src/lib/passive-mode.js +185 -0
- package/src/lib/pricing/curated-overrides.json +79 -0
- package/src/lib/pricing/index.js +143 -0
- package/src/lib/pricing/litellm-fetcher.js +172 -0
- package/src/lib/pricing/matcher.js +255 -0
- package/src/lib/pricing/seed-snapshot.json +1 -0
- package/src/lib/progress.js +76 -0
- package/src/lib/project-usage-purge.js +106 -0
- package/src/lib/prompt.js +24 -0
- package/src/lib/proxy-env.js +131 -0
- package/src/lib/rollout.js +8442 -0
- package/src/lib/runtime-config.js +122 -0
- package/src/lib/skill-usage.js +267 -0
- package/src/lib/skills-manager.js +1127 -0
- package/src/lib/source-metadata.js +48 -0
- package/src/lib/sqlite-reader.js +136 -0
- package/src/lib/static-server.js +59 -0
- package/src/lib/subscriptions.js +453 -0
- package/src/lib/tracker-paths.js +16 -0
- package/src/lib/upload-throttle.js +148 -0
- package/src/lib/usage-limits.js +1806 -0
- package/src/lib/wrapped-aggregator.js +225 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const { ensureDir, readJson, writeJson } = require("./fs");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EVENT = "SessionEnd";
|
|
7
|
+
|
|
8
|
+
async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
9
|
+
const existing = await readJson(settingsPath);
|
|
10
|
+
const settings = normalizeSettings(existing);
|
|
11
|
+
const hooks = normalizeHooks(settings.hooks);
|
|
12
|
+
const entries = normalizeEntries(hooks[event]);
|
|
13
|
+
|
|
14
|
+
const normalized = normalizeEntriesForCommand(entries, hookCommand);
|
|
15
|
+
if (normalized.changed) {
|
|
16
|
+
const nextHooks = { ...hooks, [event]: normalized.entries };
|
|
17
|
+
const nextSettings = { ...settings, hooks: nextHooks };
|
|
18
|
+
const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
|
|
19
|
+
return { changed: true, backupPath };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (hasHook(entries, hookCommand)) {
|
|
23
|
+
return { changed: false, backupPath: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nextEntries = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
|
|
27
|
+
const nextHooks = { ...hooks, [event]: nextEntries };
|
|
28
|
+
const nextSettings = { ...settings, hooks: nextHooks };
|
|
29
|
+
|
|
30
|
+
const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
|
|
31
|
+
return { changed: true, backupPath };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
35
|
+
const existing = await readJson(settingsPath);
|
|
36
|
+
if (!existing) return { removed: false, skippedReason: "settings-missing" };
|
|
37
|
+
|
|
38
|
+
const settings = normalizeSettings(existing);
|
|
39
|
+
const hooks = normalizeHooks(settings.hooks);
|
|
40
|
+
const entries = normalizeEntries(hooks[event]);
|
|
41
|
+
if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
|
|
42
|
+
|
|
43
|
+
let removed = false;
|
|
44
|
+
const nextEntries = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const res = stripHookFromEntry(entry, hookCommand);
|
|
47
|
+
if (res.removed) removed = true;
|
|
48
|
+
if (res.entry) nextEntries.push(res.entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!removed) return { removed: false, skippedReason: "hook-missing" };
|
|
52
|
+
|
|
53
|
+
const nextHooks = { ...hooks };
|
|
54
|
+
if (nextEntries.length > 0) nextHooks[event] = nextEntries;
|
|
55
|
+
else delete nextHooks[event];
|
|
56
|
+
|
|
57
|
+
const nextSettings = { ...settings };
|
|
58
|
+
if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
|
|
59
|
+
else delete nextSettings.hooks;
|
|
60
|
+
|
|
61
|
+
const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
|
|
62
|
+
return { removed: true, skippedReason: null, backupPath };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function isClaudeHookConfigured({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
66
|
+
const settings = await readJson(settingsPath);
|
|
67
|
+
if (!settings || typeof settings !== "object") return false;
|
|
68
|
+
const hooks = settings.hooks;
|
|
69
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
70
|
+
const entries = normalizeEntries(hooks[event]);
|
|
71
|
+
return hasHook(entries, hookCommand);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generic Session-hook command builder. CodeBuddy CLI is a Claude-Code fork
|
|
75
|
+
// and uses the exact same settings.json hook schema, so this function works
|
|
76
|
+
// for any source that accepts the `node notify.cjs --source=<name>` contract.
|
|
77
|
+
function buildHookCommand(notifyPath, source) {
|
|
78
|
+
const cmd = typeof notifyPath === "string" ? notifyPath : "";
|
|
79
|
+
const src = typeof source === "string" && source ? source : "claude";
|
|
80
|
+
return `/usr/bin/env node ${quoteArg(cmd)} --source=${src}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildClaudeHookCommand(notifyPath) {
|
|
84
|
+
return buildHookCommand(notifyPath, "claude");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeSettings(raw) {
|
|
88
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeHooks(raw) {
|
|
92
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeEntries(raw) {
|
|
96
|
+
return Array.isArray(raw) ? raw.slice() : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeCommand(cmd) {
|
|
100
|
+
if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
|
|
101
|
+
if (typeof cmd === "string") return cmd.trim();
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasHook(entries, hookCommand) {
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (!entry || typeof entry !== "object") continue;
|
|
108
|
+
if (entry.command && commandsEqual(entry.command, hookCommand)) return true;
|
|
109
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
110
|
+
for (const hook of hooks) {
|
|
111
|
+
if (hook && commandsEqual(hook.command, hookCommand)) return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stripHookFromEntry(entry, hookCommand) {
|
|
118
|
+
if (!entry || typeof entry !== "object") return { entry, removed: false };
|
|
119
|
+
|
|
120
|
+
if (entry.command) {
|
|
121
|
+
if (commandsEqual(entry.command, hookCommand)) return { entry: null, removed: true };
|
|
122
|
+
return { entry, removed: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : null;
|
|
126
|
+
if (!hooks) return { entry, removed: false };
|
|
127
|
+
|
|
128
|
+
const nextHooks = hooks.filter((hook) => !commandsEqual(hook?.command, hookCommand));
|
|
129
|
+
if (nextHooks.length === hooks.length) return { entry, removed: false };
|
|
130
|
+
if (nextHooks.length === 0) return { entry: null, removed: true };
|
|
131
|
+
|
|
132
|
+
return { entry: { ...entry, hooks: nextHooks }, removed: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeEntriesForCommand(entries, hookCommand) {
|
|
136
|
+
let changed = false;
|
|
137
|
+
const nextEntries = entries.map((entry) => {
|
|
138
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
139
|
+
if (entry.command && commandsEqual(entry.command, hookCommand)) {
|
|
140
|
+
if (entry.type !== "command") {
|
|
141
|
+
changed = true;
|
|
142
|
+
return { ...entry, type: "command" };
|
|
143
|
+
}
|
|
144
|
+
return entry;
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(entry.hooks)) return entry;
|
|
147
|
+
let hooksChanged = false;
|
|
148
|
+
const nextHooks = entry.hooks.map((hook) => {
|
|
149
|
+
if (hook && commandsEqual(hook.command, hookCommand)) {
|
|
150
|
+
if (hook.type !== "command") {
|
|
151
|
+
hooksChanged = true;
|
|
152
|
+
return { ...hook, type: "command" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return hook;
|
|
156
|
+
});
|
|
157
|
+
if (!hooksChanged) return entry;
|
|
158
|
+
changed = true;
|
|
159
|
+
return { ...entry, hooks: nextHooks };
|
|
160
|
+
});
|
|
161
|
+
return { entries: nextEntries, changed };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function commandsEqual(a, b) {
|
|
165
|
+
const left = normalizeCommand(a);
|
|
166
|
+
const right = normalizeCommand(b);
|
|
167
|
+
return Boolean(left && right && left === right);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function quoteArg(value) {
|
|
171
|
+
const v = typeof value === "string" ? value : "";
|
|
172
|
+
if (!v) return '""';
|
|
173
|
+
if (/^[A-Za-z0-9_\-./:@]+$/.test(v)) return v;
|
|
174
|
+
return `"${v.replace(/"/g, '\\"')}"`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function writeClaudeSettings({ settingsPath, settings }) {
|
|
178
|
+
await ensureDir(path.dirname(settingsPath));
|
|
179
|
+
let backupPath = null;
|
|
180
|
+
try {
|
|
181
|
+
const st = await fs.stat(settingsPath);
|
|
182
|
+
if (st && st.isFile()) {
|
|
183
|
+
backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
184
|
+
await fs.copyFile(settingsPath, backupPath);
|
|
185
|
+
}
|
|
186
|
+
} catch (_e) {
|
|
187
|
+
// Ignore missing file.
|
|
188
|
+
}
|
|
189
|
+
await writeJson(settingsPath, settings);
|
|
190
|
+
return backupPath;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
upsertClaudeHook,
|
|
195
|
+
removeClaudeHook,
|
|
196
|
+
isClaudeHookConfigured,
|
|
197
|
+
buildClaudeHookCommand,
|
|
198
|
+
buildHookCommand,
|
|
199
|
+
// Aliases for callers that want a name unbiased toward Claude (the schema
|
|
200
|
+
// applies equally to CodeBuddy and any future Claude-Code fork).
|
|
201
|
+
upsertSessionHook: upsertClaudeHook,
|
|
202
|
+
removeSessionHook: removeClaudeHook,
|
|
203
|
+
isSessionHookConfigured: isClaudeHookConfigured,
|
|
204
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const readline = require("node:readline");
|
|
2
|
+
|
|
3
|
+
const RESET = "\x1b[0m";
|
|
4
|
+
const BOLD = "\x1b[1m";
|
|
5
|
+
const DIM = "\x1b[2m";
|
|
6
|
+
const CYAN = "\x1b[36m";
|
|
7
|
+
const GREEN = "\x1b[32m";
|
|
8
|
+
const YELLOW = "\x1b[33m";
|
|
9
|
+
const BLUE = "\x1b[34m";
|
|
10
|
+
const UNDERLINE = "\x1b[4m";
|
|
11
|
+
|
|
12
|
+
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
13
|
+
|
|
14
|
+
function isInteractive() {
|
|
15
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatLine(line, width) {
|
|
19
|
+
if (!width) return line;
|
|
20
|
+
const raw = String(line || "");
|
|
21
|
+
const pad = Math.max(0, width - raw.length);
|
|
22
|
+
return raw + " ".repeat(pad);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderBox(lines, { padding = 1 } = {}) {
|
|
26
|
+
const content = lines.map((line) => String(line || ""));
|
|
27
|
+
const maxLen = content.reduce((max, line) => Math.max(max, line.length), 0);
|
|
28
|
+
const innerWidth = maxLen + padding * 2;
|
|
29
|
+
const top = `+${"-".repeat(innerWidth)}+`;
|
|
30
|
+
const bottom = `+${"-".repeat(innerWidth)}+`;
|
|
31
|
+
const body = content.map((line) => {
|
|
32
|
+
const padded = " ".repeat(padding) + formatLine(line, maxLen) + " ".repeat(padding);
|
|
33
|
+
return `|${padded}|`;
|
|
34
|
+
});
|
|
35
|
+
return [top, ...body, bottom].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function color(text, token) {
|
|
39
|
+
return `${token}${text}${RESET}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function underline(text) {
|
|
43
|
+
return `${UNDERLINE}${text}${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function promptEnter(message) {
|
|
47
|
+
if (!isInteractive()) return;
|
|
48
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
+
await new Promise((resolve) => rl.question(message, () => resolve()));
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
54
|
+
if (!isInteractive()) return options[defaultIndex] || options[0];
|
|
55
|
+
|
|
56
|
+
const safeOptions = Array.isArray(options) ? options : [];
|
|
57
|
+
if (safeOptions.length === 0) return "";
|
|
58
|
+
|
|
59
|
+
const maxIndex = safeOptions.length - 1;
|
|
60
|
+
let currentIndex = Math.min(Math.max(defaultIndex, 0), maxIndex);
|
|
61
|
+
const promptMessage = `${message} (Use Up/Down arrows, Enter)`;
|
|
62
|
+
const linesCount = safeOptions.length + 1;
|
|
63
|
+
|
|
64
|
+
const renderLines = () => {
|
|
65
|
+
const lines = [promptMessage];
|
|
66
|
+
safeOptions.forEach((opt, idx) => {
|
|
67
|
+
const prefix = idx === currentIndex ? ">" : " ";
|
|
68
|
+
lines.push(`${prefix} ${opt}`);
|
|
69
|
+
});
|
|
70
|
+
process.stdout.write(lines.join("\n"));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const rerender = () => {
|
|
74
|
+
for (let i = 0; i < linesCount; i += 1) {
|
|
75
|
+
process.stdout.write("\x1b[2K");
|
|
76
|
+
if (i < linesCount - 1) process.stdout.write("\x1b[1A");
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write("\r");
|
|
79
|
+
renderLines();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
renderLines();
|
|
83
|
+
|
|
84
|
+
return await new Promise((resolve) => {
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
process.stdin.off("keypress", onKeypress);
|
|
87
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
88
|
+
process.stdin.pause();
|
|
89
|
+
process.stdout.write("\n");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onKeypress = (str, key = {}) => {
|
|
93
|
+
if (key.ctrl && key.name === "c") {
|
|
94
|
+
cleanup();
|
|
95
|
+
return resolve(safeOptions[currentIndex]);
|
|
96
|
+
}
|
|
97
|
+
if (key.name === "up" || str === "k") {
|
|
98
|
+
currentIndex = currentIndex === 0 ? maxIndex : currentIndex - 1;
|
|
99
|
+
rerender();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.name === "down" || str === "j") {
|
|
103
|
+
currentIndex = currentIndex === maxIndex ? 0 : currentIndex + 1;
|
|
104
|
+
rerender();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.name === "return") {
|
|
108
|
+
cleanup();
|
|
109
|
+
return resolve(safeOptions[currentIndex]);
|
|
110
|
+
}
|
|
111
|
+
if (str && /^[1-9]$/.test(str)) {
|
|
112
|
+
const idx = Number.parseInt(str, 10) - 1;
|
|
113
|
+
if (idx >= 0 && idx <= maxIndex) {
|
|
114
|
+
currentIndex = idx;
|
|
115
|
+
cleanup();
|
|
116
|
+
return resolve(safeOptions[currentIndex]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
readline.emitKeypressEvents(process.stdin);
|
|
122
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
123
|
+
process.stdin.resume();
|
|
124
|
+
process.stdin.on("keypress", onKeypress);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createSpinner({ text, intervalMs = 80 }) {
|
|
129
|
+
let frame = 0;
|
|
130
|
+
let timer = null;
|
|
131
|
+
|
|
132
|
+
function start() {
|
|
133
|
+
if (!isInteractive()) {
|
|
134
|
+
process.stdout.write(`${text}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
timer = setInterval(() => {
|
|
138
|
+
const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
139
|
+
frame += 1;
|
|
140
|
+
process.stdout.write(`\r${glyph} ${text}`);
|
|
141
|
+
}, intervalMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stop(successText) {
|
|
145
|
+
if (timer) clearInterval(timer);
|
|
146
|
+
if (isInteractive()) {
|
|
147
|
+
process.stdout.write(`\r${" ".repeat(text.length + 4)}\r`);
|
|
148
|
+
}
|
|
149
|
+
if (successText) process.stdout.write(`${successText}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { start, stop };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatSummaryLine({ label, status, detail }) {
|
|
156
|
+
const isSuccess = status === "updated" || status === "set" || status === "installed";
|
|
157
|
+
const bullet = isSuccess ? color("*", GREEN) : "o";
|
|
158
|
+
const statusLabel = isSuccess ? detail || status : detail ? `Skipped - ${detail}` : "Skipped";
|
|
159
|
+
const line = ` ${bullet} ${label.padEnd(22)} [${statusLabel}]`;
|
|
160
|
+
return isSuccess ? line : color(line, DIM);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
BOLD,
|
|
165
|
+
DIM,
|
|
166
|
+
CYAN,
|
|
167
|
+
GREEN,
|
|
168
|
+
YELLOW,
|
|
169
|
+
BLUE,
|
|
170
|
+
RESET,
|
|
171
|
+
color,
|
|
172
|
+
underline,
|
|
173
|
+
renderBox,
|
|
174
|
+
isInteractive,
|
|
175
|
+
promptMenu,
|
|
176
|
+
promptEnter,
|
|
177
|
+
createSpinner,
|
|
178
|
+
formatSummaryLine,
|
|
179
|
+
};
|