tokenwait 0.1.3 → 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 +116 -3
- package/lib/agents.js +21 -2
- package/lib/ide.js +309 -0
- 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
|
|
|
@@ -92,6 +95,7 @@ async function cmdInstall() {
|
|
|
92
95
|
if (!flag('yes')) {
|
|
93
96
|
console.log(CONSENT);
|
|
94
97
|
console.log('Agents: ' + ids.map((id) => agents.AGENTS[id].name).join(', ') + '\n');
|
|
98
|
+
if (ids.includes('codex')) console.log('⚠ ' + agents.codexNote() + '\n');
|
|
95
99
|
const a = await ask('Install TokenWait? [y/N] ');
|
|
96
100
|
if (!/^y(es)?$/i.test(a)) return console.log('Cancelled. Nothing was changed.');
|
|
97
101
|
}
|
|
@@ -109,7 +113,7 @@ async function cmdInstall() {
|
|
|
109
113
|
const r = agents.install(id, node, app);
|
|
110
114
|
results.push({ agent: id, ok: true, file: r.file });
|
|
111
115
|
if (!json) console.log(`✓ ${agents.AGENTS[id].name}: ${r.file}`);
|
|
112
|
-
if (!json &&
|
|
116
|
+
if (!json && id === 'codex') console.log(' ⚠ ' + agents.codexNote());
|
|
113
117
|
} catch (e) {
|
|
114
118
|
results.push({ agent: id, ok: false, error: e.message });
|
|
115
119
|
if (!json) console.log(`✗ ${agents.AGENTS[id].name}: ${e.message}`);
|
|
@@ -120,14 +124,119 @@ async function cmdInstall() {
|
|
|
120
124
|
} catch (e) {
|
|
121
125
|
if (!json) console.log('Could not reach the server yet (' + e.message + '). Ads will load automatically later.');
|
|
122
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'));
|
|
123
131
|
if (json) return out({ ok: results.every((r) => r.ok), agents: results });
|
|
124
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.');
|
|
125
135
|
if (!flag('no-login') && api.credentials()) {
|
|
126
136
|
const a = await ask('Sign in now (GitHub, Google or Apple) so earnings go to your account? [Y/n] ');
|
|
127
137
|
if (!/^n/i.test(a)) await cmdLogin();
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
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
|
+
|
|
131
240
|
async function cmdUninstall() {
|
|
132
241
|
const res = [];
|
|
133
242
|
for (const id of Object.keys(agents.AGENTS)) {
|
|
@@ -212,10 +321,12 @@ function help() {
|
|
|
212
321
|
npx tokenwait install set up Claude Code / Codex CLI / Gemini CLI
|
|
213
322
|
npx tokenwait login link this machine to your account (GitHub, Google or Apple)
|
|
214
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
|
|
215
326
|
npx tokenwait logout unlink this machine
|
|
216
327
|
npx tokenwait uninstall remove everything from agent configs (--purge also deletes ~/.tokenwait)
|
|
217
328
|
|
|
218
|
-
Options: --agents claude,codex,gemini --server URL --yes --json`);
|
|
329
|
+
Options: --agents claude,codex,gemini --server URL --yes --json --ide all|none --list`);
|
|
219
330
|
}
|
|
220
331
|
|
|
221
332
|
const commands = {
|
|
@@ -224,6 +335,8 @@ const commands = {
|
|
|
224
335
|
login: cmdLogin,
|
|
225
336
|
logout: cmdLogout,
|
|
226
337
|
status: cmdStatus,
|
|
338
|
+
ide: cmdIde,
|
|
339
|
+
update: cmdUpdate,
|
|
227
340
|
'link-start': async () => out(await cmdLinkStart()),
|
|
228
341
|
me: async () => out(await api.me()),
|
|
229
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/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/package.json
CHANGED