@i14ks/ccv 0.6.2 → 0.7.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.
Files changed (26) hide show
  1. package/bundle/ccv.mjs +741 -129
  2. package/package.json +1 -1
  3. package/web/assets/{codemirror-02hKfv-c.js → codemirror-4gviWJRD.js} +1 -1
  4. package/web/assets/{index-BybHqs37.js → index-B-8FWzpe.js} +1 -1
  5. package/web/assets/{index-DRMnPfA3.js → index-BiBGEAPz.js} +1 -1
  6. package/web/assets/{index-Bm6PUpXb.js → index-BjCwOpLW.js} +1 -1
  7. package/web/assets/{index-BBkJ4cEk.js → index-BleqvvQk.js} +1 -1
  8. package/web/assets/{index-BxcnR7Nj.js → index-Bv8vIAVO.js} +1 -1
  9. package/web/assets/index-Bzq-v0Dg.js +134 -0
  10. package/web/assets/{index-sQrmYdbi.css → index-CE1G7QxT.css} +1 -1
  11. package/web/assets/{index-VSDVirVT.js → index-CJo5aeRL.js} +1 -1
  12. package/web/assets/{index-C-8irjwh.js → index-CKz5fHXP.js} +1 -1
  13. package/web/assets/{index-BC8gj6P7.js → index-CR_zbJ-Z.js} +1 -1
  14. package/web/assets/{index-C2nhZ-ID.js → index-CYwf9t3p.js} +1 -1
  15. package/web/assets/{index-DZXdWq9L.js → index-C_LuD9HT.js} +1 -1
  16. package/web/assets/{index-ChKrBnaf.js → index-Cid3pp7b.js} +1 -1
  17. package/web/assets/{index-Clyg5z5Z.js → index-CkSGxu2i.js} +1 -1
  18. package/web/assets/{index-KEWrG50i.js → index-CrX9ldma.js} +1 -1
  19. package/web/assets/{index-BorlqgNX.js → index-Depy6k3N.js} +1 -1
  20. package/web/assets/{index-zDoB4KM7.js → index-DklW2LtN.js} +1 -1
  21. package/web/assets/{index-B5hgDwts.js → index-DmEjP2AP.js} +1 -1
  22. package/web/assets/{index-DpZcQOqI.js → index-fWVyTiSX.js} +1 -1
  23. package/web/assets/{index-D0UkSEon.js → index-hElc3lLb.js} +1 -1
  24. package/web/assets/{index-i-F6wQDa.js → index-kMdLDVpb.js} +1 -1
  25. package/web/index.html +2 -2
  26. package/web/assets/index-DFJbTCVa.js +0 -129
package/bundle/ccv.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // dist/ccv.js
4
4
  import { spawn as spawn2 } from "node:child_process";
5
- import { readFile as readFile11 } from "node:fs/promises";
6
- import path16 from "node:path";
5
+ import { readFile as readFile13 } from "node:fs/promises";
6
+ import path18 from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseArgs } from "node:util";
9
9
 
@@ -146,7 +146,7 @@ function envDefaults() {
146
146
  // ../host/dist/server.js
147
147
  import { createServer } from "node:http";
148
148
  import { createReadStream, existsSync as existsSync3, statSync as statSync3 } from "node:fs";
149
- import path15 from "node:path";
149
+ import path17 from "node:path";
150
150
  import { homedir as homedir5 } from "node:os";
151
151
  import { randomUUID as randomUUID3 } from "node:crypto";
152
152
  import { WebSocketServer } from "ws";
@@ -2246,7 +2246,11 @@ function contentToText2(content) {
2246
2246
 
2247
2247
  // ../host/dist/git.js
2248
2248
  import { execFile as execFile2 } from "node:child_process";
2249
+ import { mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
2250
+ import { tmpdir } from "node:os";
2251
+ import path6 from "node:path";
2249
2252
  var TIMEOUT_MS = 2e4;
2253
+ var NET_TIMEOUT_MS = 5 * 6e4;
2250
2254
  var MAX_BUFFER = 32 * 1024 * 1024;
2251
2255
  var LOG_LIMIT = 150;
2252
2256
  var UNPUSHED_LIMIT = 2e3;
@@ -2255,6 +2259,8 @@ var MAX_FILE_PATCH = 8e4;
2255
2259
  var UNIT = "";
2256
2260
  var RECORD = "";
2257
2261
  var SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._/@^~-]*$/;
2262
+ var SAFE_URL = /^(?:https?:\/\/|ssh:\/\/|git:\/\/|[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:)[^\s]+$/;
2263
+ var CANDIDATE_LIMIT = 2e3;
2258
2264
  var GitRepo = class {
2259
2265
  cwd;
2260
2266
  constructor(cwd) {
@@ -2271,11 +2277,13 @@ var GitRepo = class {
2271
2277
  const blank = {
2272
2278
  available: false,
2273
2279
  message: "",
2280
+ noRepo: false,
2274
2281
  branch: "",
2275
2282
  head: "",
2276
2283
  detached: false,
2277
2284
  upstream: "",
2278
2285
  hasRemote: false,
2286
+ remoteUrl: "",
2279
2287
  ahead: 0,
2280
2288
  behind: 0,
2281
2289
  changed: [],
@@ -2285,7 +2293,8 @@ var GitRepo = class {
2285
2293
  try {
2286
2294
  await this.#run(["rev-parse", "--is-inside-work-tree"]);
2287
2295
  } catch (error) {
2288
- return { ...blank, message: describe2(error) };
2296
+ const runnable2 = await this.#run(["--version"]).then(() => true, () => false);
2297
+ return { ...blank, message: describe2(error), noRepo: runnable2 };
2289
2298
  }
2290
2299
  const branchRaw = await this.#soft(["rev-parse", "--abbrev-ref", "HEAD"]);
2291
2300
  const detached = branchRaw === "HEAD" || branchRaw === "";
@@ -2301,11 +2310,14 @@ var GitRepo = class {
2301
2310
  return {
2302
2311
  available: true,
2303
2312
  message: "",
2313
+ // Репозиторий есть — заводить нечего.
2314
+ noRepo: false,
2304
2315
  branch: detached ? "" : branchRaw,
2305
2316
  head: await this.#soft(["rev-parse", "HEAD"]),
2306
2317
  detached,
2307
2318
  upstream,
2308
2319
  hasRemote,
2320
+ remoteUrl: await this.#soft(["remote", "get-url", "origin"]),
2309
2321
  ahead: Number(ahead) || 0,
2310
2322
  behind: Number(behind) || 0,
2311
2323
  changed: parseStatus(await this.#soft(["status", "--porcelain=v1"])),
@@ -2432,12 +2444,11 @@ var GitRepo = class {
2432
2444
  async checkIgnored(paths) {
2433
2445
  if (paths.length === 0)
2434
2446
  return /* @__PURE__ */ new Set();
2435
- const raw = await this.#run(
2436
- ["check-ignore", "--stdin", "-z"],
2447
+ const raw = await this.#run(["check-ignore", "--stdin", "-z"], {
2437
2448
  // `-z` переводит на NUL и вход тоже — иначе путь с переводом строки в
2438
2449
  // имени разъехался бы на две записи.
2439
- paths.join("\0")
2440
- ).catch(() => "");
2450
+ input: paths.join("\0")
2451
+ }).catch(() => "");
2441
2452
  return new Set(raw.split("\0").filter(Boolean));
2442
2453
  }
2443
2454
  /**
@@ -2455,24 +2466,195 @@ var GitRepo = class {
2455
2466
  const out = await this.#run(["checkout", requireRef(target), "--"]);
2456
2467
  return out || `\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043D\u0430 ${target}`;
2457
2468
  }
2469
+ /**
2470
+ * Отправить текущую ветку.
2471
+ *
2472
+ * Ветка берётся здесь, а не приходит параметром: отправить можно только то,
2473
+ * на чём стоит HEAD, и принимать её снаружи значило бы дать окну отправить
2474
+ * не ту ветку, которую человек видит перед собой.
2475
+ *
2476
+ * Первый push ветки добавляет `--set-upstream`: без него git отправит
2477
+ * коммиты, но связь с удалённой веткой не заведёт, и панель после успешной
2478
+ * отправки продолжит показывать «не отправлено». Дальше аргументов нет
2479
+ * вовсе — `git push` сам знает, куда, и уважает `push.default` из настроек
2480
+ * пользователя.
2481
+ */
2482
+ async push(token) {
2483
+ const branch = await this.#soft(["rev-parse", "--abbrev-ref", "HEAD"]);
2484
+ if (branch === "" || branch === "HEAD") {
2485
+ throw new Error("HEAD \u043E\u0442\u0434\u0435\u043B\u0451\u043D \u043E\u0442 \u0432\u0435\u0442\u043A\u0438 \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u0441\u044C \u043D\u0430 \u0432\u0435\u0442\u043A\u0443");
2486
+ }
2487
+ const upstream = await this.#soft([
2488
+ "rev-parse",
2489
+ "--abbrev-ref",
2490
+ "--symbolic-full-name",
2491
+ "@{upstream}"
2492
+ ]);
2493
+ const out = await this.#run(upstream ? ["push"] : ["push", "--set-upstream", "origin", branch], { token, timeout: NET_TIMEOUT_MS });
2494
+ return out || `\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E: ${branch}`;
2495
+ }
2496
+ /**
2497
+ * Забрать с удалённого репозитория.
2498
+ *
2499
+ * `merge: false` — только `fetch`: счётчики «отстаём/опережаем» обновятся, а
2500
+ * рабочее дерево останется каким было. Это безопасное действие, и оно же
2501
+ * единственный способ узнать, что на remote что-то появилось.
2502
+ *
2503
+ * `merge: true` — `pull --ff-only`. Только перемотка: настоящее слияние
2504
+ * создаёт коммит и умеет оставить дерево в конфликте, а разбирать конфликт
2505
+ * из панели истории нечем. Когда перемотка невозможна, git отказывается сам,
2506
+ * и его текст объясняет это лучше нашего.
2507
+ */
2508
+ async pull(merge, token) {
2509
+ if (!merge) {
2510
+ const out2 = await this.#run(["fetch", "--all", "--prune"], {
2511
+ token,
2512
+ timeout: NET_TIMEOUT_MS
2513
+ });
2514
+ return out2 || "\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0437\u0430\u0431\u0440\u0430\u043D\u044B \u2014 \u0438\u0441\u0442\u043E\u0440\u0438\u044F remote \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430";
2515
+ }
2516
+ const out = await this.#run(["pull", "--ff-only"], { token, timeout: NET_TIMEOUT_MS });
2517
+ return out || "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E";
2518
+ }
2519
+ /**
2520
+ * Прописать `origin`.
2521
+ *
2522
+ * `set-url`, если он уже есть: «добавить существующее» — обычная причина
2523
+ * сюда попасть (адрес поменялся, репозиторий переехал), и отвечать на неё
2524
+ * ошибкой git «remote origin already exists» значило бы требовать удалить
2525
+ * remote где-то ещё, а такого места в приложении нет.
2526
+ */
2527
+ async setRemote(url2) {
2528
+ const target = requireUrl(url2);
2529
+ const existing = await this.#soft(["remote", "get-url", "origin"]);
2530
+ await this.#run(existing ? ["remote", "set-url", "origin", target] : ["remote", "add", "origin", target]);
2531
+ return existing ? `origin \u0442\u0435\u043F\u0435\u0440\u044C ${target}` : `origin \u2014 ${target}`;
2532
+ }
2533
+ /**
2534
+ * Что можно положить в первый коммит.
2535
+ *
2536
+ * `--others --cached` — файлы, которых нет в истории: неотслеживаемые и уже
2537
+ * добавленные в индекс. `--exclude-standard` уважает существующий
2538
+ * `.gitignore`: если человек уже описал, что в репозиторий не нужно,
2539
+ * спрашивать его об этом второй раз галочками незачем.
2540
+ *
2541
+ * `-z` — потому что путь с переводом строки в имени иначе разъехался бы на
2542
+ * две записи, и в коммит попал бы файл с выдуманным именем.
2543
+ */
2544
+ async candidates() {
2545
+ const args = ["ls-files", "-z", "--others", "--cached", "--exclude-standard"];
2546
+ const inside = await this.#run(["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false);
2547
+ const raw = inside ? await this.#soft(args) : await this.#withoutRepo(args);
2548
+ const all = raw.split("\0").filter(Boolean);
2549
+ return { files: all.slice(0, CANDIDATE_LIMIT), truncated: all.length > CANDIDATE_LIMIT };
2550
+ }
2551
+ /**
2552
+ * Спросить git о каталоге, который репозиторием ещё не стал.
2553
+ *
2554
+ * Нужно ровно для одного: показать список файлов до публикации проекта, у
2555
+ * которого нет даже `.git`. Просто позвать `git init` заранее нельзя —
2556
+ * открытие окна не должно оставлять после себя репозиторий, если человек
2557
+ * передумал и закрыл его.
2558
+ *
2559
+ * Поэтому репозиторий заводится пустой и во временном каталоге, а рабочим
2560
+ * деревом ему назначается проект: `--exclude-standard` при этом читает
2561
+ * настоящий `.gitignore` проекта, и список получается тот же, что и после
2562
+ * `git init`. Свой обход каталога дал бы другой — правила игнорирования
2563
+ * пришлось бы разбирать самим, и они разошлись бы с git ровно там, где это
2564
+ * важнее всего (см. `checkIgnored`).
2565
+ */
2566
+ async #withoutRepo(args) {
2567
+ const dir = await mkdtemp(path6.join(tmpdir(), "ccv-git-"));
2568
+ try {
2569
+ await this.#run(["init", "--bare", "--quiet", dir]);
2570
+ return await this.#soft([`--git-dir=${dir}`, `--work-tree=${this.cwd}`, ...args]);
2571
+ } finally {
2572
+ await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
2573
+ }
2574
+ }
2575
+ /**
2576
+ * Первый коммит: дописать `.gitignore`, добавить выбранное, закоммитить.
2577
+ *
2578
+ * Ровно тот порядок, что и в «Publish to GitHub» у VS Code, и он важен:
2579
+ * `.gitignore` должен попасть в тот же коммит, что и файлы, иначе первый же
2580
+ * `git status` после публикации покажет его неотслеживаемым.
2581
+ *
2582
+ * Пути уходят через stdin (`--pathspec-from-file=-`), а не аргументами: их
2583
+ * тут могут быть тысячи, и в командную строку Windows они не влезут. `--nul`
2584
+ * к тому же снимает вопрос о путях с пробелами и переводами строк, а заодно
2585
+ * лишает строку магии pathspec — `:(exclude)` из браузера здесь не сработает.
2586
+ */
2587
+ async commit(message, include, ignore) {
2588
+ const text = message.trim();
2589
+ if (!text)
2590
+ throw new Error("\u041F\u0443\u0441\u0442\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u0430");
2591
+ const paths = include.map(requirePath);
2592
+ if (paths.length === 0)
2593
+ throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043E \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430 \u0434\u043B\u044F \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430");
2594
+ const ignored = ignore.map(requirePath);
2595
+ if (ignored.length > 0) {
2596
+ await this.#appendGitignore(ignored);
2597
+ paths.push(".gitignore");
2598
+ }
2599
+ await this.#run(["add", "--pathspec-from-file=-", "--pathspec-file-nul"], {
2600
+ input: paths.join("\0")
2601
+ });
2602
+ return await this.#run(["commit", "-m", text]);
2603
+ }
2604
+ /**
2605
+ * Дописать в `.gitignore` то, что не выбрали.
2606
+ *
2607
+ * Дописать, а не переписать: файл мог быть уже — с правилами, которые сюда
2608
+ * не приходили (их пути `--exclude-standard` из списка и убрал), и затереть
2609
+ * их значило бы вернуть в репозиторий ровно то, что человек однажды из него
2610
+ * исключил. Повторы отсеиваются, чтобы файл не рос от каждой публикации.
2611
+ */
2612
+ async #appendGitignore(paths) {
2613
+ const file = path6.join(this.cwd, ".gitignore");
2614
+ const existing = await readFile4(file, "utf8").catch(() => "");
2615
+ const known = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
2616
+ const lines = paths.map(ignorePattern).filter((line) => !known.has(line));
2617
+ if (lines.length === 0)
2618
+ return;
2619
+ const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
2620
+ await writeFile2(file, `${existing}${prefix}${lines.join("\n")}
2621
+ `, "utf8");
2622
+ }
2458
2623
  /** Команда, чей провал — обычное дело (нет upstream, нет коммитов, detached). */
2459
2624
  async #soft(args) {
2460
2625
  return this.#run(args).catch(() => "");
2461
2626
  }
2462
- /** `input` — то, что команда читает со stdin; без него stdin сразу закрывается. */
2463
- #run(args, input) {
2627
+ /**
2628
+ * Запустить git.
2629
+ *
2630
+ * `input` — то, что команда читает со stdin; без него stdin сразу
2631
+ * закрывается. `token` — учётные данные GitHub на одну команду (см.
2632
+ * `credentialArgs`). `timeout` переопределяется у тех команд, что ходят в
2633
+ * сеть: обычный потолок для них слишком мал.
2634
+ */
2635
+ #run(args, options2 = {}) {
2636
+ const token = options2.token ?? "";
2464
2637
  return new Promise((resolve, reject) => {
2465
2638
  const child = execFile2(
2466
2639
  "git",
2467
2640
  // core.quotepath=false — иначе кириллические пути приезжают в виде
2468
2641
  // «\320\277\321\200…» и в панели читаются как мусор.
2469
- ["-c", "core.quotepath=false", ...args],
2642
+ ["-c", "core.quotepath=false", ...credentialArgs(token), ...args],
2470
2643
  {
2471
2644
  cwd: this.cwd,
2472
- timeout: TIMEOUT_MS,
2645
+ timeout: options2.timeout ?? TIMEOUT_MS,
2473
2646
  maxBuffer: MAX_BUFFER,
2474
2647
  windowsHide: true,
2475
- encoding: "utf8"
2648
+ encoding: "utf8",
2649
+ env: {
2650
+ ...process.env,
2651
+ // Токен уходит окружением, а не аргументом: аргументы видны в
2652
+ // списке процессов любому пользователю машины.
2653
+ ...token ? { CCV_GIT_TOKEN: token } : {},
2654
+ // Хост не сидит в терминале: спроси git пароль — и он будет ждать
2655
+ // ответа от stdin, которого никто не даст, до самого таймаута.
2656
+ GIT_TERMINAL_PROMPT: "0"
2657
+ }
2476
2658
  },
2477
2659
  (error, stdout, stderr) => {
2478
2660
  if (!error)
@@ -2486,10 +2668,20 @@ var GitRepo = class {
2486
2668
  );
2487
2669
  child.stdin?.on("error", () => {
2488
2670
  });
2489
- child.stdin?.end(input ?? "");
2671
+ child.stdin?.end(options2.input ?? "");
2490
2672
  });
2491
2673
  }
2492
2674
  };
