cursorconnect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # cursorconnect
2
+
3
+ CLI для Mac: bridge в фоне, pairing (QR + код), Cursor с CDP.
4
+
5
+ ## Установка (пользователи)
6
+
7
+ **Из npm** (после publish):
8
+
9
+ ```bash
10
+ npm install -g cursorconnect
11
+ ```
12
+
13
+ **Из репозитория** (без npm):
14
+
15
+ ```bash
16
+ cd CursorConnect && npm install
17
+ npm run install:cli
18
+ ```
19
+
20
+ `install:cli` собирает пакет и ставит глобально `cursorconnect`, вызывает `init` для текущего репо.
21
+
22
+ В `bridge/.env` задайте `RELAY_URL` и `RELAY_TOKEN`.
23
+
24
+ ## Команды
25
+
26
+ ```bash
27
+ cursorconnect start # bridge + QR в терминале
28
+ cursorconnect start -r # перезапуск Cursor с CDP без вопроса
29
+ cursorconnect status
30
+ cursorconnect stop
31
+ cursorconnect init /path/to/CursorConnect
32
+ ```
33
+
34
+ ## Публикация в npm (maintainers)
35
+
36
+ Из корня монорепо:
37
+
38
+ ```bash
39
+ cd connect
40
+ npm run build
41
+ npm publish --access public
42
+ ```
43
+
44
+ Пакет в registry: [`cursorconnect`](https://www.npmjs.com/package/cursorconnect) (имя в `package.json`).
45
+
46
+ Перед первой публикацией: аккаунт npm, `npm login`, уникальное имя `cursorconnect` (или `@scope/cursorconnect` → `npm install @scope/cursorconnect`).
package/dist/ask.js ADDED
@@ -0,0 +1,17 @@
1
+ import * as readline from 'readline/promises';
2
+ /** y/yes/д/да → true; пустой ввод и остальное → false */
3
+ export async function askYesNo(prompt) {
4
+ if (!process.stdin.isTTY)
5
+ return false;
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout,
9
+ });
10
+ try {
11
+ const answer = (await rl.question(prompt)).trim().toLowerCase();
12
+ return answer === 'y' || answer === 'yes' || answer === 'д' || answer === 'да';
13
+ }
14
+ finally {
15
+ rl.close();
16
+ }
17
+ }
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join, resolve } from 'path';
4
+ import { ensurePairingIdentity, loadPairingIdentity, refreshPairingCode, } from './pairing-identity.js';
5
+ import { ensureCursorCdp, isBridgeRunning, startBridge, stopBridge, waitBridgeHealth, waitRelayConnector, } from './launch.js';
6
+ import { printPairingToTerminal } from './print-pairing.js';
7
+ import { BRIDGE_LOG_FILE } from './paths.js';
8
+ import { configFilePath, isValidRepoRoot, resolveRepoRoot, saveInstallConfig, } from './repo-root.js';
9
+ 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 bridgeEnv(identity) {
25
+ const repoRoot = resolveRepoRoot();
26
+ const bridgeEnvPath = join(repoRoot, 'bridge', '.env');
27
+ const file = loadEnvFile(bridgeEnvPath);
28
+ const relayUrl = file.RELAY_URL ?? process.env.RELAY_URL ?? '';
29
+ const relayToken = file.RELAY_TOKEN ?? process.env.RELAY_TOKEN ?? '';
30
+ if (!relayUrl || !relayToken) {
31
+ console.error('Нужны RELAY_URL и RELAY_TOKEN в bridge/.env (см. bridge/.env.example)');
32
+ process.exit(1);
33
+ }
34
+ return {
35
+ RELAY_URL: relayUrl,
36
+ RELAY_TOKEN: relayToken,
37
+ RELAY_ROOM_ID: identity.roomId,
38
+ SERVER_HOST: '127.0.0.1',
39
+ SERVER_PORT: '3847',
40
+ CDP_URL: file.CDP_URL ?? 'http://127.0.0.1:9222',
41
+ };
42
+ }
43
+ function startFlags(argv) {
44
+ return {
45
+ restartCursor: argv.includes('--restart-cursor') || argv.includes('-r'),
46
+ };
47
+ }
48
+ async function cmdStart(argv) {
49
+ const { restartCursor } = startFlags(argv);
50
+ const identity = ensurePairingIdentity();
51
+ const refreshed = refreshPairingCode(identity);
52
+ const env = bridgeEnv(refreshed);
53
+ const relayUrl = env.RELAY_URL;
54
+ console.log('=== CursorConnect (Mac) ===');
55
+ if (restartCursor) {
56
+ console.log('Флаг: --restart-cursor (перезапуск без вопроса)');
57
+ }
58
+ console.log(`Machine: ${refreshed.machineLabel}`);
59
+ console.log(`Room: ${refreshed.roomId}`);
60
+ console.log(`Relay: ${relayUrl}`);
61
+ if (isBridgeRunning()) {
62
+ console.log('Перезапуск bridge (новый код pairing)…');
63
+ stopBridge();
64
+ }
65
+ startBridge(env);
66
+ console.log(`Bridge в фоне → ${BRIDGE_LOG_FILE}`);
67
+ const cdpOk = await ensureCursorCdp({
68
+ cdpUrl: env.CDP_URL,
69
+ restartCursor,
70
+ });
71
+ const health = await waitBridgeHealth(25_000);
72
+ const connectorOk = await waitRelayConnector(relayUrl, 25_000);
73
+ printPairingToTerminal(relayUrl, refreshed, {
74
+ bridge: health.ok,
75
+ cdp: cdpOk && health.cdp,
76
+ connector: connectorOk,
77
+ });
78
+ }
79
+ async function cmdStop() {
80
+ stopBridge();
81
+ console.log('Bridge остановлен');
82
+ }
83
+ async function cmdInit(pathArg) {
84
+ const target = pathArg?.trim() || process.cwd();
85
+ const abs = resolve(target);
86
+ if (!isValidRepoRoot(abs)) {
87
+ console.error(`Папка не похожа на CursorConnect: ${abs}\n` +
88
+ 'Нужен каталог с bridge/package.json (клон репозитория + npm install).');
89
+ process.exit(1);
90
+ }
91
+ saveInstallConfig(abs);
92
+ console.log(`Сохранено: ${configFilePath()}`);
93
+ console.log(`repoRoot: ${abs}`);
94
+ console.log('\nДальше: cursorconnect start');
95
+ }
96
+ async function cmdStatus() {
97
+ const identity = loadPairingIdentity();
98
+ let repoRoot = '';
99
+ try {
100
+ repoRoot = resolveRepoRoot();
101
+ }
102
+ catch {
103
+ /* optional */
104
+ }
105
+ const env = loadEnvFile(repoRoot ? join(repoRoot, 'bridge', '.env') : '');
106
+ const relayUrl = env.RELAY_URL ?? '';
107
+ console.log('Bridge running:', isBridgeRunning());
108
+ if (!identity) {
109
+ console.log('Identity не найден — запустите: npm run connect:start');
110
+ return;
111
+ }
112
+ let connector = false;
113
+ let cdp = false;
114
+ let bridgeOk = false;
115
+ try {
116
+ const h = await fetch('http://127.0.0.1:3847/health');
117
+ const j = (await h.json());
118
+ bridgeOk = Boolean(j.ok);
119
+ cdp = Boolean(j.cdp);
120
+ }
121
+ catch {
122
+ /* offline */
123
+ }
124
+ if (relayUrl) {
125
+ try {
126
+ const res = await fetch(`${relayUrl.replace(/\/$/, '')}/health`);
127
+ const j = (await res.json());
128
+ connector = Boolean(j.connector);
129
+ }
130
+ catch {
131
+ /* ignore */
132
+ }
133
+ }
134
+ if (relayUrl) {
135
+ printPairingToTerminal(relayUrl, identity, {
136
+ bridge: bridgeOk,
137
+ cdp,
138
+ connector,
139
+ });
140
+ }
141
+ else {
142
+ console.log('Room:', identity.roomId);
143
+ console.log('Код:', identity.pairingCode);
144
+ }
145
+ }
146
+ async function main() {
147
+ const argv = process.argv.slice(2);
148
+ const cmd = argv[0] ?? 'start';
149
+ const rest = argv.slice(1);
150
+ switch (cmd) {
151
+ case 'start':
152
+ await cmdStart(rest);
153
+ break;
154
+ case 'stop':
155
+ await cmdStop();
156
+ break;
157
+ case 'status':
158
+ await cmdStatus();
159
+ break;
160
+ case 'init':
161
+ await cmdInit(rest[0]);
162
+ break;
163
+ default:
164
+ console.log(`Usage: cursorconnect <command>
165
+
166
+ init [path] — путь к клону CursorConnect (~/.cursorconnect/config.json)
167
+ start [--restart-cursor|-r] — bridge в фоне + QR/код
168
+ stop — остановить bridge
169
+ status — код, QR, health
170
+
171
+ Глобально: npm install -g cursorconnect
172
+ В репо: npm run connect:start`);
173
+ process.exit(cmd === 'help' ? 0 : 1);
174
+ }
175
+ }
176
+ main().catch((e) => {
177
+ console.error(e);
178
+ process.exit(1);
179
+ });
package/dist/launch.js ADDED
@@ -0,0 +1,188 @@
1
+ import { execSync, spawn, spawnSync } from 'child_process';
2
+ import { askYesNo } from './ask.js';
3
+ import { existsSync, openSync, readFileSync, writeFileSync } from 'fs';
4
+ import { BRIDGE_LOG_FILE, BRIDGE_PID_FILE } from './paths.js';
5
+ import { resolveRepoRoot } from './repo-root.js';
6
+ export function isBridgeRunning() {
7
+ if (!existsSync(BRIDGE_PID_FILE))
8
+ return false;
9
+ const pid = parseInt(readFileSync(BRIDGE_PID_FILE, 'utf-8').trim(), 10);
10
+ if (!Number.isFinite(pid))
11
+ return false;
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function stopBridge() {
21
+ if (!existsSync(BRIDGE_PID_FILE))
22
+ return;
23
+ const pid = parseInt(readFileSync(BRIDGE_PID_FILE, 'utf-8').trim(), 10);
24
+ try {
25
+ process.kill(pid, 'SIGTERM');
26
+ }
27
+ catch {
28
+ /* already dead */
29
+ }
30
+ try {
31
+ writeFileSync(BRIDGE_PID_FILE, '');
32
+ }
33
+ catch {
34
+ /* ignore */
35
+ }
36
+ }
37
+ export function startBridge(env) {
38
+ stopBridge();
39
+ const logFd = openLogAppend();
40
+ const child = spawn('npm', ['run', 'start', '-w', 'bridge'], {
41
+ cwd: resolveRepoRoot(),
42
+ env: { ...process.env, ...env },
43
+ detached: true,
44
+ stdio: ['ignore', logFd, logFd],
45
+ });
46
+ child.unref();
47
+ writeFileSync(BRIDGE_PID_FILE, String(child.pid));
48
+ return child;
49
+ }
50
+ function openLogAppend() {
51
+ return openSync(BRIDGE_LOG_FILE, 'a');
52
+ }
53
+ export async function waitBridgeHealth(maxMs = 30_000) {
54
+ const deadline = Date.now() + maxMs;
55
+ while (Date.now() < deadline) {
56
+ try {
57
+ const res = await fetch('http://127.0.0.1:3847/health');
58
+ if (res.ok)
59
+ return (await res.json());
60
+ }
61
+ catch {
62
+ /* retry */
63
+ }
64
+ await sleep(500);
65
+ }
66
+ return { ok: false, cdp: false };
67
+ }
68
+ export async function waitRelayConnector(relayUrl, maxMs = 45_000) {
69
+ const base = relayUrl.replace(/\/$/, '');
70
+ const deadline = Date.now() + maxMs;
71
+ while (Date.now() < deadline) {
72
+ try {
73
+ const res = await fetch(`${base}/health`);
74
+ if (res.ok) {
75
+ const h = (await res.json());
76
+ if (h.connector)
77
+ return true;
78
+ }
79
+ }
80
+ catch {
81
+ /* retry */
82
+ }
83
+ await sleep(800);
84
+ }
85
+ return false;
86
+ }
87
+ export function isCursorRunning() {
88
+ try {
89
+ execSync('pgrep -x Cursor', { stdio: 'ignore' });
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ export async function quitCursor(maxWaitMs = 20_000) {
97
+ if (!isCursorRunning())
98
+ return true;
99
+ console.log('[cursorconnect] Закрываю Cursor…');
100
+ spawnSync('osascript', ['-e', 'tell application "Cursor" to quit'], {
101
+ stdio: 'ignore',
102
+ });
103
+ const deadline = Date.now() + maxWaitMs;
104
+ while (Date.now() < deadline) {
105
+ await sleep(400);
106
+ if (!isCursorRunning())
107
+ return true;
108
+ }
109
+ console.log('[cursorconnect] Принудительное завершение Cursor…');
110
+ try {
111
+ spawnSync('killall', ['Cursor'], { stdio: 'ignore' });
112
+ }
113
+ catch {
114
+ /* ignore */
115
+ }
116
+ await sleep(800);
117
+ return !isCursorRunning();
118
+ }
119
+ const CDP_LAUNCH_CMD = 'open -a Cursor --args --remote-debugging-port=9222';
120
+ export function printManualCdpInstructions() {
121
+ console.log('\n[cursorconnect] Запустите Cursor с CDP вручную:');
122
+ console.log(` ${CDP_LAUNCH_CMD}`);
123
+ console.log(' (сначала Cmd+Q, если Cursor уже открыт)\n');
124
+ }
125
+ export function launchCursorWithCdp() {
126
+ spawn('open', ['-a', 'Cursor', '--args', '--remote-debugging-port=9222'], {
127
+ detached: true,
128
+ stdio: 'ignore',
129
+ }).unref();
130
+ }
131
+ async function waitCdp(cdpUrl, attempts = 40) {
132
+ for (let i = 0; i < attempts; i++) {
133
+ if (await cdpReachable(cdpUrl))
134
+ return true;
135
+ await sleep(500);
136
+ }
137
+ return false;
138
+ }
139
+ export async function ensureCursorCdp(opts = {}) {
140
+ const cdpUrl = opts.cdpUrl ?? 'http://127.0.0.1:9222';
141
+ if (await cdpReachable(cdpUrl)) {
142
+ return true;
143
+ }
144
+ const running = isCursorRunning();
145
+ if (!running) {
146
+ console.log('[cursorconnect] Cursor не запущен — старт с --remote-debugging-port=9222…');
147
+ launchCursorWithCdp();
148
+ return waitCdp(cdpUrl, 50);
149
+ }
150
+ // Cursor запущен, CDP недоступен
151
+ let shouldRestart = Boolean(opts.restartCursor);
152
+ if (!shouldRestart) {
153
+ if (!process.stdin.isTTY) {
154
+ console.warn('[cursorconnect] Cursor без CDP (нет интерактивного терминала).');
155
+ printManualCdpInstructions();
156
+ return false;
157
+ }
158
+ shouldRestart = await askYesNo('Cursor запущен без CDP. Перезапустить с --remote-debugging-port=9222? (y/n): ');
159
+ }
160
+ if (!shouldRestart) {
161
+ printManualCdpInstructions();
162
+ return false;
163
+ }
164
+ const quitOk = await quitCursor();
165
+ if (!quitOk) {
166
+ console.warn('[cursorconnect] Не удалось закрыть Cursor.');
167
+ printManualCdpInstructions();
168
+ return false;
169
+ }
170
+ console.log('[cursorconnect] Запуск Cursor с --remote-debugging-port=9222…');
171
+ launchCursorWithCdp();
172
+ if (await waitCdp(cdpUrl, 50))
173
+ return true;
174
+ printManualCdpInstructions();
175
+ return false;
176
+ }
177
+ async function cdpReachable(cdpUrl) {
178
+ try {
179
+ const res = await fetch(`${cdpUrl.replace(/\/$/, '')}/json/version`);
180
+ return res.ok;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ }
186
+ function sleep(ms) {
187
+ return new Promise((r) => setTimeout(r, ms));
188
+ }
@@ -0,0 +1,80 @@
1
+ import { randomBytes, randomUUID } from 'crypto';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { homedir, hostname } from 'os';
4
+ import { join } from 'path';
5
+ const DIR = join(homedir(), '.cursorconnect');
6
+ 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
+ export function pairingIdentityPath() {
12
+ return FILE;
13
+ }
14
+ export function loadPairingIdentity() {
15
+ if (!existsSync(FILE))
16
+ return null;
17
+ try {
18
+ const raw = readFileSync(FILE, 'utf-8');
19
+ const data = JSON.parse(raw);
20
+ if (!data.roomId || !data.clientToken)
21
+ return null;
22
+ return data;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export function ensurePairingIdentity(machineLabel) {
29
+ const existing = loadPairingIdentity();
30
+ if (existing) {
31
+ return refreshPairingCode(existing, machineLabel);
32
+ }
33
+ const now = new Date().toISOString();
34
+ const created = {
35
+ roomId: randomUUID(),
36
+ clientToken: randomBytes(32).toString('hex'),
37
+ machineLabel: machineLabel?.trim() || defaultMachineLabel(),
38
+ pairingCode: formatPairingCode(),
39
+ pairingCodeExpiresAt: Date.now() + 10 * 60_000,
40
+ createdAt: now,
41
+ updatedAt: now,
42
+ };
43
+ savePairingIdentity(created);
44
+ return created;
45
+ }
46
+ export function refreshPairingCode(identity, machineLabel) {
47
+ const next = {
48
+ ...identity,
49
+ machineLabel: machineLabel?.trim() || identity.machineLabel,
50
+ pairingCode: formatPairingCode(),
51
+ pairingCodeExpiresAt: Date.now() + 10 * 60_000,
52
+ updatedAt: new Date().toISOString(),
53
+ };
54
+ savePairingIdentity(next);
55
+ return next;
56
+ }
57
+ function defaultMachineLabel() {
58
+ return hostname().replace(/\.local$/i, '') || 'Mac';
59
+ }
60
+ export function savePairingIdentity(identity) {
61
+ mkdirSync(DIR, { recursive: true, mode: 0o700 });
62
+ writeFileSync(FILE, JSON.stringify(identity, null, 2), { mode: 0o600 });
63
+ try {
64
+ chmodSync(FILE, 0o600);
65
+ }
66
+ catch {
67
+ /* ignore */
68
+ }
69
+ }
70
+ export function buildPairUri(relayUrl, identity) {
71
+ const base = relayUrl.replace(/\/$/, '');
72
+ const q = new URLSearchParams({
73
+ v: '1',
74
+ relay: base,
75
+ room: identity.roomId,
76
+ token: identity.clientToken,
77
+ label: identity.machineLabel,
78
+ });
79
+ return `cursorconnect://pair?${q.toString()}`;
80
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,6 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ export const CURSORCONNECT_DIR = join(homedir(), '.cursorconnect');
4
+ export const IDENTITY_FILE = join(CURSORCONNECT_DIR, 'identity.json');
5
+ export const BRIDGE_PID_FILE = join(CURSORCONNECT_DIR, 'bridge.pid');
6
+ export const BRIDGE_LOG_FILE = join(CURSORCONNECT_DIR, 'bridge.log');
@@ -0,0 +1,31 @@
1
+ import qrTerminal from 'qrcode-terminal';
2
+ import { buildPairUri } from './pairing-identity.js';
3
+ export function printPairingToTerminal(relayUrl, identity, status) {
4
+ const pairUri = buildPairUri(relayUrl, identity);
5
+ const expiresIn = Math.max(0, Math.floor((identity.pairingCodeExpiresAt - Date.now()) / 1000));
6
+ const ok = status.bridge && status.cdp && status.connector;
7
+ console.log('\n────────────────────────────────────────');
8
+ console.log(` Mac: ${identity.machineLabel}`);
9
+ console.log(` Relay: ${relayUrl}`);
10
+ console.log('');
11
+ console.log(' Код в приложении (6 цифр):');
12
+ console.log(`\n ${identity.pairingCode}\n`);
13
+ console.log(' QR (скан в CursorConnect → Подключить Mac):\n');
14
+ qrTerminal.generate(pairUri, { small: true });
15
+ console.log('');
16
+ console.log(` Ссылка (вставить вручную):\n ${pairUri}\n`);
17
+ console.log(` Код действует ~${Math.floor(expiresIn / 60)} мин`);
18
+ console.log(` Статус: bridge ${dot(status.bridge)} CDP ${dot(status.cdp)} relay ${dot(status.connector)}`);
19
+ if (!ok) {
20
+ console.log(' Подождите 10–20 с и снова: npm run connect:status');
21
+ }
22
+ else {
23
+ console.log(' Готово — подключайте телефон.');
24
+ }
25
+ console.log('────────────────────────────────────────');
26
+ console.log(' Bridge в фоне. Остановка: npm run connect:stop');
27
+ console.log(' Консоль можно закрыть.\n');
28
+ }
29
+ function dot(ok) {
30
+ return ok ? '✓' : '…';
31
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const CONFIG_DIR = join(homedir(), '.cursorconnect');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+ export function loadInstallConfig() {
8
+ if (!existsSync(CONFIG_FILE))
9
+ return null;
10
+ try {
11
+ const data = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
12
+ if (!data.repoRoot || !isValidRepoRoot(data.repoRoot))
13
+ return null;
14
+ return { repoRoot: resolve(data.repoRoot) };
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function saveInstallConfig(repoRoot) {
21
+ const abs = resolve(repoRoot);
22
+ if (!isValidRepoRoot(abs)) {
23
+ throw new Error(`Не найден bridge в ${abs} (ожидается …/bridge/package.json)`);
24
+ }
25
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
26
+ writeFileSync(CONFIG_FILE, JSON.stringify({ repoRoot: abs }, null, 2), { mode: 0o600 });
27
+ }
28
+ export function isValidRepoRoot(dir) {
29
+ return existsSync(join(dir, 'bridge', 'package.json'));
30
+ }
31
+ /** Monorepo dev: connect package inside repo */
32
+ function devRepoRoot() {
33
+ const here = dirname(fileURLToPath(import.meta.url));
34
+ const candidate = resolve(here, '..', '..');
35
+ return isValidRepoRoot(candidate) ? candidate : null;
36
+ }
37
+ function findRepoFromCwd() {
38
+ let dir = process.cwd();
39
+ for (let i = 0; i < 12; i++) {
40
+ if (isValidRepoRoot(dir))
41
+ return resolve(dir);
42
+ const parent = dirname(dir);
43
+ if (parent === dir)
44
+ break;
45
+ dir = parent;
46
+ }
47
+ return null;
48
+ }
49
+ export function resolveRepoRoot() {
50
+ const fromEnv = process.env.CURSORCONNECT_ROOT?.trim();
51
+ if (fromEnv && isValidRepoRoot(fromEnv))
52
+ return resolve(fromEnv);
53
+ const fromConfig = loadInstallConfig();
54
+ if (fromConfig)
55
+ return fromConfig.repoRoot;
56
+ const fromCwd = findRepoFromCwd();
57
+ if (fromCwd)
58
+ return fromCwd;
59
+ const fromDev = devRepoRoot();
60
+ if (fromDev)
61
+ return fromDev;
62
+ throw new Error('Не найден CursorConnect (bridge).\n' +
63
+ ' npm install -g cursorconnect\n' +
64
+ ' git clone <repo> && cd CursorConnect && npm install\n' +
65
+ ' cursorconnect init /path/to/CursorConnect\n' +
66
+ 'или: export CURSORCONNECT_ROOT=/path/to/CursorConnect');
67
+ }
68
+ export function configFilePath() {
69
+ return CONFIG_FILE;
70
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "cursorconnect",
3
+ "version": "0.1.0",
4
+ "description": "Mac CLI: CursorConnect bridge, relay pairing (QR + code), Cursor CDP",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "cursorconnect": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "dev": "tsx src/index.ts",
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "cursor",
24
+ "cursorconnect",
25
+ "cursor-ide",
26
+ "bridge",
27
+ "relay"
28
+ ],
29
+ "dependencies": {
30
+ "qrcode-terminal": "^0.12.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "@types/qrcode-terminal": "^0.12.2",
35
+ "tsx": "^4.19.0",
36
+ "typescript": "^5.9.2"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }