tokenwait 0.1.2 → 0.1.4
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/bin/tokenwait.js +118 -4
- package/lib/agents.js +21 -2
- package/lib/api.js +31 -3
- package/lib/ide.js +309 -0
- package/lib/worker.js +5 -2
- package/package.json +1 -1
package/bin/tokenwait.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// TokenWait CLI.
|
|
4
|
-
// npx tokenwait install [--agents claude,codex,gemini] [--server URL] [--yes] [--no-login]
|
|
4
|
+
// npx tokenwait install [--agents claude,codex,gemini] [--server URL] [--yes] [--no-login] [--ide all|none]
|
|
5
|
+
// npx tokenwait ide [--list] [--yes] дополнение TokenWait для VS Code / Cursor / JetBrains IDE: поставить или обновить
|
|
6
|
+
// npx tokenwait update обновить хуки агентов и уже поставленные дополнения IDE
|
|
5
7
|
// tokenwait uninstall [--purge]
|
|
6
8
|
// tokenwait login | logout | status [--json]
|
|
7
9
|
// tokenwait link-start --json | me --json | sync (для расширений IDE)
|
|
@@ -14,6 +16,7 @@ const { spawn } = require('child_process');
|
|
|
14
16
|
const util = require('../lib/util');
|
|
15
17
|
const agents = require('../lib/agents');
|
|
16
18
|
const api = require('../lib/api');
|
|
19
|
+
const ide = require('../lib/ide');
|
|
17
20
|
const worker = require('../lib/worker');
|
|
18
21
|
const { pick } = require('../lib/rotation');
|
|
19
22
|
|
|
@@ -81,7 +84,8 @@ What it changes:
|
|
|
81
84
|
• stores ads, counters and an install key in ~/.tokenwait
|
|
82
85
|
What it never does:
|
|
83
86
|
• read your code, prompts, responses or transcripts
|
|
84
|
-
• send anything except ad ids, impression counts, waiting time
|
|
87
|
+
• send anything except ad ids, impression counts, waiting time, the install key,
|
|
88
|
+
the package version and which agents and IDE add-ons are connected
|
|
85
89
|
Undo at any time: npx tokenwait uninstall
|
|
86
90
|
`;
|
|
87
91
|
|
|
@@ -91,6 +95,7 @@ async function cmdInstall() {
|
|
|
91
95
|
if (!flag('yes')) {
|
|
92
96
|
console.log(CONSENT);
|
|
93
97
|
console.log('Agents: ' + ids.map((id) => agents.AGENTS[id].name).join(', ') + '\n');
|
|
98
|
+
if (ids.includes('codex')) console.log('⚠ ' + agents.codexNote() + '\n');
|
|
94
99
|
const a = await ask('Install TokenWait? [y/N] ');
|
|
95
100
|
if (!/^y(es)?$/i.test(a)) return console.log('Cancelled. Nothing was changed.');
|
|
96
101
|
}
|
|
@@ -108,7 +113,7 @@ async function cmdInstall() {
|
|
|
108
113
|
const r = agents.install(id, node, app);
|
|
109
114
|
results.push({ agent: id, ok: true, file: r.file });
|
|
110
115
|
if (!json) console.log(`✓ ${agents.AGENTS[id].name}: ${r.file}`);
|
|
111
|
-
if (!json &&
|
|
116
|
+
if (!json && id === 'codex') console.log(' ⚠ ' + agents.codexNote());
|
|
112
117
|
} catch (e) {
|
|
113
118
|
results.push({ agent: id, ok: false, error: e.message });
|
|
114
119
|
if (!json) console.log(`✗ ${agents.AGENTS[id].name}: ${e.message}`);
|
|
@@ -119,14 +124,119 @@ async function cmdInstall() {
|
|
|
119
124
|
} catch (e) {
|
|
120
125
|
if (!json) console.log('Could not reach the server yet (' + e.message + '). Ads will load automatically later.');
|
|
121
126
|
}
|
|
127
|
+
// дополнения для IDE: спрашиваем в обычной установке; при --yes (так зовут установщик сами плагины) — только с --ide all
|
|
128
|
+
let ideResults = [];
|
|
129
|
+
const ideOpt = opt('ide');
|
|
130
|
+
if (ideOpt !== 'none' && !json && (!flag('yes') || ideOpt === 'all')) ideResults = await offerIde(!flag('yes'));
|
|
122
131
|
if (json) return out({ ok: results.every((r) => r.ok), agents: results });
|
|
123
132
|
console.log('\nRestart your agent sessions to pick up the changes.');
|
|
133
|
+
if (ideResults.some((r) => r.ok && r.target.kind === 'jetbrains' && (r.action === 'install' || r.action === 'update')))
|
|
134
|
+
console.log('Restart your JetBrains IDE to load the TokenWait plugin.');
|
|
124
135
|
if (!flag('no-login') && api.credentials()) {
|
|
125
136
|
const a = await ask('Sign in now (GitHub, Google or Apple) so earnings go to your account? [Y/n] ');
|
|
126
137
|
if (!/^n/i.test(a)) await cmdLogin();
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
// ---------- дополнения для IDE ----------
|
|
142
|
+
|
|
143
|
+
const ACTION_TEXT = { install: 'installed', update: 'updated', current: 'up to date', 'too-old': 'IDE version too old', skipped: 'not installed' };
|
|
144
|
+
|
|
145
|
+
function describeTargets(targets) {
|
|
146
|
+
return targets.map((t) => `${t.name}${t.installed ? ` (TokenWait ${t.installed})` : ''}`).join(', ');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printIdeResults(results, versions) {
|
|
150
|
+
for (const r of results) {
|
|
151
|
+
const want = r.target.kind === 'jetbrains' ? versions.jetbrains : versions.vscode;
|
|
152
|
+
if (r.ok) console.log(`${r.action === 'current' || r.action === 'skipped' || r.action === 'too-old' ? '·' : '✓'} ${r.target.name}: ${ACTION_TEXT[r.action] || r.action}${r.action === 'install' || r.action === 'update' ? ' ' + want : ''}`);
|
|
153
|
+
else console.log(`✗ ${r.target.name}: ${r.error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** В обычной установке: найти IDE и предложить поставить туда дополнение. */
|
|
158
|
+
async function offerIde(interactive) {
|
|
159
|
+
const targets = ide.detectAll();
|
|
160
|
+
if (!targets.length) return [];
|
|
161
|
+
let versions;
|
|
162
|
+
try {
|
|
163
|
+
versions = await ide.latest(api.server());
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.log('\nIDE add-ons: could not reach the server (' + e.message + '). Later: npx tokenwait ide');
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const todo = targets.filter((t) => ['install', 'update'].includes(ide.plan(t, versions)));
|
|
169
|
+
if (!todo.length) return [];
|
|
170
|
+
console.log('\nFound: ' + describeTargets(todo));
|
|
171
|
+
console.log('The TokenWait add-on shows the sponsored line in the IDE status bar (for Codex and Gemini too) and lets you sign in from the IDE.');
|
|
172
|
+
if (interactive) {
|
|
173
|
+
const a = await ask('Install or update the TokenWait add-on there? [Y/n] ');
|
|
174
|
+
if (/^n/i.test(a)) {
|
|
175
|
+
console.log('Skipped. Later: npx tokenwait ide');
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const res = await ide.apply(api.server(), todo, versions);
|
|
180
|
+
printIdeResults(res, versions);
|
|
181
|
+
return res;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function cmdIde() {
|
|
185
|
+
const targets = ide.detectAll();
|
|
186
|
+
const versions = await ide.latest(api.server());
|
|
187
|
+
if (flag('list') || !targets.length) {
|
|
188
|
+
if (json) return out({ ok: true, latest: versions, ides: targets.map((t) => ({ kind: t.kind, id: t.id, name: t.name, installed: t.installed, action: ide.plan(t, versions) })) });
|
|
189
|
+
if (!targets.length) return console.log('No supported IDE found (VS Code, Cursor, Windsurf, VSCodium, JetBrains IDEs).');
|
|
190
|
+
console.log(`Latest: VS Code add-on ${versions.vscode}, JetBrains plugin ${versions.jetbrains}`);
|
|
191
|
+
const LIST_TEXT = { install: 'can be installed', update: 'update available', current: 'up to date', 'too-old': 'IDE version too old' };
|
|
192
|
+
for (const t of targets) console.log(` ${t.name.padEnd(26)} ${(t.installed ? 'TokenWait ' + t.installed : 'not installed').padEnd(18)} ${LIST_TEXT[ide.plan(t, versions)]}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const todo = targets.filter((t) => ['install', 'update'].includes(ide.plan(t, versions)));
|
|
196
|
+
if (!todo.length) {
|
|
197
|
+
if (json) return out({ ok: true, results: [] });
|
|
198
|
+
return console.log('TokenWait add-ons are up to date in: ' + targets.map((t) => t.name).join(', '));
|
|
199
|
+
}
|
|
200
|
+
if (!flag('yes') && !json) {
|
|
201
|
+
console.log('Will install or update the TokenWait add-on in: ' + describeTargets(todo));
|
|
202
|
+
console.log(`Files come from ${api.server()}/downloads.`);
|
|
203
|
+
const a = await ask('Continue? [Y/n] ');
|
|
204
|
+
if (/^n/i.test(a)) return console.log('Cancelled. Nothing was changed.');
|
|
205
|
+
}
|
|
206
|
+
const res = await ide.apply(api.server(), todo, versions);
|
|
207
|
+
if (json) return out({ ok: res.every((r) => r.ok), results: res.map((r) => ({ name: r.target.name, action: r.action, ok: r.ok, error: r.error })) });
|
|
208
|
+
printIdeResults(res, versions);
|
|
209
|
+
if (res.some((r) => r.ok && r.target.kind === 'jetbrains')) console.log('Restart your JetBrains IDE to load the new plugin.');
|
|
210
|
+
if (res.some((r) => r.ok && r.target.kind === 'vscode')) console.log('Reload VS Code windows (Developer: Reload Window) to start the new version.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Обновить: копию пакета, хуки уже подключённых агентов и уже поставленные дополнения IDE. */
|
|
214
|
+
async function cmdUpdate() {
|
|
215
|
+
const ids = Object.keys(agents.AGENTS).filter((id) => agents.isInstalled(id));
|
|
216
|
+
if (!ids.length && !json) console.log('TokenWait is not installed for any agent yet. Run: npx tokenwait install');
|
|
217
|
+
const app = copyApp();
|
|
218
|
+
for (const id of ids) {
|
|
219
|
+
try {
|
|
220
|
+
agents.install(id, process.execPath, app);
|
|
221
|
+
if (!json) console.log(`✓ ${agents.AGENTS[id].name}: hooks updated (TokenWait ${VERSION})`);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
if (!json) console.log(`✗ ${agents.AGENTS[id].name}: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let res = [];
|
|
227
|
+
let versions = null;
|
|
228
|
+
try {
|
|
229
|
+
versions = await ide.latest(api.server());
|
|
230
|
+
res = await ide.apply(api.server(), ide.detectAll(), versions, { onlyInstalled: true });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (!json) console.log('IDE add-ons: ' + e.message);
|
|
233
|
+
}
|
|
234
|
+
if (json) return out({ ok: res.every((r) => r.ok), version: VERSION, latest: versions, ide: res.map((r) => ({ name: r.target.name, action: r.action, ok: r.ok, error: r.error })) });
|
|
235
|
+
if (versions) printIdeResults(res.filter((r) => r.action !== 'skipped'), versions);
|
|
236
|
+
if (versions && ide.cmpVersion(VERSION, versions.core) < 0) console.log(`\nA newer package is available (${versions.core}). Run: npx tokenwait@latest update`);
|
|
237
|
+
if (res.some((r) => r.ok && r.target.kind === 'jetbrains' && r.action === 'update')) console.log('Restart your JetBrains IDE to load the new plugin.');
|
|
238
|
+
}
|
|
239
|
+
|
|
130
240
|
async function cmdUninstall() {
|
|
131
241
|
const res = [];
|
|
132
242
|
for (const id of Object.keys(agents.AGENTS)) {
|
|
@@ -211,10 +321,12 @@ function help() {
|
|
|
211
321
|
npx tokenwait install set up Claude Code / Codex CLI / Gemini CLI
|
|
212
322
|
npx tokenwait login link this machine to your account (GitHub, Google or Apple)
|
|
213
323
|
npx tokenwait status what is installed, impressions and earnings
|
|
324
|
+
npx tokenwait ide install or update the add-on for VS Code, Cursor and JetBrains IDEs
|
|
325
|
+
npx tokenwait update update agent hooks and installed IDE add-ons
|
|
214
326
|
npx tokenwait logout unlink this machine
|
|
215
327
|
npx tokenwait uninstall remove everything from agent configs (--purge also deletes ~/.tokenwait)
|
|
216
328
|
|
|
217
|
-
Options: --agents claude,codex,gemini --server URL --yes --json`);
|
|
329
|
+
Options: --agents claude,codex,gemini --server URL --yes --json --ide all|none --list`);
|
|
218
330
|
}
|
|
219
331
|
|
|
220
332
|
const commands = {
|
|
@@ -223,6 +335,8 @@ const commands = {
|
|
|
223
335
|
login: cmdLogin,
|
|
224
336
|
logout: cmdLogout,
|
|
225
337
|
status: cmdStatus,
|
|
338
|
+
ide: cmdIde,
|
|
339
|
+
update: cmdUpdate,
|
|
226
340
|
'link-start': async () => out(await cmdLinkStart()),
|
|
227
341
|
me: async () => out(await api.me()),
|
|
228
342
|
sync: async () => {
|
package/lib/agents.js
CHANGED
|
@@ -33,7 +33,7 @@ const AGENTS = {
|
|
|
33
33
|
file: () => path.join(os.homedir(), '.codex', 'hooks.json'),
|
|
34
34
|
events: { UserPromptSubmit: 'start', Stop: 'stop' },
|
|
35
35
|
statusLine: false,
|
|
36
|
-
note: '
|
|
36
|
+
note: 'codex', // текст — codexNote(): Codex один раз спрашивает разрешение на хуки
|
|
37
37
|
},
|
|
38
38
|
gemini: {
|
|
39
39
|
name: 'Gemini CLI',
|
|
@@ -191,4 +191,23 @@ function restoreLegacySpinner(s) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
// Codex запускает новые хуки только после одобрения пользователем — предупреждаем заранее, чтобы окно не было неожиданным.
|
|
195
|
+
const CODEX_NOTE = {
|
|
196
|
+
en: 'Codex will ask once to approve the 2 TokenWait hooks (start and end of a response). Please allow them: in the terminal run /hooks; in the Codex panel of your IDE a hook review window opens: choose “Allow all”. Without this TokenWait cannot see when Codex is thinking, so no ads are shown and nothing is earned for Codex.',
|
|
197
|
+
ru: 'Codex один раз попросит разрешить 2 хука TokenWait (начало и конец ответа). Разрешите их: в терминале — командой /hooks, в панели Codex в IDE откроется окно «Проверка хуков» — нажмите «Разрешить все». Без этого TokenWait не видит, когда Codex думает: реклама для Codex не показывается и не засчитывается.',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Язык системы для сообщений в терминале. */
|
|
201
|
+
function sysLang() {
|
|
202
|
+
const v = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
|
|
203
|
+
if (v) return /^ru/i.test(v) ? 'ru' : 'en';
|
|
204
|
+
try {
|
|
205
|
+
return /^ru/i.test(Intl.DateTimeFormat().resolvedOptions().locale) ? 'ru' : 'en';
|
|
206
|
+
} catch (_) {
|
|
207
|
+
return 'en';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const codexNote = (lang = sysLang()) => CODEX_NOTE[lang === 'ru' ? 'ru' : 'en'];
|
|
212
|
+
|
|
213
|
+
module.exports = { codexNote, sysLang, AGENTS, detected, install, uninstall, isInstalled, parseStatusLine, HOOK_MARK, _p: p };
|
package/lib/api.js
CHANGED
|
@@ -24,8 +24,34 @@ function credentials() {
|
|
|
24
24
|
return c && c.server === server() && c.client_id && c.client_secret ? c : null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Что стоит на этой установке — для статистики оператора (без данных о человеке):
|
|
29
|
+
* подключённые агенты и дополнения IDE, которые выходили на связь за последние 7 дней (~/.tokenwait/ide.json).
|
|
30
|
+
*/
|
|
31
|
+
function installInfo() {
|
|
32
|
+
const out = {};
|
|
33
|
+
try {
|
|
34
|
+
const agents = require('./agents');
|
|
35
|
+
out['x-tokenwait-agents'] = Object.keys(agents.AGENTS).filter((id) => agents.isInstalled(id)).join(',');
|
|
36
|
+
} catch (_) {}
|
|
37
|
+
const ide = readJson(p('ide.json'), {});
|
|
38
|
+
const fresh = Object.entries(ide && typeof ide === 'object' ? ide : {})
|
|
39
|
+
.filter(([k, v]) => /^(jetbrains|vscode)$/.test(k) && v && Date.now() - Number(v.t) < 7 * 86400_000 && /^\d+\.\d+\.\d+$/.test(String(v.v)))
|
|
40
|
+
.map(([k, v]) => k + '/' + v.v);
|
|
41
|
+
out['x-tokenwait-ide'] = fresh.join(',');
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Отметка дополнения IDE: вызывают JetBrains и VS Code при запуске и раз в час. */
|
|
46
|
+
function markIde(kind, version) {
|
|
47
|
+
if (!/^(jetbrains|vscode)$/.test(kind)) return;
|
|
48
|
+
const ide = readJson(p('ide.json'), {});
|
|
49
|
+
ide[kind] = { v: String(version), t: Date.now() };
|
|
50
|
+
writeJson(p('ide.json'), ide);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function request(method, path, body, auth = true, timeoutMs = 5000, extraHeaders = {}) {
|
|
54
|
+
const headers = { accept: 'application/json', 'user-agent': 'tokenwait/' + require('../package.json').version, ...extraHeaders };
|
|
29
55
|
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
30
56
|
if (auth) {
|
|
31
57
|
const c = credentials() || (await register());
|
|
@@ -55,7 +81,7 @@ async function register() {
|
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
async function fetchAds() {
|
|
58
|
-
const r = await request('GET', '/v1/ads');
|
|
84
|
+
const r = await request('GET', '/v1/ads', undefined, true, 5000, installInfo());
|
|
59
85
|
const data = {
|
|
60
86
|
fetched_at: Date.now(),
|
|
61
87
|
ttl_seconds: Math.min(Math.max(Number(r.ttl_seconds) || 600, 60), 3600),
|
|
@@ -76,6 +102,8 @@ async function me() {
|
|
|
76
102
|
}
|
|
77
103
|
|
|
78
104
|
module.exports = {
|
|
105
|
+
installInfo,
|
|
106
|
+
markIde,
|
|
79
107
|
HttpError,
|
|
80
108
|
server,
|
|
81
109
|
config,
|
package/lib/ide.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Дополнения TokenWait для IDE: найти установленные IDE и поставить или обновить в них дополнение.
|
|
3
|
+
//
|
|
4
|
+
// VS Code и его сборки (Cursor, Windsurf, VSCodium) — штатной командой `<cli> --install-extension file.vsix --force`.
|
|
5
|
+
// JetBrains IDE (IDEA, PhpStorm, WebStorm, PyCharm, GoLand, …) — распаковкой плагина в папку плагинов последней
|
|
6
|
+
// версии каждой IDE; новая версия подхватывается при перезапуске IDE.
|
|
7
|
+
//
|
|
8
|
+
// Файлы берутся с сервера TokenWait: /downloads/versions.json, /downloads/tokenwait-vscode.vsix,
|
|
9
|
+
// /downloads/tokenwait-jetbrains.zip. Без согласия пользователя ничего не ставится (см. bin/tokenwait.js).
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const zlib = require('zlib');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const VSIX_ID = 'tokenwait.tokenwait';
|
|
17
|
+
const JB_FOLDER = 'tokenwait';
|
|
18
|
+
|
|
19
|
+
// ---------- версии ----------
|
|
20
|
+
|
|
21
|
+
function cmpVersion(a, b) {
|
|
22
|
+
const pa = String(a || '0').split('.').map((x) => parseInt(x, 10) || 0);
|
|
23
|
+
const pb = String(b || '0').split('.').map((x) => parseInt(x, 10) || 0);
|
|
24
|
+
for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) < (pb[i] || 0) ? -1 : 1;
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------- распаковка zip (без зависимостей) ----------
|
|
29
|
+
|
|
30
|
+
/** Файлы архива: [{name, data}]. Пути с «..» и абсолютные отбрасываются. */
|
|
31
|
+
function unzip(buf) {
|
|
32
|
+
let eocd = -1;
|
|
33
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
|
|
34
|
+
if (buf.readUInt32LE(i) === 0x06054b50) {
|
|
35
|
+
eocd = i;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (eocd < 0) throw new Error('not a zip archive');
|
|
40
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
41
|
+
let off = buf.readUInt32LE(eocd + 16);
|
|
42
|
+
const files = [];
|
|
43
|
+
for (let k = 0; k < count; k++) {
|
|
44
|
+
if (buf.readUInt32LE(off) !== 0x02014b50) throw new Error('broken zip archive');
|
|
45
|
+
const method = buf.readUInt16LE(off + 10);
|
|
46
|
+
const csize = buf.readUInt32LE(off + 20);
|
|
47
|
+
const nlen = buf.readUInt16LE(off + 28);
|
|
48
|
+
const xlen = buf.readUInt16LE(off + 30);
|
|
49
|
+
const clen = buf.readUInt16LE(off + 32);
|
|
50
|
+
const local = buf.readUInt32LE(off + 42);
|
|
51
|
+
const name = buf.toString('utf8', off + 46, off + 46 + nlen).replace(/\\/g, '/');
|
|
52
|
+
off += 46 + nlen + xlen + clen;
|
|
53
|
+
if (name.endsWith('/')) continue;
|
|
54
|
+
if (name.startsWith('/') || /^[A-Za-z]:/.test(name) || name.split('/').includes('..')) throw new Error('unsafe path in archive: ' + name);
|
|
55
|
+
if (buf.readUInt32LE(local) !== 0x04034b50) throw new Error('broken zip archive');
|
|
56
|
+
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
57
|
+
const raw = buf.subarray(start, start + csize);
|
|
58
|
+
let data;
|
|
59
|
+
if (method === 0) data = Buffer.from(raw);
|
|
60
|
+
else if (method === 8) data = zlib.inflateRawSync(raw);
|
|
61
|
+
else throw new Error('unsupported zip compression: ' + method);
|
|
62
|
+
files.push({ name, data });
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- JetBrains ----------
|
|
68
|
+
|
|
69
|
+
const JB_PRODUCTS = {
|
|
70
|
+
IntelliJIdea: 'IntelliJ IDEA',
|
|
71
|
+
IdeaIC: 'IntelliJ IDEA CE',
|
|
72
|
+
PhpStorm: 'PhpStorm',
|
|
73
|
+
WebStorm: 'WebStorm',
|
|
74
|
+
PyCharm: 'PyCharm',
|
|
75
|
+
PyCharmCE: 'PyCharm CE',
|
|
76
|
+
GoLand: 'GoLand',
|
|
77
|
+
RubyMine: 'RubyMine',
|
|
78
|
+
CLion: 'CLion',
|
|
79
|
+
Rider: 'Rider',
|
|
80
|
+
DataGrip: 'DataGrip',
|
|
81
|
+
DataSpell: 'DataSpell',
|
|
82
|
+
RustRover: 'RustRover',
|
|
83
|
+
Aqua: 'Aqua',
|
|
84
|
+
};
|
|
85
|
+
const JB_DIR = new RegExp(`^(${Object.keys(JB_PRODUCTS).join('|')})(\\d{4})\\.(\\d+)$`);
|
|
86
|
+
|
|
87
|
+
/** Где лежат настройки и плагины JetBrains IDE на этой ОС. */
|
|
88
|
+
function jbLayout(home = os.homedir(), platform = process.platform, env = process.env) {
|
|
89
|
+
if (platform === 'darwin') {
|
|
90
|
+
const base = path.join(home, 'Library', 'Application Support', 'JetBrains');
|
|
91
|
+
return { config: base, plugins: (dir) => path.join(base, dir, 'plugins') };
|
|
92
|
+
}
|
|
93
|
+
if (platform === 'win32') {
|
|
94
|
+
const base = path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'JetBrains');
|
|
95
|
+
return { config: base, plugins: (dir) => path.join(base, dir, 'plugins') };
|
|
96
|
+
}
|
|
97
|
+
const config = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'JetBrains');
|
|
98
|
+
const data = path.join(env.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'JetBrains');
|
|
99
|
+
return { config, plugins: (dir) => path.join(data, dir) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function jbInstalledVersion(pluginsDir) {
|
|
103
|
+
try {
|
|
104
|
+
for (const f of fs.readdirSync(path.join(pluginsDir, JB_FOLDER, 'lib'))) {
|
|
105
|
+
const m = /^tokenwait-(\d+\.\d+\.\d+)\.jar$/.exec(f);
|
|
106
|
+
if (m) return m[1];
|
|
107
|
+
}
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Последняя версия каждой JetBrains IDE, которую запускали на этой машине. */
|
|
113
|
+
function detectJetbrains(opts = {}) {
|
|
114
|
+
const layout = jbLayout(opts.home, opts.platform, opts.env);
|
|
115
|
+
let dirs = [];
|
|
116
|
+
try {
|
|
117
|
+
dirs = fs.readdirSync(layout.config);
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const latest = new Map();
|
|
122
|
+
for (const dir of dirs) {
|
|
123
|
+
const m = JB_DIR.exec(dir);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
const year = Number(m[2]);
|
|
126
|
+
const minor = Number(m[3]);
|
|
127
|
+
const prev = latest.get(m[1]);
|
|
128
|
+
if (!prev || year * 100 + minor > prev.year * 100 + prev.minor) latest.set(m[1], { dir, year, minor });
|
|
129
|
+
}
|
|
130
|
+
return [...latest.entries()].map(([product, v]) => {
|
|
131
|
+
const pluginsDir = layout.plugins(v.dir);
|
|
132
|
+
return {
|
|
133
|
+
kind: 'jetbrains',
|
|
134
|
+
id: product,
|
|
135
|
+
name: `${JB_PRODUCTS[product]} ${v.year}.${v.minor}`,
|
|
136
|
+
build: (v.year % 100) * 10 + v.minor, // 2024.3 -> 243 (номер ветки, как since-build)
|
|
137
|
+
pluginsDir,
|
|
138
|
+
installed: jbInstalledVersion(pluginsDir),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Распаковать плагин в папку плагинов IDE, заменив прежнюю версию. */
|
|
144
|
+
function installJetbrains(target, zipBuf) {
|
|
145
|
+
const files = unzip(zipBuf);
|
|
146
|
+
const top = new Set(files.map((f) => f.name.split('/')[0]));
|
|
147
|
+
if (top.size !== 1 || !top.has(JB_FOLDER)) throw new Error('unexpected plugin archive layout');
|
|
148
|
+
fs.mkdirSync(target.pluginsDir, { recursive: true });
|
|
149
|
+
const tmp = path.join(target.pluginsDir, '.tokenwait-new');
|
|
150
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
151
|
+
for (const f of files) {
|
|
152
|
+
const dst = path.join(tmp, f.name.slice(JB_FOLDER.length + 1));
|
|
153
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
154
|
+
fs.writeFileSync(dst, f.data);
|
|
155
|
+
}
|
|
156
|
+
const dst = path.join(target.pluginsDir, JB_FOLDER);
|
|
157
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
158
|
+
fs.renameSync(tmp, dst);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------- VS Code и его сборки ----------
|
|
162
|
+
|
|
163
|
+
const VSCODES = [
|
|
164
|
+
{ id: 'vscode', name: 'VS Code', bin: 'code', mac: 'Visual Studio Code.app/Contents/Resources/app/bin/code', win: 'Programs/Microsoft VS Code/bin/code.cmd' },
|
|
165
|
+
{ id: 'cursor', name: 'Cursor', bin: 'cursor', mac: 'Cursor.app/Contents/Resources/app/bin/cursor', win: 'Programs/cursor/resources/app/bin/cursor.cmd' },
|
|
166
|
+
{ id: 'windsurf', name: 'Windsurf', bin: 'windsurf', mac: 'Windsurf.app/Contents/Resources/app/bin/windsurf', win: 'Programs/Windsurf/bin/windsurf.cmd' },
|
|
167
|
+
{ id: 'vscodium', name: 'VSCodium', bin: 'codium', mac: 'VSCodium.app/Contents/Resources/app/bin/codium', win: 'Programs/VSCodium/bin/codium.cmd' },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
function which(bin, env = process.env, platform = process.platform) {
|
|
171
|
+
const exts = platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
|
|
172
|
+
for (const dir of String(env.PATH || '').split(path.delimiter)) {
|
|
173
|
+
if (!dir) continue;
|
|
174
|
+
for (const ext of exts) {
|
|
175
|
+
const f = path.join(dir, bin + ext);
|
|
176
|
+
try {
|
|
177
|
+
if (fs.statSync(f).isFile()) return f;
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runCli(cli, args, platform = process.platform) {
|
|
185
|
+
return execFileSync(cli, args, {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
timeout: 120000,
|
|
188
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
189
|
+
windowsHide: true,
|
|
190
|
+
shell: platform === 'win32' && /\.cmd$/i.test(cli),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function vscodeInstalledVersion(cli, platform) {
|
|
195
|
+
try {
|
|
196
|
+
const out = runCli(cli, ['--list-extensions', '--show-versions'], platform);
|
|
197
|
+
const line = out.split(/\r?\n/).find((l) => l.toLowerCase().startsWith(VSIX_ID + '@'));
|
|
198
|
+
return line ? line.split('@')[1].trim() : null;
|
|
199
|
+
} catch (_) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** VS Code и его сборки, у которых есть командная строка. */
|
|
205
|
+
function detectVscodes(opts = {}) {
|
|
206
|
+
const home = opts.home || os.homedir();
|
|
207
|
+
const platform = opts.platform || process.platform;
|
|
208
|
+
const env = opts.env || process.env;
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const v of VSCODES) {
|
|
212
|
+
let cli = which(v.bin, env, platform);
|
|
213
|
+
if (!cli && platform === 'darwin') {
|
|
214
|
+
for (const apps of ['/Applications', path.join(home, 'Applications')]) {
|
|
215
|
+
const f = path.join(apps, v.mac);
|
|
216
|
+
if (fs.existsSync(f)) {
|
|
217
|
+
cli = f;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!cli && platform === 'win32') {
|
|
223
|
+
const f = path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), v.win);
|
|
224
|
+
if (fs.existsSync(f)) cli = f;
|
|
225
|
+
}
|
|
226
|
+
if (!cli) continue;
|
|
227
|
+
let real = cli;
|
|
228
|
+
try {
|
|
229
|
+
real = fs.realpathSync(cli);
|
|
230
|
+
} catch (_) {}
|
|
231
|
+
if (seen.has(real)) continue;
|
|
232
|
+
seen.add(real);
|
|
233
|
+
out.push({ kind: 'vscode', id: v.id, name: v.name, cli, installed: vscodeInstalledVersion(cli, platform) });
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function installVscode(target, vsixFile, platform = process.platform) {
|
|
239
|
+
runCli(target.cli, ['--install-extension', vsixFile, '--force'], platform);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------- всё вместе ----------
|
|
243
|
+
|
|
244
|
+
function detectAll(opts = {}) {
|
|
245
|
+
return [...detectVscodes(opts), ...detectJetbrains(opts)];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function download(url, timeoutMs = 60000) {
|
|
249
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), headers: { 'user-agent': 'tokenwait/' + require('../package.json').version } });
|
|
250
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
251
|
+
return Buffer.from(await res.arrayBuffer());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Версии дополнений на сервере: {core, jetbrains, vscode, jetbrains_since_build}. */
|
|
255
|
+
async function latest(server) {
|
|
256
|
+
const buf = await download(server + '/downloads/versions.json', 10000);
|
|
257
|
+
const v = JSON.parse(buf.toString('utf8'));
|
|
258
|
+
return {
|
|
259
|
+
core: String(v.core || ''),
|
|
260
|
+
jetbrains: String(v.jetbrains || ''),
|
|
261
|
+
vscode: String(v.vscode || ''),
|
|
262
|
+
jetbrains_since_build: Number(v.jetbrains_since_build) || 0,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Что делать с этой IDE: 'install' | 'update' | 'current' | 'too-old'. */
|
|
267
|
+
function plan(target, versions) {
|
|
268
|
+
if (target.kind === 'jetbrains' && versions.jetbrains_since_build && target.build < versions.jetbrains_since_build) return 'too-old';
|
|
269
|
+
const want = target.kind === 'jetbrains' ? versions.jetbrains : versions.vscode;
|
|
270
|
+
if (!target.installed) return 'install';
|
|
271
|
+
return cmpVersion(target.installed, want) < 0 ? 'update' : 'current';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Поставить или обновить дополнение в выбранных IDE. Архивы скачиваются один раз.
|
|
276
|
+
* Возвращает [{target, action, ok, error?}].
|
|
277
|
+
*/
|
|
278
|
+
async function apply(server, targets, versions, opts = {}) {
|
|
279
|
+
const results = [];
|
|
280
|
+
let vsix = null;
|
|
281
|
+
let jbZip = null;
|
|
282
|
+
for (const t of targets) {
|
|
283
|
+
const action = plan(t, versions);
|
|
284
|
+
if (action === 'current' || action === 'too-old' || (opts.onlyInstalled && action === 'install')) {
|
|
285
|
+
results.push({ target: t, action: action === 'install' ? 'skipped' : action, ok: true });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
if (t.kind === 'vscode') {
|
|
290
|
+
if (!vsix) {
|
|
291
|
+
const buf = await download(`${server}/downloads/tokenwait-vscode.vsix?v=${encodeURIComponent(versions.vscode)}`);
|
|
292
|
+
vsix = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'tokenwait-')), `tokenwait-${versions.vscode || 'latest'}.vsix`);
|
|
293
|
+
fs.writeFileSync(vsix, buf);
|
|
294
|
+
}
|
|
295
|
+
installVscode(t, vsix);
|
|
296
|
+
} else {
|
|
297
|
+
if (!jbZip) jbZip = await download(`${server}/downloads/tokenwait-jetbrains.zip?v=${encodeURIComponent(versions.jetbrains)}`);
|
|
298
|
+
installJetbrains(t, jbZip);
|
|
299
|
+
}
|
|
300
|
+
results.push({ target: t, action, ok: true });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
results.push({ target: t, action, ok: false, error: String(e.message || e).split('\n')[0].slice(0, 200) });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (vsix) fs.rmSync(path.dirname(vsix), { recursive: true, force: true });
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { cmpVersion, unzip, jbLayout, detectJetbrains, installJetbrains, detectVscodes, installVscode, detectAll, latest, plan, apply, VSIX_ID };
|
package/lib/worker.js
CHANGED
|
@@ -54,8 +54,11 @@ async function report() {
|
|
|
54
54
|
if (Date.now() - (pend.last_report || 0) < (ads.report_seconds || 60) * 1000) return null;
|
|
55
55
|
const { counts, rest } = toImpressions(pend.ads, impSec);
|
|
56
56
|
if (!Object.keys(counts).length) return null;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Остаток неполных показов переносим вместе с его временем ожидания: иначе в следующем отчёте
|
|
58
|
+
// показ, собранный из остатка, сервер отклонит (показов не больше, чем время ожидания).
|
|
59
|
+
const restMs = Object.values(rest).reduce((x, y) => x + y, 0);
|
|
60
|
+
const waitMs = Math.max(0, (pend.wait_ms || 0) - restMs);
|
|
61
|
+
writeJson(p('pending.json'), { ads: rest, wait_ms: Math.min(restMs, pend.wait_ms || 0), last_report: Date.now() });
|
|
59
62
|
return { counts, waitMs };
|
|
60
63
|
});
|
|
61
64
|
if (!taken) return null;
|
package/package.json
CHANGED