@i14ks/ccv 0.9.5 → 0.11.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/bundle/ccv.mjs +423 -121
- package/package.json +1 -1
- package/web/assets/{codemirror-BrML4DhQ.js → codemirror-BVMwXG_R.js} +1 -1
- package/web/assets/{index-BmDgU61K.js → index-AByrn5FE.js} +1 -1
- package/web/assets/{index-BZMuV7wL.js → index-BCAmULlL.js} +1 -1
- package/web/assets/{index-BnX4KThz.js → index-BFLfK-j6.js} +1 -1
- package/web/assets/{index-CIOcaugJ.js → index-BZ8Mi5_8.js} +1 -1
- package/web/assets/{index-CzJCmwIj.js → index-BlZDoET-.js} +1 -1
- package/web/assets/{index-Cg-P0Yp1.js → index-BsH-IJBy.js} +1 -1
- package/web/assets/{index-ZQEm-9IM.js → index-Bu_Ukh1_.js} +1 -1
- package/web/assets/{index-BMtQHCPD.js → index-C-geC5VJ.js} +1 -1
- package/web/assets/{index-B11vIedv.js → index-C1BT9XMC.js} +1 -1
- package/web/assets/{index-ZDWxlF_B.js → index-C59fVtiO.js} +1 -1
- package/web/assets/{index-MEYLoAjh.js → index-CPWAVXhz.js} +1 -1
- package/web/assets/{index-BCuOAg0C.js → index-CrW8SGCA.js} +1 -1
- package/web/assets/{index-BMlMiQH4.js → index-DAjl7nVl.js} +1 -1
- package/web/assets/{index-BH51XSCv.js → index-DJYXqBtm.js} +1 -1
- package/web/assets/{index-DueUeK4W.js → index-DKofSKA7.js} +1 -1
- package/web/assets/{index-TS7xklzI.js → index-DLDXVZaf.js} +1 -1
- package/web/assets/index-DRlKZHMW.js +132 -0
- package/web/assets/{index-kYR8PJFy.js → index-Ey94wZ1k.js} +1 -1
- package/web/assets/index-c0zvhlqR.css +1 -0
- package/web/assets/{index-MtnHR_Fv.js → index-gl1MvN4b.js} +1 -1
- package/web/assets/{index-CJJjf7QW.js → index-mnefozuu.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-D0UlXcXr.css +0 -1
- package/web/assets/index-DhmD70Mu.js +0 -132
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
|
|
6
|
-
import
|
|
5
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
6
|
+
import path19 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
@@ -146,9 +146,9 @@ 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
|
|
149
|
+
import path18 from "node:path";
|
|
150
150
|
import { homedir as homedir5 } from "node:os";
|
|
151
|
-
import { randomUUID as
|
|
151
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
152
152
|
import { WebSocketServer } from "ws";
|
|
153
153
|
import { renameSession as renameSession2 } from "@anthropic-ai/claude-agent-sdk";
|
|
154
154
|
|
|
@@ -218,6 +218,22 @@ var AsyncQueue = class {
|
|
|
218
218
|
import { cp, lstat, mkdir, readFile as readFile2, readdir, realpath, rename as renamePath, rm, stat, writeFile } from "node:fs/promises";
|
|
219
219
|
import path3 from "node:path";
|
|
220
220
|
var MAX_FILE = 4 * 1024 * 1024;
|
|
221
|
+
var MAX_MEDIA = 64 * 1024 * 1024;
|
|
222
|
+
var MEDIA_MIME = {
|
|
223
|
+
".png": "image/png",
|
|
224
|
+
".jpg": "image/jpeg",
|
|
225
|
+
".jpeg": "image/jpeg",
|
|
226
|
+
".gif": "image/gif",
|
|
227
|
+
".webp": "image/webp",
|
|
228
|
+
".bmp": "image/bmp",
|
|
229
|
+
".ico": "image/x-icon",
|
|
230
|
+
".avif": "image/avif",
|
|
231
|
+
".mp4": "video/mp4",
|
|
232
|
+
".webm": "video/webm",
|
|
233
|
+
".ogv": "video/ogg",
|
|
234
|
+
".mov": "video/quicktime",
|
|
235
|
+
".m4v": "video/mp4"
|
|
236
|
+
};
|
|
221
237
|
var SNIFF = 8192;
|
|
222
238
|
var HIDDEN = /* @__PURE__ */ new Set([".git"]);
|
|
223
239
|
var SEARCH_SKIP = /* @__PURE__ */ new Set([...HIDDEN, "node_modules"]);
|
|
@@ -409,6 +425,12 @@ var ProjectFiles = class {
|
|
|
409
425
|
binary: false,
|
|
410
426
|
tooLarge: false
|
|
411
427
|
};
|
|
428
|
+
const mediaMime = MEDIA_MIME[path3.extname(file).toLowerCase()];
|
|
429
|
+
if (mediaMime) {
|
|
430
|
+
if (info.size > MAX_MEDIA)
|
|
431
|
+
return { ...blank, binary: true, mediaMime, mediaTooLarge: true };
|
|
432
|
+
return { ...blank, binary: true, mediaMime };
|
|
433
|
+
}
|
|
412
434
|
if (info.size > MAX_FILE)
|
|
413
435
|
return { ...blank, tooLarge: true };
|
|
414
436
|
const buffer = await readFile2(file);
|
|
@@ -423,6 +445,28 @@ var ProjectFiles = class {
|
|
|
423
445
|
encoding: decoded.encoding
|
|
424
446
|
};
|
|
425
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Байты картинки или видео — по отдельному запросу от просмотрщика.
|
|
450
|
+
*
|
|
451
|
+
* `read` уже сказал клиенту, что это медиа (`mediaMime`) и что оно в пределах
|
|
452
|
+
* `MAX_MEDIA`, но сами байты не читал: кодировать в base64 файл, который
|
|
453
|
+
* никто не открыл (а `fs.read` шлют не только по клику — синхронизация,
|
|
454
|
+
* чужая правка на диске), значило бы жечь диск и сеть впустую. Здесь — та
|
|
455
|
+
* самая читка, по требованию.
|
|
456
|
+
*/
|
|
457
|
+
async readMedia(relative) {
|
|
458
|
+
const file = await this.#resolve(relative);
|
|
459
|
+
const mime = MEDIA_MIME[path3.extname(file).toLowerCase()];
|
|
460
|
+
if (!mime)
|
|
461
|
+
throw new Error("\u0424\u0430\u0439\u043B \u043D\u0435 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D \u043A\u0430\u043A \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u0438\u043B\u0438 \u0432\u0438\u0434\u0435\u043E");
|
|
462
|
+
const info = await stat(file);
|
|
463
|
+
if (info.isDirectory())
|
|
464
|
+
throw new Error("\u042D\u0442\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B");
|
|
465
|
+
if (info.size > MAX_MEDIA)
|
|
466
|
+
throw new Error("\u0424\u0430\u0439\u043B \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0432\u0435\u043B\u0438\u043A \u0434\u043B\u044F \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430");
|
|
467
|
+
const buffer = await readFile2(file);
|
|
468
|
+
return { mime, base64: buffer.toString("base64") };
|
|
469
|
+
}
|
|
426
470
|
/**
|
|
427
471
|
* Записать файл, если его на диске не подменили.
|
|
428
472
|
*
|
|
@@ -2959,9 +3003,9 @@ function parsePatch(raw) {
|
|
|
2959
3003
|
body = "";
|
|
2960
3004
|
}
|
|
2961
3005
|
total += body.length;
|
|
2962
|
-
const
|
|
3006
|
+
const path20 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
2963
3007
|
files.push({
|
|
2964
|
-
path:
|
|
3008
|
+
path: path20,
|
|
2965
3009
|
oldPath: status === "renamed" ? oldPath : "",
|
|
2966
3010
|
status,
|
|
2967
3011
|
added: count(body, "+"),
|
|
@@ -2973,10 +3017,10 @@ function parsePatch(raw) {
|
|
|
2973
3017
|
return { files, truncated };
|
|
2974
3018
|
}
|
|
2975
3019
|
function stripPrefix(value) {
|
|
2976
|
-
const
|
|
2977
|
-
if (
|
|
3020
|
+
const path20 = unquote(value.trim());
|
|
3021
|
+
if (path20 === "/dev/null")
|
|
2978
3022
|
return "";
|
|
2979
|
-
return
|
|
3023
|
+
return path20.replace(/^[ab]\//, "");
|
|
2980
3024
|
}
|
|
2981
3025
|
function count(body, mark) {
|
|
2982
3026
|
let n = 0;
|
|
@@ -3508,7 +3552,7 @@ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from
|
|
|
3508
3552
|
import path9 from "node:path";
|
|
3509
3553
|
import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
|
|
3510
3554
|
var WorkspaceRegistry = class {
|
|
3511
|
-
#
|
|
3555
|
+
#entries = [];
|
|
3512
3556
|
#storePath;
|
|
3513
3557
|
constructor(dataDir) {
|
|
3514
3558
|
this.#storePath = path9.join(dataDir, "workspaces.json");
|
|
@@ -3517,52 +3561,91 @@ var WorkspaceRegistry = class {
|
|
|
3517
3561
|
try {
|
|
3518
3562
|
const raw = await readFile6(this.#storePath, "utf8");
|
|
3519
3563
|
const parsed = JSON.parse(raw);
|
|
3520
|
-
if (Array.isArray(parsed)) {
|
|
3521
|
-
this.#
|
|
3564
|
+
if (!Array.isArray(parsed)) {
|
|
3565
|
+
this.#entries = [];
|
|
3566
|
+
return;
|
|
3522
3567
|
}
|
|
3568
|
+
this.#entries = parsed.map((item) => {
|
|
3569
|
+
if (typeof item === "string")
|
|
3570
|
+
return { path: item };
|
|
3571
|
+
if (isEntry(item))
|
|
3572
|
+
return item;
|
|
3573
|
+
return null;
|
|
3574
|
+
}).filter((e) => e !== null && existsSync2(e.path));
|
|
3523
3575
|
} catch {
|
|
3524
|
-
this.#
|
|
3576
|
+
this.#entries = [];
|
|
3525
3577
|
}
|
|
3526
3578
|
}
|
|
3527
3579
|
async #persist() {
|
|
3528
3580
|
await mkdir3(path9.dirname(this.#storePath), { recursive: true });
|
|
3529
|
-
await writeFile4(this.#storePath, JSON.stringify(this.#
|
|
3581
|
+
await writeFile4(this.#storePath, JSON.stringify(this.#entries, null, 2), "utf8");
|
|
3530
3582
|
}
|
|
3583
|
+
/** Активные проекты. */
|
|
3531
3584
|
list() {
|
|
3532
|
-
return this.#
|
|
3585
|
+
return this.#entries.filter((e) => !e.archivedAt).map((e) => toInfo(e));
|
|
3586
|
+
}
|
|
3587
|
+
/** Закрытые проекты — архив, симметричный `list()`. */
|
|
3588
|
+
listArchived() {
|
|
3589
|
+
return this.#entries.filter((e) => e.archivedAt).map((e) => toInfo(e));
|
|
3533
3590
|
}
|
|
3534
3591
|
find(id) {
|
|
3535
|
-
|
|
3592
|
+
const entry = this.#entries.find((e) => workspaceIdFromPath(e.path) === id);
|
|
3593
|
+
return entry ? toInfo(entry) : void 0;
|
|
3536
3594
|
}
|
|
3537
3595
|
async open(rawPath) {
|
|
3538
3596
|
const absolute = path9.resolve(rawPath);
|
|
3539
3597
|
if (!existsSync2(absolute)) {
|
|
3540
3598
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
|
|
3541
3599
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
3600
|
+
const existing = this.#entries.find((e) => e.path === absolute);
|
|
3601
|
+
if (!existing) {
|
|
3602
|
+
this.#entries.unshift({ path: absolute });
|
|
3603
|
+
await this.#persist();
|
|
3604
|
+
} else if (existing.archivedAt) {
|
|
3605
|
+
delete existing.archivedAt;
|
|
3544
3606
|
await this.#persist();
|
|
3545
3607
|
}
|
|
3546
|
-
return toInfo(absolute);
|
|
3608
|
+
return toInfo({ path: absolute });
|
|
3547
3609
|
}
|
|
3548
3610
|
/**
|
|
3549
|
-
* Убрать проекты из списка
|
|
3611
|
+
* Убрать проекты из списка открытых — в архив, а не с концами.
|
|
3550
3612
|
*
|
|
3551
|
-
* Только реестр: каталог, его файлы и транскрипты сессий остаются на
|
|
3552
|
-
*
|
|
3553
|
-
*
|
|
3554
|
-
*
|
|
3613
|
+
* Только реестр: каталог, его файлы и транскрипты сессий остаются на месте.
|
|
3614
|
+
* Запись при этом не удаляется, а помечается временем закрытия — проект
|
|
3615
|
+
* возвращается в активный список через `restore()`, без повторного поиска
|
|
3616
|
+
* каталога. Пачкой, а не по одному, потому что список чистят разом и
|
|
3617
|
+
* переписывать файл на каждый проект незачем.
|
|
3555
3618
|
*/
|
|
3556
3619
|
async close(ids) {
|
|
3557
3620
|
const doomed = new Set(ids);
|
|
3558
|
-
const closed =
|
|
3621
|
+
const closed = [];
|
|
3622
|
+
const at = Date.now();
|
|
3623
|
+
for (const entry of this.#entries) {
|
|
3624
|
+
if (entry.archivedAt || !doomed.has(workspaceIdFromPath(entry.path)))
|
|
3625
|
+
continue;
|
|
3626
|
+
entry.archivedAt = at;
|
|
3627
|
+
closed.push(toInfo(entry));
|
|
3628
|
+
}
|
|
3559
3629
|
if (closed.length === 0)
|
|
3560
3630
|
return [];
|
|
3561
|
-
const gone = new Set(closed.map((w) => w.path));
|
|
3562
|
-
this.#paths = this.#paths.filter((p) => !gone.has(p));
|
|
3563
3631
|
await this.#persist();
|
|
3564
3632
|
return closed;
|
|
3565
3633
|
}
|
|
3634
|
+
/** Вернуть проекты из архива в активный список. */
|
|
3635
|
+
async restore(ids) {
|
|
3636
|
+
const wanted = new Set(ids);
|
|
3637
|
+
const restored = [];
|
|
3638
|
+
for (const entry of this.#entries) {
|
|
3639
|
+
if (!entry.archivedAt || !wanted.has(workspaceIdFromPath(entry.path)))
|
|
3640
|
+
continue;
|
|
3641
|
+
delete entry.archivedAt;
|
|
3642
|
+
restored.push(toInfo(entry));
|
|
3643
|
+
}
|
|
3644
|
+
if (restored.length === 0)
|
|
3645
|
+
return [];
|
|
3646
|
+
await this.#persist();
|
|
3647
|
+
return restored;
|
|
3648
|
+
}
|
|
3566
3649
|
/**
|
|
3567
3650
|
* Создать каталог проекта и открыть его.
|
|
3568
3651
|
*
|
|
@@ -3661,14 +3744,19 @@ function claudeMdStub(name) {
|
|
|
3661
3744
|
function readmeStub(name) {
|
|
3662
3745
|
return [`# ${name}`, "", "<!-- \u041E\u0434\u043D\u0430-\u0434\u0432\u0435 \u0444\u0440\u0430\u0437\u044B: \u0447\u0442\u043E \u044D\u0442\u043E \u0438 \u0437\u0430\u0447\u0435\u043C. -->", ""].join("\n");
|
|
3663
3746
|
}
|
|
3664
|
-
function toInfo(
|
|
3747
|
+
function toInfo(entry) {
|
|
3748
|
+
const { path: absolute, archivedAt } = entry;
|
|
3665
3749
|
return {
|
|
3666
3750
|
id: workspaceIdFromPath(absolute),
|
|
3667
3751
|
path: absolute,
|
|
3668
3752
|
name: path9.basename(absolute) || absolute,
|
|
3669
|
-
hasClaudeMd: existsSync2(path9.join(absolute, "CLAUDE.md"))
|
|
3753
|
+
hasClaudeMd: existsSync2(path9.join(absolute, "CLAUDE.md")),
|
|
3754
|
+
...archivedAt ? { archivedAt } : {}
|
|
3670
3755
|
};
|
|
3671
3756
|
}
|
|
3757
|
+
function isEntry(value) {
|
|
3758
|
+
return typeof value === "object" && value !== null && typeof value.path === "string" && (value.archivedAt === void 0 || typeof value.archivedAt === "number");
|
|
3759
|
+
}
|
|
3672
3760
|
|
|
3673
3761
|
// ../host/dist/archive.js
|
|
3674
3762
|
import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
@@ -3868,38 +3956,181 @@ var SessionPins = class {
|
|
|
3868
3956
|
await this.#persist();
|
|
3869
3957
|
return changed;
|
|
3870
3958
|
}
|
|
3959
|
+
async #persist() {
|
|
3960
|
+
const file = { pinned: Object.fromEntries(this.#pinned) };
|
|
3961
|
+
await mkdir5(path11.dirname(this.#storePath), { recursive: true });
|
|
3962
|
+
await writeFile6(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
function isRecord2(value) {
|
|
3966
|
+
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// ../host/dist/folders.js
|
|
3970
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
|
|
3971
|
+
import path12 from "node:path";
|
|
3972
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3973
|
+
var Folders = class {
|
|
3974
|
+
#workspaceFolders = /* @__PURE__ */ new Map();
|
|
3975
|
+
#sessionFolders = /* @__PURE__ */ new Map();
|
|
3976
|
+
#workspaceAssignments = /* @__PURE__ */ new Map();
|
|
3977
|
+
#sessionAssignments = /* @__PURE__ */ new Map();
|
|
3978
|
+
#storePath;
|
|
3979
|
+
constructor(dataDir) {
|
|
3980
|
+
this.#storePath = path12.join(dataDir, "folders.json");
|
|
3981
|
+
}
|
|
3982
|
+
async load() {
|
|
3983
|
+
try {
|
|
3984
|
+
const raw = await readFile9(this.#storePath, "utf8");
|
|
3985
|
+
const parsed = JSON.parse(raw);
|
|
3986
|
+
this.#workspaceFolders = new Map(Object.entries(parsed.workspaceFolders ?? {}).filter(([, r]) => isWorkspaceFolder(r)));
|
|
3987
|
+
this.#sessionFolders = new Map(Object.entries(parsed.sessionFolders ?? {}).filter(([, r]) => isSessionFolder(r)));
|
|
3988
|
+
this.#workspaceAssignments = new Map(Object.entries(parsed.workspaceAssignments ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
3989
|
+
this.#sessionAssignments = new Map(Object.entries(parsed.sessionAssignments ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
3990
|
+
} catch {
|
|
3991
|
+
this.#workspaceFolders = /* @__PURE__ */ new Map();
|
|
3992
|
+
this.#sessionFolders = /* @__PURE__ */ new Map();
|
|
3993
|
+
this.#workspaceAssignments = /* @__PURE__ */ new Map();
|
|
3994
|
+
this.#sessionAssignments = /* @__PURE__ */ new Map();
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
/** Все папки — для кадра `folder.list`. */
|
|
3998
|
+
list() {
|
|
3999
|
+
return {
|
|
4000
|
+
workspaceFolders: [...this.#workspaceFolders].map(([id, r]) => ({ id, name: r.name })),
|
|
4001
|
+
sessionFolders: [...this.#sessionFolders].map(([id, r]) => ({
|
|
4002
|
+
id,
|
|
4003
|
+
name: r.name,
|
|
4004
|
+
workspaceId: r.workspaceId
|
|
4005
|
+
}))
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
async createWorkspaceFolder(name) {
|
|
4009
|
+
const id = randomUUID2();
|
|
4010
|
+
this.#workspaceFolders.set(id, { name });
|
|
4011
|
+
await this.#persist();
|
|
4012
|
+
return id;
|
|
4013
|
+
}
|
|
4014
|
+
async createSessionFolder(workspaceId, name) {
|
|
4015
|
+
const id = randomUUID2();
|
|
4016
|
+
this.#sessionFolders.set(id, { name, workspaceId });
|
|
4017
|
+
await this.#persist();
|
|
4018
|
+
return id;
|
|
4019
|
+
}
|
|
4020
|
+
/** Переименовать папку любого вида — id уникален между обоими видами. */
|
|
4021
|
+
async rename(folderId, name) {
|
|
4022
|
+
const ws = this.#workspaceFolders.get(folderId);
|
|
4023
|
+
if (ws) {
|
|
4024
|
+
this.#workspaceFolders.set(folderId, { ...ws, name });
|
|
4025
|
+
await this.#persist();
|
|
4026
|
+
return true;
|
|
4027
|
+
}
|
|
4028
|
+
const sess = this.#sessionFolders.get(folderId);
|
|
4029
|
+
if (sess) {
|
|
4030
|
+
this.#sessionFolders.set(folderId, { ...sess, name });
|
|
4031
|
+
await this.#persist();
|
|
4032
|
+
return true;
|
|
4033
|
+
}
|
|
4034
|
+
return false;
|
|
4035
|
+
}
|
|
3871
4036
|
/**
|
|
3872
|
-
*
|
|
4037
|
+
* Удалить папку. Элементы внутри неё не трогаются — только перестают на неё
|
|
4038
|
+
* указывать, то есть становятся вне папок.
|
|
3873
4039
|
*
|
|
3874
|
-
*
|
|
3875
|
-
*
|
|
4040
|
+
* Возвращает, какого вида была удалённая папка (и, для папки сессий, её
|
|
4041
|
+
* проект) — вызывающему это нужно, чтобы разослать обновление только туда,
|
|
4042
|
+
* где папка была видна, а не всем спискам сразу.
|
|
3876
4043
|
*/
|
|
3877
|
-
async
|
|
3878
|
-
let
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
this.#
|
|
3883
|
-
|
|
4044
|
+
async delete(folderId) {
|
|
4045
|
+
let removed = null;
|
|
4046
|
+
if (this.#workspaceFolders.delete(folderId)) {
|
|
4047
|
+
removed = { scope: "workspace" };
|
|
4048
|
+
} else {
|
|
4049
|
+
const sess = this.#sessionFolders.get(folderId);
|
|
4050
|
+
if (sess && this.#sessionFolders.delete(folderId)) {
|
|
4051
|
+
removed = { scope: "session", workspaceId: sess.workspaceId };
|
|
4052
|
+
}
|
|
3884
4053
|
}
|
|
3885
|
-
if (
|
|
4054
|
+
if (!removed)
|
|
4055
|
+
return null;
|
|
4056
|
+
for (const [id, assigned] of this.#workspaceAssignments) {
|
|
4057
|
+
if (assigned === folderId)
|
|
4058
|
+
this.#workspaceAssignments.delete(id);
|
|
4059
|
+
}
|
|
4060
|
+
for (const [id, assigned] of this.#sessionAssignments) {
|
|
4061
|
+
if (assigned === folderId)
|
|
4062
|
+
this.#sessionAssignments.delete(id);
|
|
4063
|
+
}
|
|
4064
|
+
await this.#persist();
|
|
4065
|
+
return removed;
|
|
4066
|
+
}
|
|
4067
|
+
/** Разложить проект по папке или вынуть из неё (`folderId: null`). */
|
|
4068
|
+
async assignWorkspace(workspaceId, folderId) {
|
|
4069
|
+
if (folderId === null) {
|
|
4070
|
+
if (!this.#workspaceAssignments.delete(workspaceId))
|
|
4071
|
+
return false;
|
|
3886
4072
|
await this.#persist();
|
|
3887
|
-
|
|
4073
|
+
return true;
|
|
4074
|
+
}
|
|
4075
|
+
if (!this.#workspaceFolders.has(folderId)) {
|
|
4076
|
+
throw new Error(`\u041F\u0430\u043F\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ${folderId}`);
|
|
4077
|
+
}
|
|
4078
|
+
this.#workspaceAssignments.set(workspaceId, folderId);
|
|
4079
|
+
await this.#persist();
|
|
4080
|
+
return true;
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Разложить сессию по папке или вынуть из неё.
|
|
4084
|
+
*
|
|
4085
|
+
* Папка сессии привязана к проекту — раскладывать сессию в папку чужого
|
|
4086
|
+
* проекта нельзя, иначе список одного проекта показывал бы группу, для
|
|
4087
|
+
* которой в нём нет ни одной другой строки.
|
|
4088
|
+
*/
|
|
4089
|
+
async assignSession(sessionId, workspaceId, folderId) {
|
|
4090
|
+
if (folderId === null) {
|
|
4091
|
+
if (!this.#sessionAssignments.delete(sessionId))
|
|
4092
|
+
return false;
|
|
4093
|
+
await this.#persist();
|
|
4094
|
+
return true;
|
|
4095
|
+
}
|
|
4096
|
+
const folder = this.#sessionFolders.get(folderId);
|
|
4097
|
+
if (!folder)
|
|
4098
|
+
throw new Error(`\u041F\u0430\u043F\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ${folderId}`);
|
|
4099
|
+
if (folder.workspaceId !== workspaceId) {
|
|
4100
|
+
throw new Error("\u041F\u0430\u043F\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u0439 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442 \u0434\u0440\u0443\u0433\u043E\u043C\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0443");
|
|
4101
|
+
}
|
|
4102
|
+
this.#sessionAssignments.set(sessionId, folderId);
|
|
4103
|
+
await this.#persist();
|
|
4104
|
+
return true;
|
|
4105
|
+
}
|
|
4106
|
+
folderIdForWorkspace(workspaceId) {
|
|
4107
|
+
return this.#workspaceAssignments.get(workspaceId);
|
|
4108
|
+
}
|
|
4109
|
+
folderIdForSession(sessionId) {
|
|
4110
|
+
return this.#sessionAssignments.get(sessionId);
|
|
3888
4111
|
}
|
|
3889
4112
|
async #persist() {
|
|
3890
|
-
const file = {
|
|
3891
|
-
|
|
3892
|
-
|
|
4113
|
+
const file = {
|
|
4114
|
+
workspaceFolders: Object.fromEntries(this.#workspaceFolders),
|
|
4115
|
+
sessionFolders: Object.fromEntries(this.#sessionFolders),
|
|
4116
|
+
workspaceAssignments: Object.fromEntries(this.#workspaceAssignments),
|
|
4117
|
+
sessionAssignments: Object.fromEntries(this.#sessionAssignments)
|
|
4118
|
+
};
|
|
4119
|
+
await mkdir6(path12.dirname(this.#storePath), { recursive: true });
|
|
4120
|
+
await writeFile7(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
3893
4121
|
}
|
|
3894
4122
|
};
|
|
3895
|
-
function
|
|
3896
|
-
return typeof value === "object" && value !== null && typeof value.
|
|
4123
|
+
function isWorkspaceFolder(value) {
|
|
4124
|
+
return typeof value === "object" && value !== null && typeof value.name === "string";
|
|
4125
|
+
}
|
|
4126
|
+
function isSessionFolder(value) {
|
|
4127
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.workspaceId === "string";
|
|
3897
4128
|
}
|
|
3898
4129
|
|
|
3899
4130
|
// ../host/dist/app-settings.js
|
|
3900
|
-
import { mkdir as
|
|
4131
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
|
|
3901
4132
|
import { homedir as homedir3 } from "node:os";
|
|
3902
|
-
import
|
|
4133
|
+
import path13 from "node:path";
|
|
3903
4134
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
3904
4135
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
3905
4136
|
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
@@ -3934,11 +4165,11 @@ var AppSettingsStore = class {
|
|
|
3934
4165
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
3935
4166
|
#storePath;
|
|
3936
4167
|
constructor(dataDir) {
|
|
3937
|
-
this.#storePath =
|
|
4168
|
+
this.#storePath = path13.join(dataDir, "app-settings.json");
|
|
3938
4169
|
}
|
|
3939
4170
|
async load() {
|
|
3940
4171
|
try {
|
|
3941
|
-
const raw = await
|
|
4172
|
+
const raw = await readFile10(this.#storePath, "utf8");
|
|
3942
4173
|
this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
3943
4174
|
} catch {
|
|
3944
4175
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -3958,8 +4189,8 @@ var AppSettingsStore = class {
|
|
|
3958
4189
|
async update(patch) {
|
|
3959
4190
|
const next = sanitize(patch, this.#settings);
|
|
3960
4191
|
this.#settings = next;
|
|
3961
|
-
await
|
|
3962
|
-
await
|
|
4192
|
+
await mkdir7(path13.dirname(this.#storePath), { recursive: true });
|
|
4193
|
+
await writeFile8(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
3963
4194
|
return this.current;
|
|
3964
4195
|
}
|
|
3965
4196
|
};
|
|
@@ -3997,7 +4228,7 @@ function sanitize(value, fallback) {
|
|
|
3997
4228
|
return result;
|
|
3998
4229
|
}
|
|
3999
4230
|
function claudeHome() {
|
|
4000
|
-
return
|
|
4231
|
+
return path13.join(homedir3(), ".claude");
|
|
4001
4232
|
}
|
|
4002
4233
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
4003
4234
|
if (mode === "all")
|
|
@@ -4007,7 +4238,7 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4007
4238
|
return [...roots];
|
|
4008
4239
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
4009
4240
|
roots.add(workspace);
|
|
4010
|
-
const parent =
|
|
4241
|
+
const parent = path13.dirname(workspace);
|
|
4011
4242
|
if (parent !== workspace)
|
|
4012
4243
|
roots.add(parent);
|
|
4013
4244
|
}
|
|
@@ -4015,9 +4246,9 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4015
4246
|
}
|
|
4016
4247
|
|
|
4017
4248
|
// ../host/dist/plugins.js
|
|
4018
|
-
import { readFile as
|
|
4249
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
4019
4250
|
import { homedir as homedir4 } from "node:os";
|
|
4020
|
-
import
|
|
4251
|
+
import path14 from "node:path";
|
|
4021
4252
|
var TIMEOUT_MS2 = 12e4;
|
|
4022
4253
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
4023
4254
|
var PluginCli = class {
|
|
@@ -4116,10 +4347,10 @@ async function readCatalog(markets, installed) {
|
|
|
4116
4347
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
4117
4348
|
}
|
|
4118
4349
|
async function readMarketplace(market, installed) {
|
|
4119
|
-
const root = market.installLocation ||
|
|
4350
|
+
const root = market.installLocation || path14.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
|
|
4120
4351
|
let manifest;
|
|
4121
4352
|
try {
|
|
4122
|
-
const text = await
|
|
4353
|
+
const text = await readFile11(path14.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
4123
4354
|
manifest = JSON.parse(text);
|
|
4124
4355
|
} catch {
|
|
4125
4356
|
return [];
|
|
@@ -4360,8 +4591,8 @@ function describe4(error) {
|
|
|
4360
4591
|
|
|
4361
4592
|
// ../host/dist/relay-link.js
|
|
4362
4593
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4363
|
-
import { readFile as
|
|
4364
|
-
import
|
|
4594
|
+
import { readFile as readFile12, writeFile as writeFile9, rm as rm3, mkdir as mkdir8 } from "node:fs/promises";
|
|
4595
|
+
import path15 from "node:path";
|
|
4365
4596
|
import { hostname } from "node:os";
|
|
4366
4597
|
import WebSocket from "ws";
|
|
4367
4598
|
|
|
@@ -4416,7 +4647,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4416
4647
|
#log;
|
|
4417
4648
|
constructor(options2) {
|
|
4418
4649
|
super();
|
|
4419
|
-
this.#file =
|
|
4650
|
+
this.#file = path15.join(options2.dataDir, "relay.json");
|
|
4420
4651
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
4421
4652
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
4422
4653
|
}
|
|
@@ -4497,7 +4728,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4497
4728
|
}
|
|
4498
4729
|
async #read() {
|
|
4499
4730
|
try {
|
|
4500
|
-
const raw = await
|
|
4731
|
+
const raw = await readFile12(this.#file, "utf8");
|
|
4501
4732
|
const parsed = JSON.parse(raw);
|
|
4502
4733
|
if (typeof parsed !== "object" || parsed === null)
|
|
4503
4734
|
return null;
|
|
@@ -4510,8 +4741,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4510
4741
|
}
|
|
4511
4742
|
}
|
|
4512
4743
|
async #write(credentials) {
|
|
4513
|
-
await
|
|
4514
|
-
await
|
|
4744
|
+
await mkdir8(path15.dirname(this.#file), { recursive: true });
|
|
4745
|
+
await writeFile9(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
4515
4746
|
`, {
|
|
4516
4747
|
encoding: "utf8",
|
|
4517
4748
|
mode: 384
|
|
@@ -4768,8 +4999,8 @@ function humanize(error) {
|
|
|
4768
4999
|
// ../host/dist/terminal.js
|
|
4769
5000
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
4770
5001
|
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
4771
|
-
import
|
|
4772
|
-
import { randomUUID as
|
|
5002
|
+
import path16 from "node:path";
|
|
5003
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4773
5004
|
var SCROLLBACK_BYTES = 256 * 1024;
|
|
4774
5005
|
var FLUSH_MS = 12;
|
|
4775
5006
|
var MAX_TERMINALS = 24;
|
|
@@ -4806,7 +5037,7 @@ var TerminalHub = class extends EventEmitter4 {
|
|
|
4806
5037
|
env: environment()
|
|
4807
5038
|
});
|
|
4808
5039
|
const info = {
|
|
4809
|
-
id:
|
|
5040
|
+
id: randomUUID3(),
|
|
4810
5041
|
workspaceId: options2.workspaceId,
|
|
4811
5042
|
title: shell.title,
|
|
4812
5043
|
shell: shell.file,
|
|
@@ -4938,22 +5169,22 @@ async function defaultShell() {
|
|
|
4938
5169
|
if (pwsh)
|
|
4939
5170
|
return { file: pwsh, args: [], title: "pwsh" };
|
|
4940
5171
|
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
4941
|
-
const powershell =
|
|
5172
|
+
const powershell = path16.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
4942
5173
|
if (await exists2(powershell))
|
|
4943
5174
|
return { file: powershell, args: [], title: "powershell" };
|
|
4944
5175
|
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
4945
5176
|
}
|
|
4946
5177
|
const shell = process.env.SHELL;
|
|
4947
5178
|
if (shell && await exists2(shell))
|
|
4948
|
-
return { file: shell, args: [], title:
|
|
5179
|
+
return { file: shell, args: [], title: path16.basename(shell) };
|
|
4949
5180
|
if (await exists2("/bin/bash"))
|
|
4950
5181
|
return { file: "/bin/bash", args: [], title: "bash" };
|
|
4951
5182
|
return { file: "/bin/sh", args: [], title: "sh" };
|
|
4952
5183
|
}
|
|
4953
5184
|
async function findOnPath(name) {
|
|
4954
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
5185
|
+
const dirs = (process.env.PATH ?? "").split(path16.delimiter).filter(Boolean);
|
|
4955
5186
|
for (const dir of dirs) {
|
|
4956
|
-
const candidate =
|
|
5187
|
+
const candidate = path16.join(dir, name);
|
|
4957
5188
|
if (await exists2(candidate))
|
|
4958
5189
|
return candidate;
|
|
4959
5190
|
}
|
|
@@ -4964,10 +5195,10 @@ function exists2(file) {
|
|
|
4964
5195
|
}
|
|
4965
5196
|
|
|
4966
5197
|
// ../host/dist/mcp-config.js
|
|
4967
|
-
import { readFile as
|
|
4968
|
-
import
|
|
5198
|
+
import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir9 } from "node:fs/promises";
|
|
5199
|
+
import path17 from "node:path";
|
|
4969
5200
|
var MCP_FILE = ".mcp.json";
|
|
4970
|
-
var LOCAL_SETTINGS2 =
|
|
5201
|
+
var LOCAL_SETTINGS2 = path17.join(".claude", "settings.local.json");
|
|
4971
5202
|
var queues = /* @__PURE__ */ new Map();
|
|
4972
5203
|
function serialize(cwd, work) {
|
|
4973
5204
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -4976,7 +5207,7 @@ function serialize(cwd, work) {
|
|
|
4976
5207
|
return next;
|
|
4977
5208
|
}
|
|
4978
5209
|
async function readProjectServers(cwd) {
|
|
4979
|
-
const file = await readJson(
|
|
5210
|
+
const file = await readJson(path17.join(cwd, MCP_FILE));
|
|
4980
5211
|
const servers = file?.mcpServers;
|
|
4981
5212
|
if (!servers || typeof servers !== "object")
|
|
4982
5213
|
return {};
|
|
@@ -4984,23 +5215,23 @@ async function readProjectServers(cwd) {
|
|
|
4984
5215
|
}
|
|
4985
5216
|
function addProjectServer(cwd, name, config) {
|
|
4986
5217
|
return serialize(cwd, async () => {
|
|
4987
|
-
const file = await readJson(
|
|
5218
|
+
const file = await readJson(path17.join(cwd, MCP_FILE)) ?? {};
|
|
4988
5219
|
const servers = file.mcpServers ?? {};
|
|
4989
5220
|
servers[name] = config;
|
|
4990
5221
|
file.mcpServers = servers;
|
|
4991
|
-
await writeJson(
|
|
5222
|
+
await writeJson(path17.join(cwd, MCP_FILE), file);
|
|
4992
5223
|
await approve(cwd, name);
|
|
4993
5224
|
});
|
|
4994
5225
|
}
|
|
4995
5226
|
function removeProjectServer(cwd, name) {
|
|
4996
5227
|
return serialize(cwd, async () => {
|
|
4997
|
-
const file = await readJson(
|
|
5228
|
+
const file = await readJson(path17.join(cwd, MCP_FILE));
|
|
4998
5229
|
const servers = file?.mcpServers;
|
|
4999
5230
|
if (!servers || !(name in servers))
|
|
5000
5231
|
return false;
|
|
5001
5232
|
delete servers[name];
|
|
5002
|
-
await writeJson(
|
|
5003
|
-
const settingsPath =
|
|
5233
|
+
await writeJson(path17.join(cwd, MCP_FILE), file);
|
|
5234
|
+
const settingsPath = path17.join(cwd, LOCAL_SETTINGS2);
|
|
5004
5235
|
const settings = await readJson(settingsPath);
|
|
5005
5236
|
const enabled = settings?.enabledMcpjsonServers;
|
|
5006
5237
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -5011,7 +5242,7 @@ function removeProjectServer(cwd, name) {
|
|
|
5011
5242
|
});
|
|
5012
5243
|
}
|
|
5013
5244
|
async function approve(cwd, name) {
|
|
5014
|
-
const file =
|
|
5245
|
+
const file = path17.join(cwd, LOCAL_SETTINGS2);
|
|
5015
5246
|
const settings = await readJson(file) ?? {};
|
|
5016
5247
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
5017
5248
|
if (!enabled.includes(name))
|
|
@@ -5050,21 +5281,21 @@ function describeConfig(config) {
|
|
|
5050
5281
|
}
|
|
5051
5282
|
async function readJson(file) {
|
|
5052
5283
|
try {
|
|
5053
|
-
const raw = await
|
|
5284
|
+
const raw = await readFile13(file, "utf8");
|
|
5054
5285
|
const parsed = JSON.parse(raw);
|
|
5055
5286
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5056
5287
|
} catch (error) {
|
|
5057
5288
|
if (error.code === "ENOENT")
|
|
5058
5289
|
return null;
|
|
5059
5290
|
if (error instanceof SyntaxError) {
|
|
5060
|
-
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${
|
|
5291
|
+
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path17.basename(file)}: ${error.message}`);
|
|
5061
5292
|
}
|
|
5062
5293
|
throw error;
|
|
5063
5294
|
}
|
|
5064
5295
|
}
|
|
5065
5296
|
async function writeJson(file, data) {
|
|
5066
|
-
await
|
|
5067
|
-
await
|
|
5297
|
+
await mkdir9(path17.dirname(file), { recursive: true });
|
|
5298
|
+
await writeFile10(file, `${JSON.stringify(data, null, 2)}
|
|
5068
5299
|
`, "utf8");
|
|
5069
5300
|
}
|
|
5070
5301
|
|
|
@@ -5078,6 +5309,7 @@ var HostServer = class {
|
|
|
5078
5309
|
#workspaces;
|
|
5079
5310
|
#archive;
|
|
5080
5311
|
#pins;
|
|
5312
|
+
#folders;
|
|
5081
5313
|
#settings;
|
|
5082
5314
|
#finder;
|
|
5083
5315
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -5095,7 +5327,7 @@ var HostServer = class {
|
|
|
5095
5327
|
#clients = /* @__PURE__ */ new Set();
|
|
5096
5328
|
/** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
|
|
5097
5329
|
#remotes = /* @__PURE__ */ new Map();
|
|
5098
|
-
#hostId =
|
|
5330
|
+
#hostId = randomUUID4();
|
|
5099
5331
|
#plugins = new PluginCli();
|
|
5100
5332
|
#auth = new AuthCli();
|
|
5101
5333
|
#relay;
|
|
@@ -5115,6 +5347,7 @@ var HostServer = class {
|
|
|
5115
5347
|
this.#workspaces = new WorkspaceRegistry(options2.dataDir);
|
|
5116
5348
|
this.#archive = new SessionArchive(options2.dataDir);
|
|
5117
5349
|
this.#pins = new SessionPins(options2.dataDir);
|
|
5350
|
+
this.#folders = new Folders(options2.dataDir);
|
|
5118
5351
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
5119
5352
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
5120
5353
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
@@ -5132,6 +5365,7 @@ var HostServer = class {
|
|
|
5132
5365
|
await this.#workspaces.load();
|
|
5133
5366
|
await this.#archive.load();
|
|
5134
5367
|
await this.#pins.load();
|
|
5368
|
+
await this.#folders.load();
|
|
5135
5369
|
await this.#settings.load();
|
|
5136
5370
|
await this.#github.load();
|
|
5137
5371
|
await warmClaudeLauncher();
|
|
@@ -5281,6 +5515,27 @@ var HostServer = class {
|
|
|
5281
5515
|
client.send(frame);
|
|
5282
5516
|
}
|
|
5283
5517
|
}
|
|
5518
|
+
/** Активные и архивные проекты разом, с папкой каждого — для `workspace.list`. */
|
|
5519
|
+
#workspaceLists() {
|
|
5520
|
+
const withFolder = (w) => {
|
|
5521
|
+
const folderId = this.#folders.folderIdForWorkspace(w.id);
|
|
5522
|
+
return folderId ? { ...w, folderId } : w;
|
|
5523
|
+
};
|
|
5524
|
+
return {
|
|
5525
|
+
workspaces: this.#workspaces.list().map(withFolder),
|
|
5526
|
+
archived: this.#workspaces.listArchived().map(withFolder)
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
#broadcastWorkspaces() {
|
|
5530
|
+
this.#broadcast({ type: "workspace.list", ...this.#workspaceLists() });
|
|
5531
|
+
}
|
|
5532
|
+
#sendFolders(client) {
|
|
5533
|
+
const frame = { type: "folder.list", ...this.#folders.list() };
|
|
5534
|
+
if (client)
|
|
5535
|
+
this.#send(client, frame);
|
|
5536
|
+
else
|
|
5537
|
+
this.#broadcast(frame);
|
|
5538
|
+
}
|
|
5284
5539
|
async #handleFrame(socket, frame) {
|
|
5285
5540
|
switch (frame.type) {
|
|
5286
5541
|
case "hello":
|
|
@@ -5291,10 +5546,8 @@ var HostServer = class {
|
|
|
5291
5546
|
hostVersion: this.#options.version,
|
|
5292
5547
|
platform: process.platform
|
|
5293
5548
|
});
|
|
5294
|
-
this.#send(socket, {
|
|
5295
|
-
|
|
5296
|
-
workspaces: this.#workspaces.list()
|
|
5297
|
-
});
|
|
5549
|
+
this.#send(socket, { type: "workspace.list", ...this.#workspaceLists() });
|
|
5550
|
+
this.#sendFolders(socket);
|
|
5298
5551
|
this.#auth.status().then((status) => this.#send(socket, { type: "auth.status", status })).catch(() => void 0);
|
|
5299
5552
|
this.#send(socket, { type: "relay.state", state: this.#relay.state });
|
|
5300
5553
|
return;
|
|
@@ -5302,17 +5555,11 @@ var HostServer = class {
|
|
|
5302
5555
|
this.#send(socket, { type: "pong", nonce: frame.nonce });
|
|
5303
5556
|
return;
|
|
5304
5557
|
case "workspace.list":
|
|
5305
|
-
this.#send(socket, {
|
|
5306
|
-
type: "workspace.list",
|
|
5307
|
-
workspaces: this.#workspaces.list()
|
|
5308
|
-
});
|
|
5558
|
+
this.#send(socket, { type: "workspace.list", ...this.#workspaceLists() });
|
|
5309
5559
|
return;
|
|
5310
5560
|
case "workspace.open":
|
|
5311
5561
|
await this.#workspaces.open(frame.path);
|
|
5312
|
-
this.#
|
|
5313
|
-
type: "workspace.list",
|
|
5314
|
-
workspaces: this.#workspaces.list()
|
|
5315
|
-
});
|
|
5562
|
+
this.#broadcastWorkspaces();
|
|
5316
5563
|
await this.#closeFinder();
|
|
5317
5564
|
return;
|
|
5318
5565
|
case "workspace.close": {
|
|
@@ -5325,17 +5572,14 @@ var HostServer = class {
|
|
|
5325
5572
|
continue;
|
|
5326
5573
|
await this.#dropSession(session.id, "workspace closed");
|
|
5327
5574
|
}
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
});
|
|
5337
|
-
if (unpinned)
|
|
5338
|
-
await this.#sendPinned();
|
|
5575
|
+
this.#broadcastWorkspaces();
|
|
5576
|
+
return;
|
|
5577
|
+
}
|
|
5578
|
+
case "workspace.restore": {
|
|
5579
|
+
const restored = await this.#workspaces.restore(frame.workspaceIds);
|
|
5580
|
+
if (restored.length === 0)
|
|
5581
|
+
return;
|
|
5582
|
+
this.#broadcastWorkspaces();
|
|
5339
5583
|
return;
|
|
5340
5584
|
}
|
|
5341
5585
|
case "workspace.find": {
|
|
@@ -5357,10 +5601,7 @@ var HostServer = class {
|
|
|
5357
5601
|
return;
|
|
5358
5602
|
case "workspace.create": {
|
|
5359
5603
|
const { workspace, warnings } = await this.#workspaces.create(frame.parent, frame.name, frame.init);
|
|
5360
|
-
this.#
|
|
5361
|
-
type: "workspace.list",
|
|
5362
|
-
workspaces: this.#workspaces.list()
|
|
5363
|
-
});
|
|
5604
|
+
this.#broadcastWorkspaces();
|
|
5364
5605
|
this.#send(socket, { type: "workspace.created", workspace, warnings });
|
|
5365
5606
|
await this.#closeFinder();
|
|
5366
5607
|
return;
|
|
@@ -5422,6 +5663,42 @@ var HostServer = class {
|
|
|
5422
5663
|
case "session.pins":
|
|
5423
5664
|
await this.#sendPinned();
|
|
5424
5665
|
return;
|
|
5666
|
+
case "folder.create": {
|
|
5667
|
+
if (frame.scope === "session") {
|
|
5668
|
+
if (!frame.workspaceId)
|
|
5669
|
+
throw new Error("\u0414\u043B\u044F \u043F\u0430\u043F\u043A\u0438 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0443\u0436\u0435\u043D workspaceId");
|
|
5670
|
+
await this.#folders.createSessionFolder(frame.workspaceId, frame.name);
|
|
5671
|
+
} else {
|
|
5672
|
+
await this.#folders.createWorkspaceFolder(frame.name);
|
|
5673
|
+
}
|
|
5674
|
+
this.#sendFolders();
|
|
5675
|
+
return;
|
|
5676
|
+
}
|
|
5677
|
+
case "folder.rename":
|
|
5678
|
+
if (await this.#folders.rename(frame.folderId, frame.name))
|
|
5679
|
+
this.#sendFolders();
|
|
5680
|
+
return;
|
|
5681
|
+
case "folder.delete": {
|
|
5682
|
+
const removed = await this.#folders.delete(frame.folderId);
|
|
5683
|
+
if (!removed)
|
|
5684
|
+
return;
|
|
5685
|
+
this.#sendFolders();
|
|
5686
|
+
if (removed.scope === "workspace")
|
|
5687
|
+
this.#broadcastWorkspaces();
|
|
5688
|
+
else
|
|
5689
|
+
await this.#sendSessionList(removed.workspaceId);
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
case "folder.assign": {
|
|
5693
|
+
if (frame.workspaceId) {
|
|
5694
|
+
await this.#folders.assignSession(frame.itemId, frame.workspaceId, frame.folderId);
|
|
5695
|
+
await this.#sendSessionList(frame.workspaceId);
|
|
5696
|
+
} else {
|
|
5697
|
+
await this.#folders.assignWorkspace(frame.itemId, frame.folderId);
|
|
5698
|
+
this.#broadcastWorkspaces();
|
|
5699
|
+
}
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5425
5702
|
case "session.rename":
|
|
5426
5703
|
await this.#renameSession(frame.sessionId, frame.title, frame.workspaceId);
|
|
5427
5704
|
return;
|
|
@@ -5679,6 +5956,26 @@ var HostServer = class {
|
|
|
5679
5956
|
file: await this.#files(frame.workspaceId).read(frame.path)
|
|
5680
5957
|
});
|
|
5681
5958
|
return;
|
|
5959
|
+
case "fs.readMedia": {
|
|
5960
|
+
try {
|
|
5961
|
+
const { mime, base64 } = await this.#files(frame.workspaceId).readMedia(frame.path);
|
|
5962
|
+
this.#send(socket, {
|
|
5963
|
+
type: "fs.media",
|
|
5964
|
+
workspaceId: frame.workspaceId,
|
|
5965
|
+
path: normalize(frame.path),
|
|
5966
|
+
mime,
|
|
5967
|
+
base64
|
|
5968
|
+
});
|
|
5969
|
+
} catch (error) {
|
|
5970
|
+
this.#send(socket, {
|
|
5971
|
+
type: "fs.mediaFailed",
|
|
5972
|
+
workspaceId: frame.workspaceId,
|
|
5973
|
+
path: normalize(frame.path),
|
|
5974
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
return;
|
|
5978
|
+
}
|
|
5682
5979
|
case "fs.write": {
|
|
5683
5980
|
const files = this.#files(frame.workspaceId);
|
|
5684
5981
|
let saved;
|
|
@@ -6468,7 +6765,12 @@ var HostServer = class {
|
|
|
6468
6765
|
const archived = [];
|
|
6469
6766
|
for (const base of [...live, ...historical].sort((a, b) => b.updatedAt - a.updatedAt)) {
|
|
6470
6767
|
const pin = this.#pins.find(base.claudeSessionId ?? base.id);
|
|
6471
|
-
const
|
|
6768
|
+
const folderId = this.#folders.folderIdForSession(base.claudeSessionId ?? base.id);
|
|
6769
|
+
const summary = {
|
|
6770
|
+
...base,
|
|
6771
|
+
...pin ? { pinned: true, pinnedAt: pin.at } : {},
|
|
6772
|
+
...folderId ? { folderId } : {}
|
|
6773
|
+
};
|
|
6472
6774
|
const record = this.#archive.find(summary.claudeSessionId ?? summary.id);
|
|
6473
6775
|
if (!record) {
|
|
6474
6776
|
sessions.push(summary);
|
|
@@ -6506,8 +6808,8 @@ var HostServer = class {
|
|
|
6506
6808
|
return;
|
|
6507
6809
|
}
|
|
6508
6810
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
6509
|
-
const requested =
|
|
6510
|
-
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested :
|
|
6811
|
+
const requested = path18.join(webRoot, path18.normalize(url2.pathname));
|
|
6812
|
+
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path18.join(webRoot, "index.html");
|
|
6511
6813
|
if (!existsSync3(target)) {
|
|
6512
6814
|
res.writeHead(404).end("not found");
|
|
6513
6815
|
return;
|
|
@@ -6533,12 +6835,12 @@ function contentType(file) {
|
|
|
6533
6835
|
".svg": "image/svg+xml",
|
|
6534
6836
|
".woff2": "font/woff2"
|
|
6535
6837
|
};
|
|
6536
|
-
return types[
|
|
6838
|
+
return types[path18.extname(file)] ?? "application/octet-stream";
|
|
6537
6839
|
}
|
|
6538
6840
|
|
|
6539
6841
|
// dist/ccv.js
|
|
6540
|
-
var HERE =
|
|
6541
|
-
var PACKAGE_ROOT =
|
|
6842
|
+
var HERE = path19.dirname(fileURLToPath(import.meta.url));
|
|
6843
|
+
var PACKAGE_ROOT = path19.resolve(HERE, "..");
|
|
6542
6844
|
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.
|
|
6543
6845
|
|
|
6544
6846
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -6605,7 +6907,7 @@ var server = new HostServer({
|
|
|
6605
6907
|
dataDir: defaults.dataDir,
|
|
6606
6908
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
6607
6909
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
6608
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
6910
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path19.join(PACKAGE_ROOT, "web"),
|
|
6609
6911
|
version,
|
|
6610
6912
|
relayUrl: defaults.relayUrl
|
|
6611
6913
|
});
|
|
@@ -6634,7 +6936,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
6634
6936
|
}
|
|
6635
6937
|
async function packageVersion() {
|
|
6636
6938
|
try {
|
|
6637
|
-
const text = await
|
|
6939
|
+
const text = await readFile14(path19.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
6638
6940
|
return JSON.parse(text).version ?? "0.0.0";
|
|
6639
6941
|
} catch {
|
|
6640
6942
|
return "0.0.0";
|