vibeusage 0.2.8
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.md +182 -0
- package/README.zh-CN.md +182 -0
- package/bin/tracker.js +28 -0
- package/package.json +46 -0
- package/src/cli.js +64 -0
- package/src/commands/diagnostics.js +39 -0
- package/src/commands/init.js +798 -0
- package/src/commands/status.js +155 -0
- package/src/commands/sync.js +479 -0
- package/src/commands/uninstall.js +153 -0
- package/src/lib/browser-auth.js +175 -0
- package/src/lib/claude-config.js +190 -0
- package/src/lib/cli-ui.js +179 -0
- package/src/lib/codex-config.js +224 -0
- package/src/lib/debug-flags.js +9 -0
- package/src/lib/diagnostics.js +190 -0
- package/src/lib/fs.js +62 -0
- package/src/lib/gemini-config.js +284 -0
- package/src/lib/init-flow.js +48 -0
- package/src/lib/insforge-client.js +75 -0
- package/src/lib/insforge.js +23 -0
- package/src/lib/opencode-config.js +98 -0
- package/src/lib/progress.js +77 -0
- package/src/lib/prompt.js +20 -0
- package/src/lib/rollout.js +1317 -0
- package/src/lib/tracker-paths.js +66 -0
- package/src/lib/upload-throttle.js +129 -0
- package/src/lib/uploader.js +116 -0
- package/src/lib/vibescore-api.js +222 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EVENT = 'SessionEnd';
|
|
8
|
+
const DEFAULT_HOOK_NAME = 'vibeusage-tracker';
|
|
9
|
+
const DEFAULT_MATCHER = 'exit|clear|logout|prompt_input_exit|other';
|
|
10
|
+
|
|
11
|
+
function resolveGeminiConfigDir({ home = os.homedir(), env = process.env } = {}) {
|
|
12
|
+
const explicit = typeof env.GEMINI_HOME === 'string' ? env.GEMINI_HOME.trim() : '';
|
|
13
|
+
if (explicit) return path.resolve(explicit);
|
|
14
|
+
return path.join(home, '.gemini');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveGeminiSettingsPath({ configDir }) {
|
|
18
|
+
return path.join(configDir, 'settings.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function upsertGeminiHook({
|
|
22
|
+
settingsPath,
|
|
23
|
+
hookCommand,
|
|
24
|
+
hookName = DEFAULT_HOOK_NAME,
|
|
25
|
+
matcher = DEFAULT_MATCHER,
|
|
26
|
+
event = DEFAULT_EVENT
|
|
27
|
+
}) {
|
|
28
|
+
const existing = await readJson(settingsPath);
|
|
29
|
+
const settings = normalizeSettings(existing);
|
|
30
|
+
const enableResult = ensureHooksEnabled(settings);
|
|
31
|
+
const baseSettings = enableResult.settings;
|
|
32
|
+
const hooks = normalizeHooks(settings.hooks);
|
|
33
|
+
const entries = normalizeEntries(hooks[event]);
|
|
34
|
+
|
|
35
|
+
const normalized = normalizeEntriesForHook(entries, { hookCommand, hookName });
|
|
36
|
+
let nextEntries = normalized.entries;
|
|
37
|
+
let changed = normalized.changed || enableResult.changed;
|
|
38
|
+
|
|
39
|
+
if (!hasHook(nextEntries, { hookCommand, hookName })) {
|
|
40
|
+
nextEntries = nextEntries.concat([buildHookEntry({ hookCommand, hookName, matcher })]);
|
|
41
|
+
changed = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!changed) return { changed: false, backupPath: null };
|
|
45
|
+
|
|
46
|
+
const nextHooks = { ...hooks, [event]: nextEntries };
|
|
47
|
+
const nextSettings = { ...baseSettings, hooks: nextHooks };
|
|
48
|
+
const backupPath = await writeGeminiSettings({ settingsPath, settings: nextSettings });
|
|
49
|
+
return { changed: true, backupPath };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function removeGeminiHook({
|
|
53
|
+
settingsPath,
|
|
54
|
+
hookCommand,
|
|
55
|
+
hookName = DEFAULT_HOOK_NAME,
|
|
56
|
+
event = DEFAULT_EVENT
|
|
57
|
+
}) {
|
|
58
|
+
const existing = await readJson(settingsPath);
|
|
59
|
+
if (!existing) return { removed: false, skippedReason: 'settings-missing' };
|
|
60
|
+
|
|
61
|
+
const settings = normalizeSettings(existing);
|
|
62
|
+
const hooks = normalizeHooks(settings.hooks);
|
|
63
|
+
const entries = normalizeEntries(hooks[event]);
|
|
64
|
+
if (entries.length === 0) return { removed: false, skippedReason: 'hook-missing' };
|
|
65
|
+
|
|
66
|
+
let removed = false;
|
|
67
|
+
const nextEntries = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const res = stripHookFromEntry(entry, { hookCommand, hookName });
|
|
70
|
+
if (res.removed) removed = true;
|
|
71
|
+
if (res.entry) nextEntries.push(res.entry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!removed) return { removed: false, skippedReason: 'hook-missing' };
|
|
75
|
+
|
|
76
|
+
const nextHooks = { ...hooks };
|
|
77
|
+
if (nextEntries.length > 0) nextHooks[event] = nextEntries;
|
|
78
|
+
else delete nextHooks[event];
|
|
79
|
+
|
|
80
|
+
const nextSettings = { ...settings };
|
|
81
|
+
if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
|
|
82
|
+
else delete nextSettings.hooks;
|
|
83
|
+
|
|
84
|
+
const backupPath = await writeGeminiSettings({ settingsPath, settings: nextSettings });
|
|
85
|
+
return { removed: true, skippedReason: null, backupPath };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function isGeminiHookConfigured({
|
|
89
|
+
settingsPath,
|
|
90
|
+
hookCommand,
|
|
91
|
+
hookName = DEFAULT_HOOK_NAME,
|
|
92
|
+
event = DEFAULT_EVENT
|
|
93
|
+
}) {
|
|
94
|
+
const settings = await readJson(settingsPath);
|
|
95
|
+
if (!settings || typeof settings !== 'object') return false;
|
|
96
|
+
const hooks = settings.hooks;
|
|
97
|
+
if (!hooks || typeof hooks !== 'object') return false;
|
|
98
|
+
const entries = normalizeEntries(hooks[event]);
|
|
99
|
+
return hasHook(entries, { hookCommand, hookName });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildGeminiHookCommand(notifyPath) {
|
|
103
|
+
const cmd = typeof notifyPath === 'string' ? notifyPath : '';
|
|
104
|
+
return `/usr/bin/env node ${quoteArg(cmd)} --source=gemini`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildHookEntry({ hookCommand, hookName, matcher }) {
|
|
108
|
+
const hook = {
|
|
109
|
+
name: hookName,
|
|
110
|
+
type: 'command',
|
|
111
|
+
command: hookCommand
|
|
112
|
+
};
|
|
113
|
+
const entry = { hooks: [hook] };
|
|
114
|
+
if (typeof matcher === 'string' && matcher.length > 0) entry.matcher = matcher;
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeSettings(raw) {
|
|
119
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeHooks(raw) {
|
|
123
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeTools(raw) {
|
|
127
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeEntries(raw) {
|
|
131
|
+
return Array.isArray(raw) ? raw.slice() : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeCommand(cmd) {
|
|
135
|
+
if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join('\u0000');
|
|
136
|
+
if (typeof cmd === 'string') return cmd.trim();
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeName(name) {
|
|
141
|
+
if (typeof name !== 'string') return null;
|
|
142
|
+
const trimmed = name.trim();
|
|
143
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ensureHooksEnabled(settings) {
|
|
147
|
+
const tools = normalizeTools(settings.tools);
|
|
148
|
+
if (tools.enableHooks === true) return { settings, changed: false };
|
|
149
|
+
const nextTools = { ...tools, enableHooks: true };
|
|
150
|
+
return { settings: { ...settings, tools: nextTools }, changed: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hookMatches(hook, { hookCommand, hookName, requireCommand = false }) {
|
|
154
|
+
if (!hook || typeof hook !== 'object') return false;
|
|
155
|
+
const name = normalizeName(hook.name);
|
|
156
|
+
const targetName = normalizeName(hookName);
|
|
157
|
+
const cmd = normalizeCommand(hook.command);
|
|
158
|
+
const targetCmd = normalizeCommand(hookCommand);
|
|
159
|
+
const commandMatches = Boolean(cmd && targetCmd && cmd === targetCmd);
|
|
160
|
+
if (requireCommand) return commandMatches;
|
|
161
|
+
const nameMatches = Boolean(name && targetName && name === targetName);
|
|
162
|
+
return Boolean(commandMatches || nameMatches);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function entryMatches(entry, { hookCommand, hookName, requireCommand = false }) {
|
|
166
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
167
|
+
if (entry.command || entry.name) return hookMatches(entry, { hookCommand, hookName, requireCommand });
|
|
168
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
169
|
+
return entry.hooks.some((hook) => hookMatches(hook, { hookCommand, hookName, requireCommand }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasHook(entries, { hookCommand, hookName }) {
|
|
173
|
+
return entries.some((entry) => entryMatches(entry, { hookCommand, hookName }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeEntriesForHook(entries, { hookCommand, hookName }) {
|
|
177
|
+
let changed = false;
|
|
178
|
+
const nextEntries = entries.map((entry) => {
|
|
179
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
180
|
+
|
|
181
|
+
if (entry.command || entry.name) {
|
|
182
|
+
if (hookMatches(entry, { hookCommand, hookName })) {
|
|
183
|
+
const next = normalizeHookObject(entry, { hookCommand, hookName });
|
|
184
|
+
if (next !== entry) changed = true;
|
|
185
|
+
return next;
|
|
186
|
+
}
|
|
187
|
+
return entry;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : null;
|
|
191
|
+
if (!hooks) return entry;
|
|
192
|
+
|
|
193
|
+
let hooksChanged = false;
|
|
194
|
+
const nextHooks = hooks.map((hook) => {
|
|
195
|
+
if (hookMatches(hook, { hookCommand, hookName })) {
|
|
196
|
+
const next = normalizeHookObject(hook, { hookCommand, hookName });
|
|
197
|
+
if (next !== hook) hooksChanged = true;
|
|
198
|
+
return next;
|
|
199
|
+
}
|
|
200
|
+
return hook;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!hooksChanged) return entry;
|
|
204
|
+
changed = true;
|
|
205
|
+
return { ...entry, hooks: nextHooks };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { entries: nextEntries, changed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeHookObject(hook, { hookCommand, hookName }) {
|
|
212
|
+
const next = { ...hook };
|
|
213
|
+
let changed = false;
|
|
214
|
+
|
|
215
|
+
if (next.type !== 'command') {
|
|
216
|
+
next.type = 'command';
|
|
217
|
+
changed = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (hookCommand && next.command !== hookCommand) {
|
|
221
|
+
next.command = hookCommand;
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (hookName && next.name !== hookName) {
|
|
226
|
+
next.name = hookName;
|
|
227
|
+
changed = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return changed ? next : hook;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stripHookFromEntry(entry, { hookCommand, hookName }) {
|
|
234
|
+
if (!entry || typeof entry !== 'object') return { entry, removed: false };
|
|
235
|
+
|
|
236
|
+
if (entry.command || entry.name) {
|
|
237
|
+
if (hookMatches(entry, { hookCommand, hookName, requireCommand: true })) return { entry: null, removed: true };
|
|
238
|
+
return { entry, removed: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : null;
|
|
242
|
+
if (!hooks) return { entry, removed: false };
|
|
243
|
+
|
|
244
|
+
const nextHooks = hooks.filter((hook) => !hookMatches(hook, { hookCommand, hookName, requireCommand: true }));
|
|
245
|
+
if (nextHooks.length === hooks.length) return { entry, removed: false };
|
|
246
|
+
if (nextHooks.length === 0) return { entry: null, removed: true };
|
|
247
|
+
|
|
248
|
+
return { entry: { ...entry, hooks: nextHooks }, removed: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function quoteArg(value) {
|
|
252
|
+
const v = typeof value === 'string' ? value : '';
|
|
253
|
+
if (!v) return '""';
|
|
254
|
+
if (/^[A-Za-z0-9_\-./:@]+$/.test(v)) return v;
|
|
255
|
+
return `"${v.replace(/"/g, '\\"')}"`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function writeGeminiSettings({ settingsPath, settings }) {
|
|
259
|
+
await ensureDir(path.dirname(settingsPath));
|
|
260
|
+
let backupPath = null;
|
|
261
|
+
try {
|
|
262
|
+
const st = await fs.stat(settingsPath);
|
|
263
|
+
if (st && st.isFile()) {
|
|
264
|
+
backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
265
|
+
await fs.copyFile(settingsPath, backupPath);
|
|
266
|
+
}
|
|
267
|
+
} catch (_e) {
|
|
268
|
+
// Ignore missing file.
|
|
269
|
+
}
|
|
270
|
+
await writeJson(settingsPath, settings);
|
|
271
|
+
return backupPath;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
DEFAULT_EVENT,
|
|
276
|
+
DEFAULT_HOOK_NAME,
|
|
277
|
+
DEFAULT_MATCHER,
|
|
278
|
+
resolveGeminiConfigDir,
|
|
279
|
+
resolveGeminiSettingsPath,
|
|
280
|
+
buildGeminiHookCommand,
|
|
281
|
+
upsertGeminiHook,
|
|
282
|
+
removeGeminiHook,
|
|
283
|
+
isGeminiHookConfigured
|
|
284
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatSummaryLine, renderBox, underline } = require('./cli-ui');
|
|
4
|
+
|
|
5
|
+
const DIVIDER = '----------------------------------------------';
|
|
6
|
+
|
|
7
|
+
function renderLocalReport({ summary, isDryRun }) {
|
|
8
|
+
const header = isDryRun
|
|
9
|
+
? 'Dry run complete. Preview only; no changes were applied.'
|
|
10
|
+
: 'Local configuration complete.';
|
|
11
|
+
const lines = [header, '', 'Integration Status:'];
|
|
12
|
+
for (const item of summary || []) lines.push(formatSummaryLine(item));
|
|
13
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderAuthTransition({ authUrl, canAutoOpen }) {
|
|
17
|
+
const lines = ['', DIVIDER, '', 'Next: Registering device...'];
|
|
18
|
+
if (canAutoOpen) {
|
|
19
|
+
lines.push('Opening your browser to link account...');
|
|
20
|
+
if (authUrl) lines.push(`If it does not open, visit: ${underline(authUrl)}`);
|
|
21
|
+
} else {
|
|
22
|
+
lines.push('Open the link below to sign in.');
|
|
23
|
+
if (authUrl) lines.push(`Visit: ${underline(authUrl)}`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderSuccessBox({ configPath, dashboardUrl }) {
|
|
30
|
+
const identityLine = 'Account linked.';
|
|
31
|
+
const lines = [
|
|
32
|
+
'You are all set!',
|
|
33
|
+
'',
|
|
34
|
+
identityLine,
|
|
35
|
+
`Token saved to: ${configPath}`,
|
|
36
|
+
''
|
|
37
|
+
];
|
|
38
|
+
if (dashboardUrl) lines.push(`View your stats at: ${dashboardUrl}`);
|
|
39
|
+
lines.push('You can close this terminal window.');
|
|
40
|
+
process.stdout.write(`${renderBox(lines)}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
DIVIDER,
|
|
45
|
+
renderLocalReport,
|
|
46
|
+
renderAuthTransition,
|
|
47
|
+
renderSuccessBox
|
|
48
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createClient } = require('@insforge/sdk');
|
|
4
|
+
|
|
5
|
+
function getAnonKey({ env = process.env } = {}) {
|
|
6
|
+
return (
|
|
7
|
+
env.VIBEUSAGE_INSFORGE_ANON_KEY ||
|
|
8
|
+
env.VIBESCORE_INSFORGE_ANON_KEY ||
|
|
9
|
+
env.INSFORGE_ANON_KEY ||
|
|
10
|
+
''
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getHttpTimeoutMs({ env = process.env } = {}) {
|
|
15
|
+
const raw = readEnvValue(env, ['VIBEUSAGE_HTTP_TIMEOUT_MS', 'VIBESCORE_HTTP_TIMEOUT_MS']);
|
|
16
|
+
if (raw == null || raw === '') return 20_000;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
if (!Number.isFinite(n)) return 20_000;
|
|
19
|
+
if (n <= 0) return 0;
|
|
20
|
+
return clampInt(n, 1000, 120_000);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createTimeoutFetch(baseFetch) {
|
|
24
|
+
if (!baseFetch) return baseFetch;
|
|
25
|
+
return async (input, init = {}) => {
|
|
26
|
+
const timeoutMs = getHttpTimeoutMs();
|
|
27
|
+
if (!timeoutMs) return baseFetch(input, init);
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
30
|
+
try {
|
|
31
|
+
return await baseFetch(input, { ...init, signal: controller.signal });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (controller.signal.aborted) {
|
|
34
|
+
const timeoutErr = new Error(`Request timeout after ${timeoutMs}ms`);
|
|
35
|
+
timeoutErr.cause = err;
|
|
36
|
+
throw timeoutErr;
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createInsforgeClient({ baseUrl, accessToken } = {}) {
|
|
46
|
+
if (!baseUrl) throw new Error('Missing baseUrl');
|
|
47
|
+
const anonKey = getAnonKey();
|
|
48
|
+
return createClient({
|
|
49
|
+
baseUrl,
|
|
50
|
+
anonKey: anonKey || undefined,
|
|
51
|
+
edgeFunctionToken: accessToken || undefined,
|
|
52
|
+
fetch: createTimeoutFetch(globalThis.fetch)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clampInt(value, min, max) {
|
|
57
|
+
const n = Number(value);
|
|
58
|
+
if (!Number.isFinite(n)) return min;
|
|
59
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readEnvValue(env, keys) {
|
|
63
|
+
if (!env || !Array.isArray(keys)) return undefined;
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const value = env?.[key];
|
|
66
|
+
if (value != null && value !== '') return value;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
createInsforgeClient,
|
|
73
|
+
getAnonKey,
|
|
74
|
+
getHttpTimeoutMs
|
|
75
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { exchangeLinkCode, issueDeviceToken, signInWithPassword } = require('./vibescore-api');
|
|
2
|
+
|
|
3
|
+
async function issueDeviceTokenWithPassword({ baseUrl, email, password, deviceName }) {
|
|
4
|
+
const accessToken = await signInWithPassword({ baseUrl, email, password });
|
|
5
|
+
const issued = await issueDeviceToken({ baseUrl, accessToken, deviceName });
|
|
6
|
+
return issued;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function issueDeviceTokenWithAccessToken({ baseUrl, accessToken, deviceName }) {
|
|
10
|
+
const issued = await issueDeviceToken({ baseUrl, accessToken, deviceName });
|
|
11
|
+
return issued;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function issueDeviceTokenWithLinkCode({ baseUrl, linkCode, requestId, deviceName, platform }) {
|
|
15
|
+
const issued = await exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, platform });
|
|
16
|
+
return { token: issued.token, deviceId: issued.deviceId };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
issueDeviceTokenWithPassword,
|
|
21
|
+
issueDeviceTokenWithAccessToken,
|
|
22
|
+
issueDeviceTokenWithLinkCode
|
|
23
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const { ensureDir } = require('./fs');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PLUGIN_NAME = 'vibeusage-tracker.js';
|
|
8
|
+
const PLUGIN_MARKER = 'VIBEUSAGE_TRACKER_PLUGIN';
|
|
9
|
+
const DEFAULT_EVENT = 'session.idle';
|
|
10
|
+
|
|
11
|
+
function resolveOpencodeConfigDir({ home = os.homedir(), env = process.env } = {}) {
|
|
12
|
+
const explicit = typeof env.OPENCODE_CONFIG_DIR === 'string' ? env.OPENCODE_CONFIG_DIR.trim() : '';
|
|
13
|
+
if (explicit) return path.resolve(explicit);
|
|
14
|
+
const xdg = typeof env.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
|
|
15
|
+
const base = xdg || path.join(home, '.config');
|
|
16
|
+
return path.join(base, 'opencode');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveOpencodePluginDir({ configDir }) {
|
|
20
|
+
return path.join(configDir, 'plugin');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildOpencodePlugin({ notifyPath }) {
|
|
24
|
+
const safeNotifyPath = typeof notifyPath === 'string' ? notifyPath : '';
|
|
25
|
+
return `// ${PLUGIN_MARKER}\n` +
|
|
26
|
+
`const notifyPath = ${JSON.stringify(safeNotifyPath)};\n` +
|
|
27
|
+
`export const VibeUsagePlugin = async ({ $ }) => {\n` +
|
|
28
|
+
` return {\n` +
|
|
29
|
+
` event: async ({ event }) => {\n` +
|
|
30
|
+
` if (!event || event.type !== ${JSON.stringify(DEFAULT_EVENT)}) return;\n` +
|
|
31
|
+
` try {\n` +
|
|
32
|
+
` if (!notifyPath) return;\n` +
|
|
33
|
+
` const proc = $\`/usr/bin/env node ${'${notifyPath}'} --source=opencode\`;\n` +
|
|
34
|
+
` if (proc && typeof proc.catch === 'function') proc.catch(() => {});\n` +
|
|
35
|
+
` } catch (_) {}\n` +
|
|
36
|
+
` }\n` +
|
|
37
|
+
` };\n` +
|
|
38
|
+
`};\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function upsertOpencodePlugin({
|
|
42
|
+
configDir,
|
|
43
|
+
notifyPath,
|
|
44
|
+
pluginName = DEFAULT_PLUGIN_NAME
|
|
45
|
+
}) {
|
|
46
|
+
if (!configDir) return { changed: false, pluginPath: null, skippedReason: 'config-missing' };
|
|
47
|
+
const pluginDir = resolveOpencodePluginDir({ configDir });
|
|
48
|
+
const pluginPath = path.join(pluginDir, pluginName);
|
|
49
|
+
const next = buildOpencodePlugin({ notifyPath });
|
|
50
|
+
const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
|
|
51
|
+
|
|
52
|
+
if (existing === next) {
|
|
53
|
+
return { changed: false, pluginPath, skippedReason: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await ensureDir(pluginDir);
|
|
57
|
+
|
|
58
|
+
let backupPath = null;
|
|
59
|
+
if (existing != null) {
|
|
60
|
+
backupPath = `${pluginPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
61
|
+
await fs.copyFile(pluginPath, backupPath).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await fs.writeFile(pluginPath, next, 'utf8');
|
|
65
|
+
return { changed: true, pluginPath, backupPath, skippedReason: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function removeOpencodePlugin({ configDir, pluginName = DEFAULT_PLUGIN_NAME }) {
|
|
69
|
+
if (!configDir) return { removed: false, skippedReason: 'config-missing' };
|
|
70
|
+
const pluginPath = path.join(resolveOpencodePluginDir({ configDir }), pluginName);
|
|
71
|
+
const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
|
|
72
|
+
if (existing == null) return { removed: false, skippedReason: 'plugin-missing' };
|
|
73
|
+
if (!hasPluginMarker(existing)) return { removed: false, skippedReason: 'unexpected-content' };
|
|
74
|
+
await fs.unlink(pluginPath).catch(() => {});
|
|
75
|
+
return { removed: true, skippedReason: null };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function isOpencodePluginInstalled({ configDir, pluginName = DEFAULT_PLUGIN_NAME }) {
|
|
79
|
+
if (!configDir) return false;
|
|
80
|
+
const pluginPath = path.join(resolveOpencodePluginDir({ configDir }), pluginName);
|
|
81
|
+
const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
|
|
82
|
+
if (!existing) return false;
|
|
83
|
+
return hasPluginMarker(existing);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasPluginMarker(text) {
|
|
87
|
+
return typeof text === 'string' && text.includes(PLUGIN_MARKER);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
DEFAULT_PLUGIN_NAME,
|
|
92
|
+
resolveOpencodeConfigDir,
|
|
93
|
+
resolveOpencodePluginDir,
|
|
94
|
+
buildOpencodePlugin,
|
|
95
|
+
upsertOpencodePlugin,
|
|
96
|
+
removeOpencodePlugin,
|
|
97
|
+
isOpencodePluginInstalled
|
|
98
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function createProgress({ stream } = {}) {
|
|
2
|
+
const out = stream || process.stdout;
|
|
3
|
+
const enabled = Boolean(out && out.isTTY);
|
|
4
|
+
const frames = ['|', '/', '-', '\\'];
|
|
5
|
+
const intervalMs = 90;
|
|
6
|
+
|
|
7
|
+
let timer = null;
|
|
8
|
+
let text = '';
|
|
9
|
+
let frame = 0;
|
|
10
|
+
let lastLen = 0;
|
|
11
|
+
|
|
12
|
+
function render() {
|
|
13
|
+
if (!enabled) return;
|
|
14
|
+
const line = `${frames[frame++ % frames.length]} ${text}`;
|
|
15
|
+
const pad = lastLen > line.length ? ' '.repeat(lastLen - line.length) : '';
|
|
16
|
+
lastLen = line.length;
|
|
17
|
+
out.write(`\r${line}${pad}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function start(initialText) {
|
|
21
|
+
if (!enabled) return;
|
|
22
|
+
text = initialText || '';
|
|
23
|
+
if (timer) clearInterval(timer);
|
|
24
|
+
timer = setInterval(render, intervalMs);
|
|
25
|
+
render();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function update(nextText) {
|
|
29
|
+
text = nextText || '';
|
|
30
|
+
render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stop() {
|
|
34
|
+
if (!enabled) return;
|
|
35
|
+
if (timer) clearInterval(timer);
|
|
36
|
+
timer = null;
|
|
37
|
+
out.write(`\r${' '.repeat(lastLen)}\r`);
|
|
38
|
+
lastLen = 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { enabled, start, update, stop };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderBar(progress, width = 20) {
|
|
45
|
+
const p = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 0;
|
|
46
|
+
const filled = Math.round(p * width);
|
|
47
|
+
const empty = Math.max(0, width - filled);
|
|
48
|
+
return `[${'='.repeat(filled)}${'-'.repeat(empty)}] ${Math.round(p * 100)}%`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatNumber(n) {
|
|
52
|
+
const v = Number(n);
|
|
53
|
+
if (!Number.isFinite(v)) return '0';
|
|
54
|
+
return Math.trunc(v).toLocaleString('en-US');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatBytes(bytes) {
|
|
58
|
+
const n = Number(bytes);
|
|
59
|
+
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
60
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
61
|
+
let v = n;
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
64
|
+
v /= 1024;
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
const fixed = i === 0 ? String(Math.trunc(v)) : v.toFixed(v >= 10 ? 1 : 2);
|
|
68
|
+
return `${fixed} ${units[i]}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
createProgress,
|
|
73
|
+
renderBar,
|
|
74
|
+
formatNumber,
|
|
75
|
+
formatBytes
|
|
76
|
+
};
|
|
77
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const readline = require('node:readline');
|
|
2
|
+
|
|
3
|
+
async function prompt(label) {
|
|
4
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
const value = await new Promise((resolve) => rl.question(label, resolve));
|
|
6
|
+
rl.close();
|
|
7
|
+
return String(value || '').trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function promptHidden(label) {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
12
|
+
const value = await new Promise((resolve) => {
|
|
13
|
+
rl._writeToOutput = function _writeToOutput() {};
|
|
14
|
+
rl.question(label, (answer) => resolve(answer));
|
|
15
|
+
});
|
|
16
|
+
rl.close();
|
|
17
|
+
return String(value || '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { prompt, promptHidden };
|