2675
+ function credentialArgs(token) {
2676
+ if (!token)
2677
+ return [];
2678
+ return [
2679
+ "-c",
2680
+ "credential.helper=",
2681
+ "-c",
2682
+ 'credential.helper=!f() { echo username=x-access-token; echo "password=$CCV_GIT_TOKEN"; }; f'
2683
+ ];
2684
+ }
2493
2685
  function requireRef(value) {
2494
2686
  const trimmed = value.trim();
2495
2687
  if (!SAFE_REF.test(trimmed) || trimmed.includes("..")) {
@@ -2497,6 +2689,23 @@ function requireRef(value) {
2497
2689
  }
2498
2690
  return trimmed;
2499
2691
  }
2692
+ function requireUrl(value) {
2693
+ const trimmed = value.trim();
2694
+ if (!SAFE_URL.test(trimmed)) {
2695
+ throw new Error(`\u041D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0430\u0434\u0440\u0435\u0441 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F: \xAB${value}\xBB`);
2696
+ }
2697
+ return trimmed;
2698
+ }
2699
+ function requirePath(value) {
2700
+ const trimmed = value.trim().replace(/\\/g, "/");
2701
+ const bad = trimmed === "" || trimmed.startsWith("-") || trimmed.startsWith("/") || trimmed.includes(":") || trimmed.split("/").includes("..");
2702
+ if (bad)
2703
+ throw new Error(`\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u043F\u0443\u0442\u044C: \xAB${value}\xBB`);
2704
+ return trimmed;
2705
+ }
2706
+ function ignorePattern(relPath) {
2707
+ return `/${relPath.replace(/[\\*?[\]]/g, (char) => `\\${char}`)}`;
2708
+ }
2500
2709
  function describe2(error) {
2501
2710
  return error instanceof Error ? error.message : String(error);
2502
2711
  }
@@ -2607,9 +2816,9 @@ function parsePatch(raw) {
2607
2816
  body = "";
2608
2817
  }
2609
2818
  total += body.length;
2610
- const path17 = status === "deleted" ? oldPath : newPath || oldPath;
2819
+ const path19 = status === "deleted" ? oldPath : newPath || oldPath;
2611
2820
  files.push({
2612
- path: path17,
2821
+ path: path19,
2613
2822
  oldPath: status === "renamed" ? oldPath : "",
2614
2823
  status,
2615
2824
  added: count(body, "+"),
@@ -2621,10 +2830,10 @@ function parsePatch(raw) {
2621
2830
  return { files, truncated };
2622
2831
  }
2623
2832
  function stripPrefix(value) {
2624
- const path17 = unquote(value.trim());
2625
- if (path17 === "/dev/null")
2833
+ const path19 = unquote(value.trim());
2834
+ if (path19 === "/dev/null")
2626
2835
  return "";
2627
- return path17.replace(/^[ab]\//, "");
2836
+ return path19.replace(/^[ab]\//, "");
2628
2837
  }
2629
2838
  function count(body, mark) {
2630
2839
  let n = 0;
@@ -2635,11 +2844,241 @@ function count(body, mark) {
2635
2844
  return n;
2636
2845
  }
2637
2846
 
2847
+ // ../host/dist/github.js
2848
+ import { execFile as execFile3 } from "node:child_process";
2849
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
2850
+ import path7 from "node:path";
2851
+ var API = "https://api.github.com";
2852
+ var API_VERSION = "2022-11-28";
2853
+ var API_TIMEOUT_MS = 2e4;
2854
+ var GH_TIMEOUT_MS = 15e3;
2855
+ var ORG_LIMIT = 100;
2856
+ var GitHubAccount = class {
2857
+ #storePath;
2858
+ #token = "";
2859
+ /**
2860
+ * Есть ли `gh` в PATH. `null` — ещё не проверяли.
2861
+ *
2862
+ * Промах не запоминается навсегда по той же причине, что и в поиске `claude`
2863
+ * (см. [`claude-cli.ts`](./claude-cli.ts)): CLI могли поставить уже после
2864
+ * запуска хоста, и требовать за это перезапуск было бы грубо. Здесь для
2865
+ * этого хватает сброса при каждом чтении статуса — его спрашивают тогда,
2866
+ * когда человек стоит перед окном публикации и готов подождать одну попытку
2867
+ * запуска процесса.
2868
+ */
2869
+ #ghInstalled = null;
2870
+ constructor(dataDir) {
2871
+ this.#storePath = path7.join(dataDir, "github.json");
2872
+ }
2873
+ async load() {
2874
+ try {
2875
+ const raw = JSON.parse(await readFile5(this.#storePath, "utf8"));
2876
+ const token = raw.token;
2877
+ this.#token = typeof token === "string" ? token.trim() : "";
2878
+ } catch {
2879
+ this.#token = "";
2880
+ }
2881
+ }
2882
+ /** Сохранить токен или стереть его пустой строкой. */
2883
+ async setToken(token) {
2884
+ this.#token = token.trim();
2885
+ await mkdir2(path7.dirname(this.#storePath), { recursive: true });
2886
+ await writeFile3(this.#storePath, JSON.stringify({ token: this.#token }, null, 2), {
2887
+ encoding: "utf8",
2888
+ mode: 384
2889
+ });
2890
+ }
2891
+ /**
2892
+ * Кто подключён.
2893
+ *
2894
+ * Не «есть ли токен», а «принял ли его GitHub»: просроченный или отозванный
2895
+ * токен лежит в файле ровно так же, как рабочий, и разница видна только по
2896
+ * ответу на запрос. Поэтому здесь всегда поход в сеть — зато окно публикации
2897
+ * не предложит создать репозиторий тем, чем создать его не выйдет.
2898
+ */
2899
+ async status() {
2900
+ const ghInstalled = await this.#hasGh();
2901
+ const base = {
2902
+ connected: false,
2903
+ source: "none",
2904
+ login: "",
2905
+ ghInstalled,
2906
+ hasToken: this.#token !== "",
2907
+ message: ""
2908
+ };
2909
+ try {
2910
+ const credential = await this.credential();
2911
+ return { ...base, connected: true, source: credential.source, login: credential.login };
2912
+ } catch (error) {
2913
+ return { ...base, message: describe3(error) };
2914
+ }
2915
+ }
2916
+ /**
2917
+ * Чем работать с GitHub. Бросает, если ни один источник не подошёл.
2918
+ *
2919
+ * Логин приходит тем же запросом, что и проверка токена: `GET /user` всё
2920
+ * равно единственный способ узнать, жив ли токен, и спрашивать имя вторым
2921
+ * запросом было бы лишним походом в сеть за тем, что уже пришло.
2922
+ */
2923
+ async credential() {
2924
+ const ghToken = await this.#ghToken();
2925
+ if (ghToken) {
2926
+ const login = await this.#login(ghToken).catch(() => "");
2927
+ if (login)
2928
+ return { token: ghToken, source: "gh", login };
2929
+ }
2930
+ if (this.#token) {
2931
+ const login = await this.#login(this.#token);
2932
+ return { token: this.#token, source: "token", login };
2933
+ }
2934
+ throw new Error(ghToken ? "GitHub CLI \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D \u2014 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 `gh auth login` \u0438\u043B\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043E\u043A\u0435\u043D" : "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A GitHub \u2014 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043E\u043A\u0435\u043D \u0438\u043B\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 `gh auth login`");
2935
+ }
2936
+ /**
2937
+ * Куда можно создать репозиторий: сам пользователь и его организации.
2938
+ *
2939
+ * Организации, в которых нет права заводить репозитории, GitHub всё равно
2940
+ * вернёт — прав он по этому запросу не сообщает. Отсеивать их здесь нечем, и
2941
+ * ошибку в таком случае показывает уже создание: она приходит от GitHub
2942
+ * текстом, который объясняет причину лучше, чем наша догадка.
2943
+ */
2944
+ async owners() {
2945
+ const { token, login } = await this.credential();
2946
+ const orgs = await this.#api(token, `/user/orgs?per_page=${ORG_LIMIT}`).catch(
2947
+ // Токен без scope `read:org` организаций не покажет. Это не повод не дать
2948
+ // создать репозиторий у себя — а именно это и нужно в большинстве случаев.
2949
+ () => []
2950
+ );
2951
+ return [
2952
+ { login, kind: "user" },
2953
+ ...orgs.map((org) => typeof org.login === "string" ? org.login : "").filter(Boolean).map((org) => ({ login: org, kind: "org" }))
2954
+ ];
2955
+ }
2956
+ /**
2957
+ * Создать репозиторий.
2958
+ *
2959
+ * Пустой — без README, .gitignore и лицензии: у нас на диске уже лежит
2960
+ * проект, и любой файл, созданный на стороне GitHub, превратил бы первый же
2961
+ * push в конфликт историй.
2962
+ */
2963
+ async createRepo(options2) {
2964
+ const { token, login } = await this.credential();
2965
+ const endpoint = options2.owner === login ? "/user/repos" : `/orgs/${encodeURIComponent(options2.owner)}/repos`;
2966
+ const created = await this.#api(token, endpoint, {
2967
+ method: "POST",
2968
+ body: {
2969
+ name: options2.name,
2970
+ description: options2.description,
2971
+ private: options2.private,
2972
+ auto_init: false
2973
+ }
2974
+ });
2975
+ const cloneUrl = str(created.clone_url);
2976
+ if (!cloneUrl)
2977
+ throw new Error("GitHub \u0441\u043E\u0437\u0434\u0430\u043B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0430\u0434\u0440\u0435\u0441 \u0434\u043B\u044F git");
2978
+ return {
2979
+ cloneUrl,
2980
+ htmlUrl: str(created.html_url) || cloneUrl.replace(/\.git$/, ""),
2981
+ fullName: str(created.full_name) || `${options2.owner}/${options2.name}`
2982
+ };
2983
+ }
2984
+ // ---------------------------------------------------------------- внутреннее
2985
+ /** Логин владельца токена; заодно это и есть проверка токена на живость. */
2986
+ async #login(token) {
2987
+ const user = await this.#api(token, "/user");
2988
+ const login = str(user.login);
2989
+ if (!login)
2990
+ throw new Error("GitHub \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u0443\u0447\u0451\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u2014 \u0442\u043E\u043A\u0435\u043D \u043D\u0435 \u043F\u043E\u0434\u043E\u0448\u0451\u043B");
2991
+ return login;
2992
+ }
2993
+ async #api(token, endpoint, init) {
2994
+ let response;
2995
+ try {
2996
+ response = await fetch(`${API}${endpoint}`, {
2997
+ method: init?.method ?? "GET",
2998
+ headers: {
2999
+ Accept: "application/vnd.github+json",
3000
+ Authorization: `Bearer ${token}`,
3001
+ "X-GitHub-Api-Version": API_VERSION,
3002
+ // GitHub отвечает 403 на запрос без User-Agent — это его требование,
3003
+ // а не вежливость.
3004
+ "User-Agent": "claude-code-visual",
3005
+ ...init ? { "Content-Type": "application/json" } : {}
3006
+ },
3007
+ body: init ? JSON.stringify(init.body) : void 0,
3008
+ signal: AbortSignal.timeout(API_TIMEOUT_MS)
3009
+ });
3010
+ } catch (error) {
3011
+ throw new Error(`GitHub \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${describe3(error)}`);
3012
+ }
3013
+ const text = await response.text();
3014
+ if (!response.ok)
3015
+ throw new Error(apiError(response.status, text));
3016
+ try {
3017
+ return JSON.parse(text);
3018
+ } catch {
3019
+ throw new Error(`GitHub \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0435 JSON (${response.status})`);
3020
+ }
3021
+ }
3022
+ /** Токен GitHub CLI; пустая строка — `gh` нет или вход в нём не выполнен. */
3023
+ async #ghToken() {
3024
+ if (this.#ghInstalled === false)
3025
+ return "";
3026
+ const token = await gh(["auth", "token"]).catch(() => "");
3027
+ return token.trim();
3028
+ }
3029
+ async #hasGh() {
3030
+ const version2 = await gh(["--version"]).catch(() => "");
3031
+ this.#ghInstalled = version2 !== "";
3032
+ return this.#ghInstalled;
3033
+ }
3034
+ };
3035
+ function gh(args) {
3036
+ return new Promise((resolve, reject) => {
3037
+ execFile3("gh", args, { timeout: GH_TIMEOUT_MS, windowsHide: true, encoding: "utf8" }, (error, stdout, stderr) => {
3038
+ if (error)
3039
+ return reject(new Error(stderr.trim() || error.message));
3040
+ resolve(stdout);
3041
+ });
3042
+ });
3043
+ }
3044
+ function apiError(status, body) {
3045
+ let message = "";
3046
+ let details = "";
3047
+ try {
3048
+ const parsed = JSON.parse(body);
3049
+ message = str(parsed.message);
3050
+ if (Array.isArray(parsed.errors)) {
3051
+ details = parsed.errors.map((item) => str(item.message)).filter(Boolean).join("; ");
3052
+ }
3053
+ } catch {
3054
+ message = body.slice(0, 200).trim();
3055
+ }
3056
+ const text = [message, details].filter(Boolean).join(" \u2014 ");
3057
+ switch (status) {
3058
+ case 401:
3059
+ return "GitHub \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0442\u043E\u043A\u0435\u043D (401) \u2014 \u043E\u043D \u0438\u0441\u0442\u0451\u043A \u0438\u043B\u0438 \u043E\u0442\u043E\u0437\u0432\u0430\u043D";
3060
+ case 403:
3061
+ return `GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B (403): ${text || "\u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u0438\u043B\u0438 \u043F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u043B\u0438\u043C\u0438\u0442 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432"}`;
3062
+ case 404:
3063
+ return `GitHub \u043E\u0442\u0432\u0435\u0442\u0438\u043B 404: ${text || "\u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0439 \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043B\u0438 \u0443 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u043D\u0430 \u043D\u0435\u0451"}`;
3064
+ case 422:
3065
+ return `GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B\u0441\u044F \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C: ${text || "\u0442\u0430\u043A\u043E\u0435 \u0438\u043C\u044F \u0443\u0436\u0435 \u0437\u0430\u043D\u044F\u0442\u043E"}`;
3066
+ default:
3067
+ return `GitHub \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${status}${text ? `: ${text}` : ""}`;
3068
+ }
3069
+ }
3070
+ function str(value) {
3071
+ return typeof value === "string" ? value : "";
3072
+ }
3073
+ function describe3(error) {
3074
+ return error instanceof Error ? error.message : String(error);
3075
+ }
3076
+
2638
3077
  // ../host/dist/finder.js
2639
3078
  import { existsSync, statSync } from "node:fs";
2640
3079
  import { readdir as readdir2 } from "node:fs/promises";
2641
3080
  import os2 from "node:os";
2642
- import path6 from "node:path";
3081
+ import path8 from "node:path";
2643
3082
  import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2644
3083
  var CACHE_TTL = 6e4;
2645
3084
  var FIND_LIMIT = 50;
@@ -2678,7 +3117,7 @@ var ProjectFinder = class {
2678
3117
  if (needles.length === 0)
2679
3118
  return { candidates: [], truncated: false };
2680
3119
  const open = new Map(this.registry.list().map((workspace) => [workspace.path.toLowerCase(), workspace]));
2681
- const byName = this.#dirs.filter((dir) => matches(path6.basename(dir.path), needles));
3120
+ const byName = this.#dirs.filter((dir) => matches(path8.basename(dir.path), needles));
2682
3121
  const matched = byName.length > 0 ? byName : this.#dirs.filter((dir) => matches(dir.path, needles));
2683
3122
  const sorted = [...matched].sort((a, b) => {
2684
3123
  const aOpen = open.has(a.path.toLowerCase());
@@ -2689,7 +3128,7 @@ var ProjectFinder = class {
2689
3128
  return a.sessions > 0 ? -1 : 1;
2690
3129
  if (a.lastUsedAt !== b.lastUsedAt)
2691
3130
  return b.lastUsedAt - a.lastUsedAt;
2692
- return path6.basename(a.path).localeCompare(path6.basename(b.path), void 0, {
3131
+ return path8.basename(a.path).localeCompare(path8.basename(b.path), void 0, {
2693
3132
  numeric: true,
2694
3133
  sensitivity: "base"
2695
3134
  });
@@ -2697,7 +3136,7 @@ var ProjectFinder = class {
2697
3136
  return {
2698
3137
  candidates: sorted.slice(0, FIND_LIMIT).map((dir) => ({
2699
3138
  path: dir.path,
2700
- name: path6.basename(dir.path) || dir.path,
3139
+ name: path8.basename(dir.path) || dir.path,
2701
3140
  sessions: dir.sessions,
2702
3141
  lastUsedAt: dir.lastUsedAt || null,
2703
3142
  open: open.has(dir.path.toLowerCase()),
@@ -2721,7 +3160,7 @@ var ProjectFinder = class {
2721
3160
  const candidates = [];
2722
3161
  for (const entry of named) {
2723
3162
  const absolute = entry.path.trim();
2724
- if (!absolute || !path6.isAbsolute(absolute))
3163
+ if (!absolute || !path8.isAbsolute(absolute))
2725
3164
  continue;
2726
3165
  const key = absolute.toLowerCase();
2727
3166
  if (seen.has(key))
@@ -2732,7 +3171,7 @@ var ProjectFinder = class {
2732
3171
  const record = known.get(key);
2733
3172
  candidates.push({
2734
3173
  path: record?.path ?? absolute,
2735
- name: path6.basename(absolute) || absolute,
3174
+ name: path8.basename(absolute) || absolute,
2736
3175
  sessions: record?.sessions ?? 0,
2737
3176
  lastUsedAt: record?.lastUsedAt || null,
2738
3177
  open: open.has(key),
@@ -2789,7 +3228,7 @@ var ProjectFinder = class {
2789
3228
  if (!dirent.isDirectory() || SKIP_NAMES.has(dirent.name) || dirent.name.startsWith(".")) {
2790
3229
  continue;
2791
3230
  }
2792
- const full = path6.join(parent, dirent.name);
3231
+ const full = path8.join(parent, dirent.name);
2793
3232
  const key = full.toLowerCase();
2794
3233
  if (dirs.has(key))
2795
3234
  continue;
@@ -2810,7 +3249,7 @@ var ProjectFinder = class {
2810
3249
  #parents(dirs) {
2811
3250
  const counted = /* @__PURE__ */ new Map();
2812
3251
  for (const dir of dirs) {
2813
- const parent = path6.dirname(dir.path);
3252
+ const parent = path8.dirname(dir.path);
2814
3253
  if (parent === dir.path || !usable(parent))
2815
3254
  continue;
2816
3255
  const key = parent.toLowerCase();
@@ -2922,18 +3361,18 @@ function fromText(text) {
2922
3361
 
2923
3362
  // ../host/dist/workspaces.js
2924
3363
  import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
2925
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
2926
- import path7 from "node:path";
3364
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
3365
+ import path9 from "node:path";
2927
3366
  import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
2928
3367
  var WorkspaceRegistry = class {
2929
3368
  #paths = [];
2930
3369
  #storePath;
2931
3370
  constructor(dataDir) {
2932
- this.#storePath = path7.join(dataDir, "workspaces.json");
3371
+ this.#storePath = path9.join(dataDir, "workspaces.json");
2933
3372
  }
2934
3373
  async load() {
2935
3374
  try {
2936
- const raw = await readFile4(this.#storePath, "utf8");
3375
+ const raw = await readFile6(this.#storePath, "utf8");
2937
3376
  const parsed = JSON.parse(raw);
2938
3377
  if (Array.isArray(parsed)) {
2939
3378
  this.#paths = parsed.filter((p) => typeof p === "string" && existsSync2(p));
@@ -2943,8 +3382,8 @@ var WorkspaceRegistry = class {
2943
3382
  }
2944
3383
  }
2945
3384
  async #persist() {
2946
- await mkdir2(path7.dirname(this.#storePath), { recursive: true });
2947
- await writeFile2(this.#storePath, JSON.stringify(this.#paths, null, 2), "utf8");
3385
+ await mkdir3(path9.dirname(this.#storePath), { recursive: true });
3386
+ await writeFile4(this.#storePath, JSON.stringify(this.#paths, null, 2), "utf8");
2948
3387
  }
2949
3388
  list() {
2950
3389
  return this.#paths.map(toInfo);
@@ -2953,7 +3392,7 @@ var WorkspaceRegistry = class {
2953
3392
  return this.list().find((w) => w.id === id);
2954
3393
  }
2955
3394
  async open(rawPath) {
2956
- const absolute = path7.resolve(rawPath);
3395
+ const absolute = path9.resolve(rawPath);
2957
3396
  if (!existsSync2(absolute)) {
2958
3397
  throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
2959
3398
  }
@@ -2995,24 +3434,24 @@ var WorkspaceRegistry = class {
2995
3434
  async create(parentRaw, nameRaw, init) {
2996
3435
  const name = nameRaw.trim();
2997
3436
  requireName(name);
2998
- const parent = path7.resolve(parentRaw.trim());
3437
+ const parent = path9.resolve(parentRaw.trim());
2999
3438
  if (!existsSync2(parent)) {
3000
3439
  throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${parent}`);
3001
3440
  }
3002
3441
  if (!statSync2(parent).isDirectory()) {
3003
3442
  throw new Error(`\u042D\u0442\u043E \u043D\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${parent}`);
3004
3443
  }
3005
- const target = path7.join(parent, name);
3444
+ const target = path9.join(parent, name);
3006
3445
  if (existsSync2(target)) {
3007
3446
  throw new Error(`\u0423\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: ${target} \u2014 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0435\u0433\u043E \u043A\u0430\u043A \u043F\u0440\u043E\u0435\u043A\u0442`);
3008
3447
  }
3009
- await mkdir2(target);
3448
+ await mkdir3(target);
3010
3449
  const warnings = [];
3011
3450
  if (init.claudeMd) {
3012
- await step(warnings, "CLAUDE.md", () => writeFile2(path7.join(target, "CLAUDE.md"), claudeMdStub(name), "utf8"));
3451
+ await step(warnings, "CLAUDE.md", () => writeFile4(path9.join(target, "CLAUDE.md"), claudeMdStub(name), "utf8"));
3013
3452
  }
3014
3453
  if (init.readme) {
3015
- await step(warnings, "README.md", () => writeFile2(path7.join(target, "README.md"), readmeStub(name), "utf8"));
3454
+ await step(warnings, "README.md", () => writeFile4(path9.join(target, "README.md"), readmeStub(name), "utf8"));
3016
3455
  }
3017
3456
  if (init.git) {
3018
3457
  await step(warnings, "git init", () => new GitRepo(target).init());
@@ -3083,24 +3522,24 @@ function toInfo(absolute) {
3083
3522
  return {
3084
3523
  id: workspaceIdFromPath(absolute),
3085
3524
  path: absolute,
3086
- name: path7.basename(absolute) || absolute,
3087
- hasClaudeMd: existsSync2(path7.join(absolute, "CLAUDE.md"))
3525
+ name: path9.basename(absolute) || absolute,
3526
+ hasClaudeMd: existsSync2(path9.join(absolute, "CLAUDE.md"))
3088
3527
  };
3089
3528
  }
3090
3529
 
3091
3530
  // ../host/dist/archive.js
3092
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
3093
- import path8 from "node:path";
3531
+ import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
3532
+ import path10 from "node:path";
3094
3533
  var SessionArchive = class {
3095
3534
  #archived = /* @__PURE__ */ new Map();
3096
3535
  #restored = /* @__PURE__ */ new Map();
3097
3536
  #storePath;
3098
3537
  constructor(dataDir) {
3099
- this.#storePath = path8.join(dataDir, "session-archive.json");
3538
+ this.#storePath = path10.join(dataDir, "session-archive.json");
3100
3539
  }
3101
3540
  async load() {
3102
3541
  try {
3103
- const raw = await readFile5(this.#storePath, "utf8");
3542
+ const raw = await readFile7(this.#storePath, "utf8");
3104
3543
  const parsed = JSON.parse(raw);
3105
3544
  this.#archived = new Map(Object.entries(parsed.archived ?? {}).filter(([, r]) => isRecord(r)));
3106
3545
  this.#restored = new Map(Object.entries(parsed.restored ?? {}).filter((entry) => typeof entry[1] === "number"));
@@ -3189,8 +3628,8 @@ var SessionArchive = class {
3189
3628
  archived: Object.fromEntries(this.#archived),
3190
3629
  restored: Object.fromEntries(this.#restored)
3191
3630
  };
3192
- await mkdir3(path8.dirname(this.#storePath), { recursive: true });
3193
- await writeFile3(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3631
+ await mkdir4(path10.dirname(this.#storePath), { recursive: true });
3632
+ await writeFile5(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3194
3633
  }
3195
3634
  };
3196
3635
  function isRecord(value) {
@@ -3198,17 +3637,17 @@ function isRecord(value) {
3198
3637
  }
3199
3638
 
3200
3639
  // ../host/dist/pins.js
3201
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
3202
- import path9 from "node:path";
3640
+ import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile6 } from "node:fs/promises";
3641
+ import path11 from "node:path";
3203
3642
  var SessionPins = class {
3204
3643
  #pinned = /* @__PURE__ */ new Map();
3205
3644
  #storePath;
3206
3645
  constructor(dataDir) {
3207
- this.#storePath = path9.join(dataDir, "session-pins.json");
3646
+ this.#storePath = path11.join(dataDir, "session-pins.json");
3208
3647
  }
3209
3648
  async load() {
3210
3649
  try {
3211
- const raw = await readFile6(this.#storePath, "utf8");
3650
+ const raw = await readFile8(this.#storePath, "utf8");
3212
3651
  const parsed = JSON.parse(raw);
3213
3652
  this.#pinned = new Map(Object.entries(parsed.pinned ?? {}).filter(([, r]) => isRecord2(r)));
3214
3653
  } catch {
@@ -3306,8 +3745,8 @@ var SessionPins = class {
3306
3745
  }
3307
3746
  async #persist() {
3308
3747
  const file = { pinned: Object.fromEntries(this.#pinned) };
3309
- await mkdir4(path9.dirname(this.#storePath), { recursive: true });
3310
- await writeFile4(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3748
+ await mkdir5(path11.dirname(this.#storePath), { recursive: true });
3749
+ await writeFile6(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3311
3750
  }
3312
3751
  };
3313
3752
  function isRecord2(value) {
@@ -3315,9 +3754,9 @@ function isRecord2(value) {
3315
3754
  }
3316
3755
 
3317
3756
  // ../host/dist/app-settings.js
3318
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
3757
+ import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
3319
3758
  import { homedir as homedir3 } from "node:os";
3320
- import path10 from "node:path";
3759
+ import path12 from "node:path";
3321
3760
  var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
3322
3761
  var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
3323
3762
  var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
@@ -3352,11 +3791,11 @@ var AppSettingsStore = class {
3352
3791
  #settings = { ...DEFAULT_APP_SETTINGS };
3353
3792
  #storePath;
3354
3793
  constructor(dataDir) {
3355
- this.#storePath = path10.join(dataDir, "app-settings.json");
3794
+ this.#storePath = path12.join(dataDir, "app-settings.json");
3356
3795
  }
3357
3796
  async load() {
3358
3797
  try {
3359
- const raw = await readFile7(this.#storePath, "utf8");
3798
+ const raw = await readFile9(this.#storePath, "utf8");
3360
3799
  this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
3361
3800
  } catch {
3362
3801
  this.#settings = { ...DEFAULT_APP_SETTINGS };
@@ -3376,8 +3815,8 @@ var AppSettingsStore = class {
3376
3815
  async update(patch) {
3377
3816
  const next = sanitize(patch, this.#settings);
3378
3817
  this.#settings = next;
3379
- await mkdir5(path10.dirname(this.#storePath), { recursive: true });
3380
- await writeFile5(this.#storePath, JSON.stringify(next, null, 2), "utf8");
3818
+ await mkdir6(path12.dirname(this.#storePath), { recursive: true });
3819
+ await writeFile7(this.#storePath, JSON.stringify(next, null, 2), "utf8");
3381
3820
  return this.current;
3382
3821
  }
3383
3822
  };
@@ -3415,7 +3854,7 @@ function sanitize(value, fallback) {
3415
3854
  return result;
3416
3855
  }
3417
3856
  function claudeHome() {
3418
- return path10.join(homedir3(), ".claude");
3857
+ return path12.join(homedir3(), ".claude");
3419
3858
  }
3420
3859
  function writableRoots(mode, cwd, workspacePaths) {
3421
3860
  if (mode === "all")
@@ -3425,7 +3864,7 @@ function writableRoots(mode, cwd, workspacePaths) {
3425
3864
  return [...roots];
3426
3865
  for (const workspace of [cwd, ...workspacePaths]) {
3427
3866
  roots.add(workspace);
3428
- const parent = path10.dirname(workspace);
3867
+ const parent = path12.dirname(workspace);
3429
3868
  if (parent !== workspace)
3430
3869
  roots.add(parent);
3431
3870
  }
@@ -3433,9 +3872,9 @@ function writableRoots(mode, cwd, workspacePaths) {
3433
3872
  }
3434
3873
 
3435
3874
  // ../host/dist/plugins.js
3436
- import { readFile as readFile8 } from "node:fs/promises";
3875
+ import { readFile as readFile10 } from "node:fs/promises";
3437
3876
  import { homedir as homedir4 } from "node:os";
3438
- import path11 from "node:path";
3877
+ import path13 from "node:path";
3439
3878
  var TIMEOUT_MS2 = 12e4;
3440
3879
  var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
3441
3880
  var PluginCli = class {
@@ -3534,10 +3973,10 @@ async function readCatalog(markets, installed) {
3534
3973
  return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
3535
3974
  }
3536
3975
  async function readMarketplace(market, installed) {
3537
- const root = market.installLocation || path11.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
3976
+ const root = market.installLocation || path13.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
3538
3977
  let manifest;
3539
3978
  try {
3540
- const text = await readFile8(path11.join(root, ".claude-plugin", "marketplace.json"), "utf8");
3979
+ const text = await readFile10(path13.join(root, ".claude-plugin", "marketplace.json"), "utf8");
3541
3980
  manifest = JSON.parse(text);
3542
3981
  } catch {
3543
3982
  return [];
@@ -3624,7 +4063,7 @@ var AuthCli = class extends EventEmitter2 {
3624
4063
  if (this.#child)
3625
4064
  throw new Error("\u0412\u0445\u043E\u0434 \u0443\u0436\u0435 \u0438\u0434\u0451\u0442 \u2014 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u0435 \u0435\u0433\u043E");
3626
4065
  if (switchAccount) {
3627
- const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(describe3);
4066
+ const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(describe4);
3628
4067
  this.#push({ ...IDLE, active: true, output: `${out}
3629
4068
  ` });
3630
4069
  this.emit("changed");
@@ -3644,7 +4083,7 @@ var AuthCli = class extends EventEmitter2 {
3644
4083
  child.stdout?.on("data", (chunk) => this.#absorb(chunk.toString()));
3645
4084
  child.stderr?.on("data", (chunk) => this.#absorb(chunk.toString()));
3646
4085
  child.on("error", (error) => {
3647
- this.#fail(error.code === "ENOENT" ? "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D claude \u0432 PATH \u2014 \u0432\u0445\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" : describe3(error));
4086
+ this.#fail(error.code === "ENOENT" ? "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D claude \u0432 PATH \u2014 \u0432\u0445\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" : describe4(error));
3648
4087
  });
3649
4088
  child.on("close", (code) => {
3650
4089
  if (this.#child !== child)
@@ -3659,14 +4098,20 @@ var AuthCli = class extends EventEmitter2 {
3659
4098
  this.emit("changed");
3660
4099
  });
3661
4100
  }
3662
- /** Отдать процессу код, принесённый из браузера. */
4101
+ /**
4102
+ * Отдать процессу код, принесённый из браузера.
4103
+ *
4104
+ * `awaitingCode` намеренно остаётся поднятым: отправка кода не конец входа, а
4105
+ * попытка. CLI отвечает на неё либо успехом и выходом (тогда поле уберёт
4106
+ * `close`), либо строкой «Invalid code» — и во втором случае человеку нужно то
4107
+ * же самое поле, чтобы вставить код целиком.
4108
+ */
3663
4109
  submitCode(code) {
3664
4110
  const child = this.#child;
3665
4111
  if (!child?.stdin?.writable)
3666
4112
  throw new Error("\u0412\u0445\u043E\u0434 \u043D\u0435 \u0438\u0434\u0451\u0442 \u2014 \u0432\u0432\u043E\u0434\u0438\u0442\u044C \u043A\u043E\u0434 \u043D\u0435\u043A\u0443\u0434\u0430");
3667
- child.stdin.write(`${code.trim()}
4113
+ child.stdin.write(`${normalizeAuthCode(code)}
3668
4114
  `);
3669
- this.#push({ ...this.#state, awaitingCode: false });
3670
4115
  }
3671
4116
  /** Прервать вход. Учётные данные при этом остаются прежними. */
3672
4117
  cancel() {
@@ -3691,7 +4136,14 @@ var AuthCli = class extends EventEmitter2 {
3691
4136
  url: this.#state.url || URL_RE.exec(output)?.[0] || "",
3692
4137
  // Приглашение ищем в хвосте: раньше в выводе оно встретиться не может, а
3693
4138
  // так мы не примем за него упоминание кода в старом сообщении.
3694
- awaitingCode: CODE_PROMPT_RE.test(output.slice(-200))
4139
+ //
4140
+ // И не гасим его обратно: приглашение CLI печатает ровно один раз, а
4141
+ // читает строки со stdin до самого конца входа. Отвяжи мы поле от этой
4142
+ // однократной печати — первый же неверный код («Invalid code…» в хвосте
4143
+ // перестаёт подходить под выражение) убирал бы поле с экрана, и
4144
+ // исправить опечатку стало бы негде: вход ещё идёт, а ввести в него
4145
+ // нечего.
4146
+ awaitingCode: this.#state.awaitingCode || CODE_PROMPT_RE.test(output.slice(-200))
3695
4147
  });
3696
4148
  }
3697
4149
  #fail(message) {
@@ -3713,6 +4165,23 @@ var AuthCli = class extends EventEmitter2 {
3713
4165
  function run(args, timeout) {
3714
4166
  return runClaude(args, { timeout, missing: "\u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u043C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E" });
3715
4167
  }
4168
+ function normalizeAuthCode(raw) {
4169
+ const value = raw.trim();
4170
+ if (!/^https?:\/\//i.test(value))
4171
+ return value;
4172
+ let url2;
4173
+ try {
4174
+ url2 = new URL(value);
4175
+ } catch {
4176
+ return value;
4177
+ }
4178
+ const params = new URLSearchParams(url2.search.slice(1) || url2.hash.replace(/^#/, ""));
4179
+ const code = params.get("code");
4180
+ const state = params.get("state");
4181
+ if (!code)
4182
+ return value;
4183
+ return state ? `${code}#${state}` : code;
4184
+ }
3716
4185
  function parseStatus2(raw) {
3717
4186
  const empty = {
3718
4187
  loggedIn: false,
@@ -3733,23 +4202,23 @@ function parseStatus2(raw) {
3733
4202
  }
3734
4203
  return {
3735
4204
  loggedIn: parsed.loggedIn === true,
3736
- method: str(parsed.authMethod) || "none",
3737
- email: str(parsed.email),
3738
- organization: str(parsed.orgName),
3739
- plan: str(parsed.subscriptionType)
4205
+ method: str2(parsed.authMethod) || "none",
4206
+ email: str2(parsed.email),
4207
+ organization: str2(parsed.orgName),
4208
+ plan: str2(parsed.subscriptionType)
3740
4209
  };
3741
4210
  }
3742
- function str(value) {
4211
+ function str2(value) {
3743
4212
  return typeof value === "string" ? value : "";
3744
4213
  }
3745
- function describe3(error) {
4214
+ function describe4(error) {
3746
4215
  return error instanceof Error ? error.message : String(error);
3747
4216
  }
3748
4217
 
3749
4218
  // ../host/dist/relay-link.js
3750
4219
  import { EventEmitter as EventEmitter3 } from "node:events";
3751
- import { readFile as readFile9, writeFile as writeFile6, rm as rm2, mkdir as mkdir6 } from "node:fs/promises";
3752
- import path12 from "node:path";
4220
+ import { readFile as readFile11, writeFile as writeFile8, rm as rm3, mkdir as mkdir7 } from "node:fs/promises";
4221
+ import path14 from "node:path";
3753
4222
  import { hostname } from "node:os";
3754
4223
  import WebSocket from "ws";
3755
4224
 
@@ -3790,7 +4259,7 @@ var RelayLink = class extends EventEmitter3 {
3790
4259
  #log;
3791
4260
  constructor(options2) {
3792
4261
  super();
3793
- this.#file = path12.join(options2.dataDir, "relay.json");
4262
+ this.#file = path14.join(options2.dataDir, "relay.json");
3794
4263
  this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
3795
4264
  this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
3796
4265
  }
@@ -3871,7 +4340,7 @@ var RelayLink = class extends EventEmitter3 {
3871
4340
  }
3872
4341
  async #read() {
3873
4342
  try {
3874
- const raw = await readFile9(this.#file, "utf8");
4343
+ const raw = await readFile11(this.#file, "utf8");
3875
4344
  const parsed = JSON.parse(raw);
3876
4345
  if (typeof parsed !== "object" || parsed === null)
3877
4346
  return null;
@@ -3884,8 +4353,8 @@ var RelayLink = class extends EventEmitter3 {
3884
4353
  }
3885
4354
  }
3886
4355
  async #write(credentials) {
3887
- await mkdir6(path12.dirname(this.#file), { recursive: true });
3888
- await writeFile6(this.#file, `${JSON.stringify(credentials, null, 2)}
4356
+ await mkdir7(path14.dirname(this.#file), { recursive: true });
4357
+ await writeFile8(this.#file, `${JSON.stringify(credentials, null, 2)}
3889
4358
  `, {
3890
4359
  encoding: "utf8",
3891
4360
  mode: 384
@@ -3909,7 +4378,7 @@ var RelayLink = class extends EventEmitter3 {
3909
4378
  body: JSON.stringify({ code, name: machine, platform: process.platform })
3910
4379
  });
3911
4380
  } catch (error) {
3912
- throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${describe4(error)}`);
4381
+ throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${describe5(error)}`);
3913
4382
  }
3914
4383
  const data = await response.json().catch(() => ({}));
3915
4384
  if (!response.ok || !data.token || !data.hostId) {
@@ -3937,7 +4406,7 @@ var RelayLink = class extends EventEmitter3 {
3937
4406
  this.#error = null;
3938
4407
  this.#clients.clear();
3939
4408
  this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
3940
- await rm2(this.#file, { force: true });
4409
+ await rm3(this.#file, { force: true });
3941
4410
  this.#changed();
3942
4411
  this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
3943
4412
  }
@@ -3982,7 +4451,7 @@ var RelayLink = class extends EventEmitter3 {
3982
4451
  });
3983
4452
  socket.on("message", (raw) => this.#onMessage(socket, String(raw)));
3984
4453
  socket.on("error", (error) => {
3985
- this.#error = describe4(error);
4454
+ this.#error = describe5(error);
3986
4455
  });
3987
4456
  socket.on("close", (code, reason) => {
3988
4457
  if (this.#socket !== socket)
@@ -4088,14 +4557,14 @@ var RelayLink = class extends EventEmitter3 {
4088
4557
  socket.send(JSON.stringify(frame));
4089
4558
  }
4090
4559
  };
4091
- function describe4(error) {
4560
+ function describe5(error) {
4092
4561
  return error instanceof Error ? error.message : String(error);
4093
4562
  }
4094
4563
 
4095
4564
  // ../host/dist/terminal.js
4096
4565
  import { EventEmitter as EventEmitter4 } from "node:events";
4097
4566
  import { access as access2, constants as constants2 } from "node:fs/promises";
4098
- import path13 from "node:path";
4567
+ import path15 from "node:path";
4099
4568
  import { randomUUID as randomUUID2 } from "node:crypto";
4100
4569
  var SCROLLBACK_BYTES = 256 * 1024;
4101
4570
  var FLUSH_MS = 12;
@@ -4224,12 +4693,25 @@ var TerminalHub = class extends EventEmitter4 {
4224
4693
  #pty() {
4225
4694
  this.#module ??= import("@lydell/node-pty").then((module) => module).catch((error) => {
4226
4695
  this.#module = null;
4227
- const reason = error instanceof Error ? error.message : String(error);
4228
- throw new Error(`\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${reason}`);
4696
+ throw new Error(explainMissingPty(error));
4229
4697
  });
4230
4698
  return this.#module;
4231
4699
  }
4232
4700
  };
4701
+ function explainMissingPty(error) {
4702
+ const reason = error instanceof Error ? error.message : String(error);
4703
+ const platformPackageMissing = reason.includes("could not find the platform-specific package") || /Cannot find module '@lydell\/node-pty-/.test(reason);
4704
+ if (!platformPackageMissing) {
4705
+ return `\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${reason}`;
4706
+ }
4707
+ return [
4708
+ `\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: \u0432 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u043C ccv \u043D\u0435\u0442 \u0441\u0431\u043E\u0440\u043A\u0438 PTY \u043F\u043E\u0434 ${process.platform}-${process.arch}.`,
4709
+ "\u041E\u0431\u044B\u0447\u043D\u043E \u044D\u0442\u043E \u0437\u043D\u0430\u0447\u0438\u0442, \u0447\u0442\u043E \u043F\u0430\u043A\u0435\u0442 \u0441\u0442\u0430\u0432\u0438\u043B\u0438 \u0441 \xAB--omit=optional\xBB \u043B\u0438\u0431\u043E node_modules \u043F\u0435\u0440\u0435\u0435\u0445\u0430\u043B \u0441 \u0434\u0440\u0443\u0433\u043E\u0439",
4710
+ "\u0441\u0438\u0441\u0442\u0435\u043C\u044B. \u041B\u0435\u0447\u0438\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u043E\u0439: npm i -g @i14ks/ccv \u2014 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 ccv.",
4711
+ "",
4712
+ reason
4713
+ ].join("\n");
4714
+ }
4233
4715
  function clampSize(value, min, max, fallback) {
4234
4716
  if (!Number.isFinite(value))
4235
4717
  return fallback;
@@ -4252,22 +4734,22 @@ async function defaultShell() {
4252
4734
  if (pwsh)
4253
4735
  return { file: pwsh, args: [], title: "pwsh" };
4254
4736
  const system = process.env.SystemRoot ?? "C:\\Windows";
4255
- const powershell = path13.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
4737
+ const powershell = path15.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
4256
4738
  if (await exists2(powershell))
4257
4739
  return { file: powershell, args: [], title: "powershell" };
4258
4740
  return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
4259
4741
  }
4260
4742
  const shell = process.env.SHELL;
4261
4743
  if (shell && await exists2(shell))
4262
- return { file: shell, args: [], title: path13.basename(shell) };
4744
+ return { file: shell, args: [], title: path15.basename(shell) };
4263
4745
  if (await exists2("/bin/bash"))
4264
4746
  return { file: "/bin/bash", args: [], title: "bash" };
4265
4747
  return { file: "/bin/sh", args: [], title: "sh" };
4266
4748
  }
4267
4749
  async function findOnPath(name) {
4268
- const dirs = (process.env.PATH ?? "").split(path13.delimiter).filter(Boolean);
4750
+ const dirs = (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean);
4269
4751
  for (const dir of dirs) {
4270
- const candidate = path13.join(dir, name);
4752
+ const candidate = path15.join(dir, name);
4271
4753
  if (await exists2(candidate))
4272
4754
  return candidate;
4273
4755
  }
@@ -4278,10 +4760,10 @@ function exists2(file) {
4278
4760
  }
4279
4761
 
4280
4762
  // ../host/dist/mcp-config.js
4281
- import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "node:fs/promises";
4282
- import path14 from "node:path";
4763
+ import { readFile as readFile12, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
4764
+ import path16 from "node:path";
4283
4765
  var MCP_FILE = ".mcp.json";
4284
- var LOCAL_SETTINGS2 = path14.join(".claude", "settings.local.json");
4766
+ var LOCAL_SETTINGS2 = path16.join(".claude", "settings.local.json");
4285
4767
  var queues = /* @__PURE__ */ new Map();
4286
4768
  function serialize(cwd, work) {
4287
4769
  const previous = queues.get(cwd) ?? Promise.resolve();
@@ -4290,7 +4772,7 @@ function serialize(cwd, work) {
4290
4772
  return next;
4291
4773
  }
4292
4774
  async function readProjectServers(cwd) {
4293
- const file = await readJson(path14.join(cwd, MCP_FILE));
4775
+ const file = await readJson(path16.join(cwd, MCP_FILE));
4294
4776
  const servers = file?.mcpServers;
4295
4777
  if (!servers || typeof servers !== "object")
4296
4778
  return {};
@@ -4298,23 +4780,23 @@ async function readProjectServers(cwd) {
4298
4780
  }
4299
4781
  function addProjectServer(cwd, name, config) {
4300
4782
  return serialize(cwd, async () => {
4301
- const file = await readJson(path14.join(cwd, MCP_FILE)) ?? {};
4783
+ const file = await readJson(path16.join(cwd, MCP_FILE)) ?? {};
4302
4784
  const servers = file.mcpServers ?? {};
4303
4785
  servers[name] = config;
4304
4786
  file.mcpServers = servers;
4305
- await writeJson(path14.join(cwd, MCP_FILE), file);
4787
+ await writeJson(path16.join(cwd, MCP_FILE), file);
4306
4788
  await approve(cwd, name);
4307
4789
  });
4308
4790
  }
4309
4791
  function removeProjectServer(cwd, name) {
4310
4792
  return serialize(cwd, async () => {
4311
- const file = await readJson(path14.join(cwd, MCP_FILE));
4793
+ const file = await readJson(path16.join(cwd, MCP_FILE));
4312
4794
  const servers = file?.mcpServers;
4313
4795
  if (!servers || !(name in servers))
4314
4796
  return false;
4315
4797
  delete servers[name];
4316
- await writeJson(path14.join(cwd, MCP_FILE), file);
4317
- const settingsPath = path14.join(cwd, LOCAL_SETTINGS2);
4798
+ await writeJson(path16.join(cwd, MCP_FILE), file);
4799
+ const settingsPath = path16.join(cwd, LOCAL_SETTINGS2);
4318
4800
  const settings = await readJson(settingsPath);
4319
4801
  const enabled = settings?.enabledMcpjsonServers;
4320
4802
  if (settings && Array.isArray(enabled) && enabled.includes(name)) {
@@ -4325,7 +4807,7 @@ function removeProjectServer(cwd, name) {
4325
4807
  });
4326
4808
  }
4327
4809
  async function approve(cwd, name) {
4328
- const file = path14.join(cwd, LOCAL_SETTINGS2);
4810
+ const file = path16.join(cwd, LOCAL_SETTINGS2);
4329
4811
  const settings = await readJson(file) ?? {};
4330
4812
  const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
4331
4813
  if (!enabled.includes(name))
@@ -4364,21 +4846,21 @@ function describeConfig(config) {
4364
4846
  }
4365
4847
  async function readJson(file) {
4366
4848
  try {
4367
- const raw = await readFile10(file, "utf8");
4849
+ const raw = await readFile12(file, "utf8");
4368
4850
  const parsed = JSON.parse(raw);
4369
4851
  return parsed && typeof parsed === "object" ? parsed : null;
4370
4852
  } catch (error) {
4371
4853
  if (error.code === "ENOENT")
4372
4854
  return null;
4373
4855
  if (error instanceof SyntaxError) {
4374
- throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path14.basename(file)}: ${error.message}`);
4856
+ throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path16.basename(file)}: ${error.message}`);
4375
4857
  }
4376
4858
  throw error;
4377
4859
  }
4378
4860
  }
4379
4861
  async function writeJson(file, data) {
4380
- await mkdir7(path14.dirname(file), { recursive: true });
4381
- await writeFile7(file, `${JSON.stringify(data, null, 2)}
4862
+ await mkdir8(path16.dirname(file), { recursive: true });
4863
+ await writeFile9(file, `${JSON.stringify(data, null, 2)}
4382
4864
  `, "utf8");
4383
4865
  }
4384
4866
 
@@ -4414,6 +4896,16 @@ var HostServer = class {
4414
4896
  #auth = new AuthCli();
4415
4897
  #relay;
4416
4898
  #terminals = new TerminalHub();
4899
+ #github;
4900
+ /**
4901
+ * Проекты, в которых прямо сейчас идёт сетевая операция git.
4902
+ *
4903
+ * Замок, а не индикатор: рабочее дерево у вкладок общее, и второй `push`
4904
+ * поверх первого — это два процесса git на один `.git`, то есть блокировка
4905
+ * индекса и половина операции. Ключ — `workspaceId`: параллельная работа с
4906
+ * разными проектами при этом остаётся возможной.
4907
+ */
4908
+ #gitBusy = /* @__PURE__ */ new Set();
4417
4909
  constructor(options2) {
4418
4910
  this.#options = options2;
4419
4911
  this.#workspaces = new WorkspaceRegistry(options2.dataDir);
@@ -4422,6 +4914,7 @@ var HostServer = class {
4422
4914
  this.#settings = new AppSettingsStore(options2.dataDir);
4423
4915
  this.#finder = new ProjectFinder(this.#workspaces);
4424
4916
  this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
4917
+ this.#github = new GitHubAccount(options2.dataDir);
4425
4918
  this.#wireRelay();
4426
4919
  this.#wireTerminals();
4427
4920
  this.#auth.on("login", (state) => {
@@ -4436,6 +4929,7 @@ var HostServer = class {
4436
4929
  await this.#archive.load();
4437
4930
  await this.#pins.load();
4438
4931
  await this.#settings.load();
4932
+ await this.#github.load();
4439
4933
  await warmClaudeLauncher();
4440
4934
  void this.#finder.warm();
4441
4935
  const httpServer = createServer((req, res) => this.#serveHttp(req, res));
@@ -4899,24 +5393,47 @@ var HostServer = class {
4899
5393
  detail: await this.#git(frame.workspaceId).show(frame.ref)
4900
5394
  });
4901
5395
  return;
4902
- case "git.checkout": {
4903
- const repo = this.#git(frame.workspaceId);
4904
- let ok = true;
4905
- let message;
4906
- try {
4907
- message = await repo.checkout(frame.target);
4908
- } catch (error) {
4909
- ok = false;
4910
- message = error instanceof Error ? error.message : String(error);
4911
- }
4912
- this.#broadcast({ type: "git.result", workspaceId: frame.workspaceId, ok, message });
4913
- this.#broadcast({
4914
- type: "git.state",
5396
+ case "git.checkout":
5397
+ await this.#gitOperation(frame.workspaceId, async (repo) => ({
5398
+ message: await repo.checkout(frame.target)
5399
+ }));
5400
+ return;
5401
+ case "git.remote":
5402
+ await this.#gitOperation(frame.workspaceId, async (repo) => ({
5403
+ message: await repo.setRemote(frame.url)
5404
+ }));
5405
+ return;
5406
+ case "git.push":
5407
+ await this.#gitNetwork(frame.workspaceId, "\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\u2026", async (repo) => ({
5408
+ message: await repo.push(await this.#gitToken())
5409
+ }));
5410
+ return;
5411
+ case "git.pull":
5412
+ await this.#gitNetwork(frame.workspaceId, frame.merge ? "\u0417\u0430\u0431\u0438\u0440\u0430\u0435\u043C\u2026" : "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C remote\u2026", async (repo) => ({ message: await repo.pull(frame.merge, await this.#gitToken()) }));
5413
+ return;
5414
+ case "git.publish":
5415
+ await this.#gitNetwork(frame.workspaceId, "\u041F\u0443\u0431\u043B\u0438\u043A\u0443\u0435\u043C \u043D\u0430 GitHub\u2026", (repo) => this.#publish(repo, frame));
5416
+ return;
5417
+ case "git.files": {
5418
+ const { files, truncated } = await this.#git(frame.workspaceId).candidates();
5419
+ this.#send(socket, {
5420
+ type: "git.files",
4915
5421
  workspaceId: frame.workspaceId,
4916
- state: await repo.state()
5422
+ files,
5423
+ truncated
4917
5424
  });
4918
5425
  return;
4919
5426
  }
5427
+ case "github.status":
5428
+ this.#send(socket, { type: "github.status", status: await this.#github.status() });
5429
+ return;
5430
+ case "github.token":
5431
+ await this.#github.setToken(frame.token);
5432
+ this.#broadcast({ type: "github.status", status: await this.#github.status() });
5433
+ return;
5434
+ case "github.owners":
5435
+ this.#send(socket, { type: "github.owners", owners: await this.#github.owners() });
5436
+ return;
4920
5437
  case "fs.list":
4921
5438
  this.#send(socket, {
4922
5439
  type: "fs.dir",
@@ -5523,6 +6040,101 @@ var HostServer = class {
5523
6040
  throw new Error(`\u0412\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${workspaceId}`);
5524
6041
  return new GitRepo(workspace.path);
5525
6042
  }
6043
+ /**
6044
+ * Пишущая операция панели: её итог и новое состояние получают все клиенты.
6045
+ *
6046
+ * Broadcast, а не ответ спросившему, потому что рабочее дерево у вкладок
6047
+ * общее: после переключения ветки или появления `origin` вторая вкладка
6048
+ * показывала бы прежнюю картину, ничем не отличимую от настоящей.
6049
+ *
6050
+ * Ошибка сюда приходит текстом git и уходит текстом же — панель показывает
6051
+ * его как есть. Своей формулировки у нас нет и быть не должно: git объясняет
6052
+ * отказ («ваши правки будут перезаписаны», «обновлений нет») точнее.
6053
+ */
6054
+ async #gitOperation(workspaceId, run2) {
6055
+ let ok = true;
6056
+ let message = "";
6057
+ let url2;
6058
+ try {
6059
+ const outcome = await run2(this.#git(workspaceId));
6060
+ message = outcome.message;
6061
+ url2 = outcome.url;
6062
+ } catch (error) {
6063
+ ok = false;
6064
+ message = error instanceof Error ? error.message : String(error);
6065
+ }
6066
+ this.#broadcast({ type: "git.result", workspaceId, ok, message, ...url2 ? { url: url2 } : {} });
6067
+ const state = await this.#git(workspaceId).state().catch(() => null);
6068
+ if (state)
6069
+ this.#broadcast({ type: "git.state", workspaceId, state });
6070
+ }
6071
+ /**
6072
+ * То же, но операция ходит в сеть: под замком и с пометкой «идёт».
6073
+ *
6074
+ * Замок — не про удобство, а про целостность: два `push` на один `.git`
6075
+ * встретятся на блокировке индекса, и второй упадёт посреди работы. Пометка
6076
+ * рассылается всем по той же причине, по которой рассылается итог, — вкладки
6077
+ * смотрят в один каталог.
6078
+ */
6079
+ async #gitNetwork(workspaceId, label, run2) {
6080
+ if (this.#gitBusy.has(workspaceId)) {
6081
+ this.#broadcast({
6082
+ type: "git.result",
6083
+ workspaceId,
6084
+ ok: false,
6085
+ message: "\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u0441 remote \u0443\u0436\u0435 \u0438\u0434\u0451\u0442 \u2014 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0435\u0451"
6086
+ });
6087
+ return;
6088
+ }
6089
+ this.#gitBusy.add(workspaceId);
6090
+ this.#broadcast({ type: "git.busy", workspaceId, label });
6091
+ try {
6092
+ await this.#gitOperation(workspaceId, run2);
6093
+ } finally {
6094
+ this.#gitBusy.delete(workspaceId);
6095
+ this.#broadcast({ type: "git.busy", workspaceId, label: "" });
6096
+ }
6097
+ }
6098
+ /**
6099
+ * Токен GitHub для команды git — или пустая строка, если аккаунта нет.
6100
+ *
6101
+ * Пустая строка здесь законна: push на не-GitHub remote (или на GitHub через
6102
+ * уже настроенный ssh-ключ) должен работать ровно так же, как из терминала.
6103
+ * Требовать учётную запись GitHub ради него значило бы сломать то, что и без
6104
+ * нас работало.
6105
+ */
6106
+ async #gitToken() {
6107
+ return await this.#github.credential().then((credential) => credential.token, () => "");
6108
+ }
6109
+ /**
6110
+ * Опубликовать проект: коммит (если истории ещё нет), репозиторий, remote, push.
6111
+ *
6112
+ * Порядок не переставить. Коммит идёт до создания репозитория: не настроен
6113
+ * `user.email` — git откажет, и лучше это узнать до того, как на GitHub
6114
+ * появится пустой репозиторий, который придётся удалять руками. Репозиторий
6115
+ * создаётся до `origin`, потому что адрес приходит вместе с ним.
6116
+ */
6117
+ async #publish(repo, frame) {
6118
+ const credential = await this.#github.credential();
6119
+ if (!(await repo.state()).available)
6120
+ await repo.init();
6121
+ if (frame.message)
6122
+ await repo.commit(frame.message, frame.include, frame.ignore);
6123
+ const created = await this.#github.createRepo({
6124
+ owner: frame.owner,
6125
+ name: frame.name,
6126
+ description: frame.description,
6127
+ private: frame.private
6128
+ });
6129
+ await repo.setRemote(created.cloneUrl);
6130
+ try {
6131
+ await repo.push(credential.token);
6132
+ } catch (error) {
6133
+ const detail = error instanceof Error ? error.message : String(error);
6134
+ throw new Error(`\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 ${created.fullName} \u0441\u043E\u0437\u0434\u0430\u043D, \u043D\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E: ${detail}`);
6135
+ }
6136
+ return { message: `\u041E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043E: ${created.fullName}`, url: created.htmlUrl };
6137
+ }
5526
6138
  // -------------------------------------------------------------------------
5527
6139
  // Файлы проекта
5528
6140
  // -------------------------------------------------------------------------
@@ -5680,8 +6292,8 @@ var HostServer = class {
5680
6292
  return;
5681
6293
  }
5682
6294
  const url2 = new URL(req.url ?? "/", "http://localhost");
5683
- const requested = path15.join(webRoot, path15.normalize(url2.pathname));
5684
- const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path15.join(webRoot, "index.html");
6295
+ const requested = path17.join(webRoot, path17.normalize(url2.pathname));
6296
+ const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path17.join(webRoot, "index.html");
5685
6297
  if (!existsSync3(target)) {
5686
6298
  res.writeHead(404).end("not found");
5687
6299
  return;
@@ -5707,12 +6319,12 @@ function contentType(file) {
5707
6319
  ".svg": "image/svg+xml",
5708
6320
  ".woff2": "font/woff2"
5709
6321
  };
5710
- return types[path15.extname(file)] ?? "application/octet-stream";
6322
+ return types[path17.extname(file)] ?? "application/octet-stream";
5711
6323
  }
5712
6324
 
5713
6325
  // dist/ccv.js
5714
- var HERE = path16.dirname(fileURLToPath(import.meta.url));
5715
- var PACKAGE_ROOT = path16.resolve(HERE, "..");
6326
+ var HERE = path18.dirname(fileURLToPath(import.meta.url));
6327
+ var PACKAGE_ROOT = path18.resolve(HERE, "..");
5716
6328
  var USAGE = `ccv \u2014 Claude Code Visual: \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0445\u043E\u0441\u0442 Claude Code \u0441 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043E\u043C \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.
5717
6329
 
5718
6330
  \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
@@ -5779,7 +6391,7 @@ var server = new HostServer({
5779
6391
  dataDir: defaults.dataDir,
5780
6392
  // UI лежит в пакете рядом с бандлом — это и есть разница между установленной
5781
6393
  // утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
5782
- webRoot: process.env.CCV_WEB_ROOT ?? path16.join(PACKAGE_ROOT, "web"),
6394
+ webRoot: process.env.CCV_WEB_ROOT ?? path18.join(PACKAGE_ROOT, "web"),
5783
6395
  version,
5784
6396
  relayUrl: defaults.relayUrl
5785
6397
  });
@@ -5808,7 +6420,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
5808
6420
  }
5809
6421
  async function packageVersion() {
5810
6422
  try {
5811
- const text = await readFile11(path16.join(PACKAGE_ROOT, "package.json"), "utf8");
6423
+ const text = await readFile13(path18.join(PACKAGE_ROOT, "package.json"), "utf8");
5812
6424
  return JSON.parse(text).version ?? "0.0.0";
5813
6425
  } catch {
5814
6426
  return "0.0.0";