cursorconnect 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.
Files changed (36) hide show
  1. package/README.md +5 -4
  2. package/bridge-runtime/dist/agent-title-match.js +16 -0
  3. package/bridge-runtime/dist/chat-display-store.d.ts +13 -0
  4. package/bridge-runtime/dist/chat-display-store.js +29 -0
  5. package/bridge-runtime/dist/chat-display.d.ts +11 -0
  6. package/bridge-runtime/dist/chat-display.js +290 -0
  7. package/bridge-runtime/dist/chat-sync.d.ts +6 -0
  8. package/bridge-runtime/dist/chat-sync.js +88 -0
  9. package/bridge-runtime/dist/extract-page.js +99 -3
  10. package/bridge-runtime/dist/history-pipeline-log.d.ts +16 -0
  11. package/bridge-runtime/dist/history-pipeline-log.js +29 -0
  12. package/bridge-runtime/dist/jsonl-index.d.ts +15 -3
  13. package/bridge-runtime/dist/jsonl-index.js +48 -12
  14. package/bridge-runtime/dist/message-filter.d.ts +10 -0
  15. package/bridge-runtime/dist/message-filter.js +65 -5
  16. package/bridge-runtime/dist/pairing-code.d.ts +3 -0
  17. package/bridge-runtime/dist/pairing-code.js +17 -0
  18. package/bridge-runtime/dist/pairing-identity.js +4 -7
  19. package/bridge-runtime/dist/relay.d.ts +8 -0
  20. package/bridge-runtime/dist/relay.js +254 -25
  21. package/bridge-runtime/dist/sidebar-merge.js +2 -2
  22. package/bridge-runtime/dist/types.d.ts +9 -1
  23. package/config.env.defaults +3 -0
  24. package/dist/bridge-dir.js +5 -0
  25. package/dist/cli-version.js +13 -0
  26. package/dist/diagnose.js +224 -0
  27. package/dist/index.js +56 -55
  28. package/dist/launch.js +45 -13
  29. package/dist/pairing-code.js +18 -0
  30. package/dist/pairing-identity.js +3 -6
  31. package/dist/print-pairing.js +9 -7
  32. package/dist/relay-config.js +49 -0
  33. package/dist/semver.js +21 -0
  34. package/dist/version-check.js +31 -0
  35. package/package.json +6 -2
  36. package/version-policy.json +8 -0
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync } from 'fs';
3
2
  import { join, resolve } from 'path';
4
3
  import { ensurePairingIdentity, loadPairingIdentity, refreshPairingCode, } from './pairing-identity.js';
5
4
  import { ensureCursorCdp, isBridgeRunning, startBridge, stopBridge, waitBridgeHealth, waitRelayConnector, } from './launch.js';
@@ -7,40 +6,24 @@ import { printPairingToTerminal } from './print-pairing.js';
7
6
  import { BRIDGE_LOG_FILE } from './paths.js';
8
7
  import { ensureUserConfig, isValidBridgeDir, resolveBridgeDir, USER_CONFIG_ENV, } from './bridge-dir.js';
9
8
  import { configFilePath, saveInstallConfig } from './repo-root.js';
10
- function loadEnvFile(path) {
11
- if (!existsSync(path))
12
- return {};
13
- const out = {};
14
- for (const line of readFileSync(path, 'utf-8').split('\n')) {
15
- const t = line.trim();
16
- if (!t || t.startsWith('#'))
17
- continue;
18
- const i = t.indexOf('=');
19
- if (i < 1)
20
- continue;
21
- out[t.slice(0, i).trim()] = t.slice(i + 1).trim();
22
- }
23
- return out;
24
- }
9
+ import { formatDiagnoseReport, runDiagnose } from './diagnose.js';
10
+ import { loadEnvFile, resolveRelayConfig } from './relay-config.js';
11
+ import { ensureCliVersionForRelay } from './version-check.js';
12
+ import { CLI_VERSION } from './cli-version.js';
25
13
  function bridgeEnv(identity) {
26
14
  ensureUserConfig();
27
- const paths = [
28
- USER_CONFIG_ENV,
29
- join(resolveBridgeDir(), '.env'),
30
- ];
31
- let file = {};
32
- for (const p of paths) {
33
- if (existsSync(p)) {
34
- file = { ...file, ...loadEnvFile(p) };
35
- }
36
- }
37
- const relayUrl = file.RELAY_URL ?? process.env.RELAY_URL ?? '';
38
- const relayToken = file.RELAY_TOKEN ?? process.env.RELAY_TOKEN ?? '';
39
- if (!relayUrl || !relayToken) {
40
- console.error(`Нужны RELAY_URL и RELAY_TOKEN в ${USER_CONFIG_ENV}\n` +
41
- '(создаётся при установке; отредактируйте и снова: cursorconnect start)');
15
+ const bridgeDotEnv = join(resolveBridgeDir(), '.env');
16
+ const { relayUrl, relayToken } = resolveRelayConfig([bridgeDotEnv]);
17
+ if (!relayToken) {
18
+ console.error('[cursorconnect] Не найден RELAY_TOKEN (встроенный конфиг пакета повреждён).\n' +
19
+ ` Переустановите: npm install -g cursorconnect\n` +
20
+ ` Или укажите RELAY_TOKEN в ${USER_CONFIG_ENV} (свой relay).`);
42
21
  process.exit(1);
43
22
  }
23
+ const file = {
24
+ ...loadEnvFile(bridgeDotEnv),
25
+ ...loadEnvFile(USER_CONFIG_ENV),
26
+ };
44
27
  return {
45
28
  RELAY_URL: relayUrl,
46
29
  RELAY_TOKEN: relayToken,
@@ -48,39 +31,49 @@ function bridgeEnv(identity) {
48
31
  SERVER_HOST: '127.0.0.1',
49
32
  SERVER_PORT: '3847',
50
33
  CDP_URL: file.CDP_URL ?? 'http://127.0.0.1:9222',
34
+ CURSORCONNECT_CLI_VERSION: CLI_VERSION,
51
35
  };
52
36
  }
53
37
  function startFlags(argv) {
54
38
  return {
55
39
  restartCursor: argv.includes('--restart-cursor') || argv.includes('-r'),
40
+ noRestartCursor: argv.includes('--no-restart-cursor'),
56
41
  };
57
42
  }
58
43
  async function cmdStart(argv) {
59
- const { restartCursor } = startFlags(argv);
44
+ const { restartCursor, noRestartCursor } = startFlags(argv);
60
45
  const identity = ensurePairingIdentity();
61
46
  const refreshed = refreshPairingCode(identity);
62
47
  const env = bridgeEnv(refreshed);
63
48
  const relayUrl = env.RELAY_URL;
64
- console.log('CursorConnect');
65
- if (restartCursor) {
66
- console.log('Перезапуск Cursor с CDP…');
49
+ await ensureCliVersionForRelay(relayUrl);
50
+ // 1) Сначала CDP (порт 9222), иначе bridge стартует без сайдбара Cursor.
51
+ const cdpPortOk = await ensureCursorCdp({
52
+ cdpUrl: env.CDP_URL,
53
+ restartCursor,
54
+ noRestartCursor,
55
+ });
56
+ if (!cdpPortOk) {
57
+ console.warn('[cursorconnect] CDP порт недоступен — bridge запустится, список чатов будет из архива JSONL.');
67
58
  }
68
- console.log(`Компьютер: ${refreshed.machineLabel}`);
69
59
  if (isBridgeRunning()) {
70
- console.log('Перезапуск bridge (новый код pairing)…');
60
+ console.log('Перезапуск bridge…');
71
61
  stopBridge();
72
62
  }
73
63
  startBridge(env);
74
64
  console.log(`Bridge в фоне → ${BRIDGE_LOG_FILE}`);
75
- const cdpOk = await ensureCursorCdp({
76
- cdpUrl: env.CDP_URL,
77
- restartCursor,
78
- });
79
- const health = await waitBridgeHealth(25_000);
80
- const connectorOk = await waitRelayConnector(relayUrl, 25_000);
65
+ // 2) Ждём, пока bridge реально подключится к workbench (health.cdp), не только :9222.
66
+ const health = await waitBridgeHealth(45_000, { requireCdp: cdpPortOk });
67
+ const connectorOk = await waitRelayConnector(relayUrl, 30_000);
68
+ const cdpReady = cdpPortOk && health.cdp;
69
+ if (cdpPortOk && !health.cdp) {
70
+ console.warn('[cursorconnect] Порт 9222 открыт, но bridge не подключился к Cursor (cdp:false). ' +
71
+ 'Проверьте: cursorconnect status · tail -f ' +
72
+ BRIDGE_LOG_FILE);
73
+ }
81
74
  printPairingToTerminal(refreshed, {
82
75
  bridge: health.ok,
83
- cdp: cdpOk && health.cdp,
76
+ cdp: cdpReady,
84
77
  connector: connectorOk,
85
78
  });
86
79
  }
@@ -102,11 +95,17 @@ async function cmdInit(pathArg) {
102
95
  console.log(`repoRoot: ${abs}`);
103
96
  console.log('\nДальше: cursorconnect start');
104
97
  }
98
+ async function cmdDiagnose() {
99
+ const report = await runDiagnose();
100
+ console.log(formatDiagnoseReport(report));
101
+ const failed = report.checks.some((c) => !c.ok);
102
+ process.exit(failed ? 1 : 0);
103
+ }
105
104
  async function cmdStatus() {
106
105
  const identity = loadPairingIdentity();
107
106
  ensureUserConfig();
108
- const env = loadEnvFile(USER_CONFIG_ENV);
109
- const relayUrl = env.RELAY_URL ?? process.env.RELAY_URL ?? '';
107
+ const bridgeDotEnv = join(resolveBridgeDir(), '.env');
108
+ const { relayUrl } = resolveRelayConfig([bridgeDotEnv]);
110
109
  console.log('Bridge running:', isBridgeRunning());
111
110
  if (!identity) {
112
111
  console.log('Identity не найден — запустите: cursorconnect start');
@@ -133,18 +132,15 @@ async function cmdStatus() {
133
132
  catch {
134
133
  /* ignore */
135
134
  }
135
+ printPairingToTerminal(identity, { bridge: bridgeOk, cdp, connector });
136
136
  }
137
- if (relayUrl) {
137
+ else {
138
138
  printPairingToTerminal(identity, {
139
139
  bridge: bridgeOk,
140
140
  cdp,
141
- connector,
141
+ connector: false,
142
142
  });
143
143
  }
144
- else {
145
- console.log('Room:', identity.roomId);
146
- console.log('Код:', identity.pairingCode);
147
- }
148
144
  }
149
145
  async function main() {
150
146
  const argv = process.argv.slice(2);
@@ -160,21 +156,26 @@ async function main() {
160
156
  case 'status':
161
157
  await cmdStatus();
162
158
  break;
159
+ case 'diagnose':
160
+ await cmdDiagnose();
161
+ break;
163
162
  case 'init':
164
163
  await cmdInit(rest[0]);
165
164
  break;
166
165
  default:
167
166
  console.log(`Usage: cursorconnect <command>
168
167
 
169
- start [--restart-cursor|-r] bridge в фоне + код для приложения
168
+ start CDP (авто-перезапуск Cursor) + bridge + код
169
+ start -r — то же (явный перезапуск Cursor)
170
+ start --no-restart-cursor — не трогать Cursor (спросит y/n в TTY)
170
171
  stop — остановить bridge
171
172
  status — код и статус
173
+ diagnose — проверка relay / bridge / socket (без гаданий)
172
174
  init [path] — только для разработки (клон репо)
173
175
 
174
176
  Установка (из любой папки):
175
177
  npm install -g cursorconnect
176
- # один раз: ~/.cursorconnect/config.env RELAY_URL и RELAY_TOKEN
177
- cursorconnect start`);
178
+ cursorconnect start # код pairing, relay уже в пакете`);
178
179
  process.exit(cmd === 'help' ? 0 : 1);
179
180
  }
180
181
  }
package/dist/launch.js CHANGED
@@ -34,8 +34,22 @@ export function stopBridge() {
34
34
  /* ignore */
35
35
  }
36
36
  }
37
+ function killProcessesOnBridgePort() {
38
+ try {
39
+ execSync('lsof -t -i:3847 2>/dev/null | xargs kill -TERM 2>/dev/null', {
40
+ stdio: 'ignore',
41
+ shell: '/bin/bash',
42
+ });
43
+ /* give old bridge time to exit */
44
+ execSync('sleep 0.5', { stdio: 'ignore', shell: '/bin/bash' });
45
+ }
46
+ catch {
47
+ /* port free */
48
+ }
49
+ }
37
50
  export function startBridge(env) {
38
51
  stopBridge();
52
+ killProcessesOnBridgePort();
39
53
  const bridgeDir = resolveBridgeDir();
40
54
  const logFd = openLogAppend();
41
55
  const child = spawn('node', ['dist/index.js'], {
@@ -51,19 +65,35 @@ export function startBridge(env) {
51
65
  function openLogAppend() {
52
66
  return openSync(BRIDGE_LOG_FILE, 'a');
53
67
  }
54
- export async function waitBridgeHealth(maxMs = 30_000) {
68
+ export async function waitBridgeHealth(maxMs = 30_000, opts) {
69
+ const requireCdp = opts?.requireCdp === true;
55
70
  const deadline = Date.now() + maxMs;
56
71
  while (Date.now() < deadline) {
57
72
  try {
58
73
  const res = await fetch('http://127.0.0.1:3847/health');
59
- if (res.ok)
60
- return (await res.json());
74
+ if (res.ok) {
75
+ const j = (await res.json());
76
+ const ok = Boolean(j.ok);
77
+ const cdp = Boolean(j.cdp);
78
+ if (ok && (!requireCdp || cdp))
79
+ return { ok, cdp };
80
+ }
61
81
  }
62
82
  catch {
63
83
  /* retry */
64
84
  }
65
85
  await sleep(500);
66
86
  }
87
+ try {
88
+ const res = await fetch('http://127.0.0.1:3847/health');
89
+ if (res.ok) {
90
+ const j = (await res.json());
91
+ return { ok: Boolean(j.ok), cdp: Boolean(j.cdp) };
92
+ }
93
+ }
94
+ catch {
95
+ /* ignore */
96
+ }
67
97
  return { ok: false, cdp: false };
68
98
  }
69
99
  export async function waitRelayConnector(relayUrl, maxMs = 45_000) {
@@ -146,29 +176,31 @@ export async function ensureCursorCdp(opts = {}) {
146
176
  if (!running) {
147
177
  console.log('[cursorconnect] Cursor не запущен — старт с --remote-debugging-port=9222…');
148
178
  launchCursorWithCdp();
149
- return waitCdp(cdpUrl, 50);
179
+ if (await waitCdp(cdpUrl, 50))
180
+ return true;
181
+ printManualCdpInstructions();
182
+ return false;
150
183
  }
151
- // Cursor запущен, CDP недоступен
152
- let shouldRestart = Boolean(opts.restartCursor);
184
+ // Cursor запущен, порт 9222 не отвечает — обычный запуск без --remote-debugging-port
185
+ let shouldRestart = opts.noRestartCursor !== true;
153
186
  if (!shouldRestart) {
154
- if (!process.stdin.isTTY) {
155
- console.warn('[cursorconnect] Cursor без CDP (нет интерактивного терминала).');
156
- printManualCdpInstructions();
157
- return false;
187
+ shouldRestart = Boolean(opts.restartCursor);
188
+ if (!shouldRestart && process.stdin.isTTY) {
189
+ shouldRestart = await askYesNo('Cursor без CDP. Перезапустить с --remote-debugging-port=9222? (y/n): ');
158
190
  }
159
- shouldRestart = await askYesNo('Cursor запущен без CDP. Перезапустить с --remote-debugging-port=9222? (y/n): ');
160
191
  }
161
192
  if (!shouldRestart) {
193
+ console.warn('[cursorconnect] Cursor без CDP — список чатов будет только из JSONL.');
162
194
  printManualCdpInstructions();
163
195
  return false;
164
196
  }
197
+ console.log('[cursorconnect] Cursor без CDP — перезапуск с --remote-debugging-port=9222…');
165
198
  const quitOk = await quitCursor();
166
199
  if (!quitOk) {
167
- console.warn('[cursorconnect] Не удалось закрыть Cursor.');
200
+ console.warn('[cursorconnect] Не удалось закрыть Cursor (Cmd+Q вручную).');
168
201
  printManualCdpInstructions();
169
202
  return false;
170
203
  }
171
- console.log('[cursorconnect] Запуск Cursor с --remote-debugging-port=9222…');
172
204
  launchCursorWithCdp();
173
205
  if (await waitCdp(cdpUrl, 50))
174
206
  return true;
@@ -0,0 +1,18 @@
1
+ import { randomBytes } from 'crypto';
2
+ export const PAIRING_CODE_LENGTH = 6;
3
+ /** Latin letters + digits (ASCII), uppercase in storage. */
4
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
5
+ export function generatePairingCode() {
6
+ const bytes = randomBytes(PAIRING_CODE_LENGTH);
7
+ let out = '';
8
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
9
+ out += ALPHABET[bytes[i] % ALPHABET.length];
10
+ }
11
+ return out;
12
+ }
13
+ export function normalizePairingCode(raw) {
14
+ const code = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
15
+ if (code.length !== PAIRING_CODE_LENGTH)
16
+ return null;
17
+ return code;
18
+ }
@@ -1,13 +1,10 @@
1
1
  import { randomBytes, randomUUID } from 'crypto';
2
+ import { generatePairingCode } from './pairing-code.js';
2
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
4
  import { homedir, hostname } from 'os';
4
5
  import { join } from 'path';
5
6
  const DIR = join(homedir(), '.cursorconnect');
6
7
  const FILE = join(DIR, 'identity.json');
7
- function formatPairingCode() {
8
- const n = randomBytes(3).readUIntBE(0, 3) % 1_000_000;
9
- return String(n).padStart(6, '0');
10
- }
11
8
  export function pairingIdentityPath() {
12
9
  return FILE;
13
10
  }
@@ -35,7 +32,7 @@ export function ensurePairingIdentity(machineLabel) {
35
32
  roomId: randomUUID(),
36
33
  clientToken: randomBytes(32).toString('hex'),
37
34
  machineLabel: machineLabel?.trim() || defaultMachineLabel(),
38
- pairingCode: formatPairingCode(),
35
+ pairingCode: generatePairingCode(),
39
36
  pairingCodeExpiresAt: Date.now() + 10 * 60_000,
40
37
  createdAt: now,
41
38
  updatedAt: now,
@@ -47,7 +44,7 @@ export function refreshPairingCode(identity, machineLabel) {
47
44
  const next = {
48
45
  ...identity,
49
46
  machineLabel: machineLabel?.trim() || identity.machineLabel,
50
- pairingCode: formatPairingCode(),
47
+ pairingCode: generatePairingCode(),
51
48
  pairingCodeExpiresAt: Date.now() + 10 * 60_000,
52
49
  updatedAt: new Date().toISOString(),
53
50
  };
@@ -1,19 +1,18 @@
1
- import { renderBigCode } from './big-code.js';
2
1
  export function printPairingToTerminal(identity, status) {
3
2
  const expiresIn = Math.max(0, Math.floor((identity.pairingCodeExpiresAt - Date.now()) / 1000));
4
- const ok = status.bridge && status.cdp && status.connector;
3
+ const ready = status.bridge && status.cdp && status.connector;
5
4
  const code = identity.pairingCode;
6
5
  console.log('\n────────────────────────────────────────');
7
- console.log(` ${identity.machineLabel}`);
6
+ console.log(' CursorConnect');
7
+ console.log(` Компьютер: ${identity.machineLabel}`);
8
8
  console.log('');
9
9
  console.log(' Введите код в приложении CursorConnect:');
10
10
  console.log('');
11
- console.log(renderBigCode(code));
11
+ console.log(` ${code}`);
12
12
  console.log('');
13
- console.log(` Код: ${code.split('').join(' ')}`);
13
+ console.log(' Код одноразовый — после подключения снова: cursorconnect start');
14
14
  console.log(` Действует ~${Math.max(1, Math.floor(expiresIn / 60))} мин`);
15
- console.log('');
16
- if (!ok) {
15
+ if (!ready) {
17
16
  const parts = [];
18
17
  if (!status.bridge)
19
18
  parts.push('bridge');
@@ -21,12 +20,15 @@ export function printPairingToTerminal(identity, status) {
21
20
  parts.push('Cursor');
22
21
  if (!status.connector)
23
22
  parts.push('сервер');
23
+ console.log('');
24
24
  console.log(` Ожидание: ${parts.join(', ')}…`);
25
25
  console.log(' Подождите 10–20 с и снова: cursorconnect status');
26
26
  }
27
27
  else {
28
+ console.log('');
28
29
  console.log(' Готово — введите код на телефоне.');
29
30
  }
31
+ console.log('');
30
32
  console.log('────────────────────────────────────────');
31
33
  console.log(' Bridge в фоне · cursorconnect stop');
32
34
  console.log(' Консоль можно закрыть.\n');
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { resolvePackageRoot } from './bridge-dir.js';
4
+ import { USER_CONFIG_ENV } from './bridge-dir.js';
5
+ /** Shipped with npm package (generated at publish from bridge/.env). */
6
+ export const BUNDLED_CONFIG_ENV = join(resolvePackageRoot(), 'config.env.defaults');
7
+ /** Public production relay (same as app `extra.bridgeUrl`). */
8
+ export const DEFAULT_RELAY_URL = 'https://cc.fanpay.online';
9
+ export function loadEnvFile(path) {
10
+ if (!existsSync(path))
11
+ return {};
12
+ const out = {};
13
+ for (const line of readFileSync(path, 'utf-8').split('\n')) {
14
+ const t = line.trim();
15
+ if (!t || t.startsWith('#'))
16
+ continue;
17
+ const i = t.indexOf('=');
18
+ if (i < 1)
19
+ continue;
20
+ out[t.slice(0, i).trim()] = t.slice(i + 1).trim();
21
+ }
22
+ return out;
23
+ }
24
+ function pickNonEmpty(...values) {
25
+ for (const v of values) {
26
+ const t = v?.trim();
27
+ if (t)
28
+ return t;
29
+ }
30
+ return '';
31
+ }
32
+ /** Relay URL + connector token: bundled defaults → user config → bridge .env → process.env. */
33
+ export function resolveRelayConfig(extraPaths = []) {
34
+ const layers = [];
35
+ if (existsSync(BUNDLED_CONFIG_ENV))
36
+ layers.push(loadEnvFile(BUNDLED_CONFIG_ENV));
37
+ for (const p of extraPaths) {
38
+ if (existsSync(p))
39
+ layers.push(loadEnvFile(p));
40
+ }
41
+ if (existsSync(USER_CONFIG_ENV))
42
+ layers.push(loadEnvFile(USER_CONFIG_ENV));
43
+ const merged = {};
44
+ for (const layer of layers)
45
+ Object.assign(merged, layer);
46
+ const relayUrl = pickNonEmpty(merged.RELAY_URL, process.env.RELAY_URL, DEFAULT_RELAY_URL);
47
+ const relayToken = pickNonEmpty(merged.RELAY_TOKEN, process.env.RELAY_TOKEN);
48
+ return { relayUrl, relayToken };
49
+ }
package/dist/semver.js ADDED
@@ -0,0 +1,21 @@
1
+ export function parseSemver(v) {
2
+ const m = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
3
+ if (!m)
4
+ return null;
5
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
6
+ }
7
+ export function compareSemver(a, b) {
8
+ const pa = parseSemver(a);
9
+ const pb = parseSemver(b);
10
+ if (!pa || !pb)
11
+ return null;
12
+ for (let i = 0; i < 3; i++) {
13
+ if (pa[i] !== pb[i])
14
+ return pa[i] < pb[i] ? -1 : 1;
15
+ }
16
+ return 0;
17
+ }
18
+ export function isVersionAtLeast(current, minimum) {
19
+ const c = compareSemver(current, minimum);
20
+ return c !== null && c >= 0;
21
+ }
@@ -0,0 +1,31 @@
1
+ import { isVersionAtLeast } from './semver.js';
2
+ import { CLI_VERSION } from './cli-version.js';
3
+ export async function fetchRemoteVersionPolicy(relayUrl) {
4
+ const base = relayUrl.replace(/\/$/, '');
5
+ try {
6
+ const res = await fetch(`${base}/api/version-policy`, {
7
+ signal: AbortSignal.timeout(12_000),
8
+ });
9
+ if (!res.ok)
10
+ return null;
11
+ return (await res.json());
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function assertCliVersionAllowed(policy, current = CLI_VERSION) {
18
+ if (isVersionAtLeast(current, policy.minCliVersion))
19
+ return;
20
+ console.error(`[cursorconnect] Версия ${current} устарела (нужна ≥ ${policy.minCliVersion}).\n` +
21
+ ` ${policy.updateCliCommand}`);
22
+ process.exit(1);
23
+ }
24
+ export async function ensureCliVersionForRelay(relayUrl) {
25
+ const policy = await fetchRemoteVersionPolicy(relayUrl);
26
+ if (!policy?.minCliVersion) {
27
+ console.warn('[cursorconnect] Не удалось проверить версию на relay — продолжаем без проверки.');
28
+ return;
29
+ }
30
+ assertCliVersionAllowed(policy);
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursorconnect",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI: CursorConnect bridge + relay pairing — install once, run from anywhere",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,6 +10,8 @@
10
10
  "files": [
11
11
  "dist/**/*.js",
12
12
  "bridge-runtime/**",
13
+ "config.env.defaults",
14
+ "version-policy.json",
13
15
  "README.md"
14
16
  ],
15
17
  "scripts": {
@@ -29,7 +31,9 @@
29
31
  "bridge",
30
32
  "relay"
31
33
  ],
32
- "dependencies": {},
34
+ "dependencies": {
35
+ "socket.io-client": "^4.8.0"
36
+ },
33
37
  "devDependencies": {
34
38
  "@types/node": "^22.0.0",
35
39
  "tsx": "^4.19.0",
@@ -0,0 +1,8 @@
1
+ {
2
+ "minCliVersion": "0.1.4",
3
+ "minAppVersion": "0.2.2",
4
+ "latestCliVersion": "0.1.4",
5
+ "latestAppVersion": "0.2.2",
6
+ "updateCliCommand": "npm install -g cursorconnect@latest",
7
+ "updateAppHint": "Обновите CursorConnect в App Store / TestFlight до последней сборки."
8
+ }