dsh-edt-projects 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/cordis.patch.yml +10 -0
- package/lib/bsl.mjs +3 -1
- package/lib/config.mjs +41 -8
- package/lib/enable.mjs +28 -0
- package/lib/index.js +615 -51
- package/lib/repo.mjs +185 -0
- package/lib/schema.mjs +84 -1
- package/lib/workspace-run.mjs +690 -0
- package/lib/workspace.mjs +1089 -0
- package/locale/en.json +1 -1
- package/locale/ru.json +1 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
// включения на лету), поэтому правки применяются через edt_reload без
|
|
19
19
|
// перезапуска DSH.
|
|
20
20
|
//
|
|
21
|
+
// Git worktree: включённость и настройки наследуются от основного checkout, а
|
|
22
|
+
// окружение EDT поднимается своё — workspace в <worktree>/build/ws, свой порт и
|
|
23
|
+
// фоновый процесс (lib/workspace-run.mjs). Так параллельные worktree не делят
|
|
24
|
+
// один workspace EDT; основной checkout продолжает работать с ручным сервером.
|
|
25
|
+
//
|
|
21
26
|
// `shared: true` больше не работает: сервер всегда живёт в скоупе сессии, иначе
|
|
22
27
|
// его инструменты протекали бы в чужие проекты.
|
|
23
28
|
export const name = 'mcp-hub';
|
|
@@ -25,17 +30,44 @@ export const name = 'mcp-hub';
|
|
|
25
30
|
export const inject = ['tools', 'workingDirectory'];
|
|
26
31
|
|
|
27
32
|
/** Собственные инструменты плагина; префикс edt_ отделяет их от `mcp__edt__*`. */
|
|
28
|
-
export const TOOL_NAMES = ['edt_servers', 'edt_pin', 'edt_unpin', 'edt_reload', 'edt_config'];
|
|
33
|
+
export const TOOL_NAMES = ['edt_servers', 'edt_pin', 'edt_unpin', 'edt_reload', 'edt_config', 'edt_workspace'];
|
|
29
34
|
|
|
35
|
+
import { spawn } from 'node:child_process';
|
|
36
|
+
import { join, resolve as resolvePath, isAbsolute } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
30
38
|
import { watch } from 'node:fs';
|
|
31
39
|
import { bslAvailable, bslSettings, ensureJar, resolveBsl, startBslSupervisor } from './bsl.mjs';
|
|
32
40
|
import { projectConfigLoaded, projectConfigPath, resolveConfig, userConfigPath } from './config.mjs';
|
|
33
|
-
import {
|
|
41
|
+
import { activationForPaths } from './enable.mjs';
|
|
34
42
|
import { normalizeWiki, targetPathOf, writeDenialReason } from './wiki.mjs';
|
|
35
43
|
import { normalizeTestTooling } from './tests.mjs';
|
|
36
44
|
import { initFile, upgradeFile } from './config-file.mjs';
|
|
37
45
|
import { stringifyConfig, validateConfig } from './schema.mjs';
|
|
38
46
|
import { createSkillsProvider } from './skills.mjs';
|
|
47
|
+
import { repositoriesUnder, repositoryAt, resolveContextCwd } from './repo.mjs';
|
|
48
|
+
import {
|
|
49
|
+
BUILD_DIR,
|
|
50
|
+
detectEdtHome,
|
|
51
|
+
formatElapsed,
|
|
52
|
+
isPortOpen,
|
|
53
|
+
normalizeWorkspaceSettings,
|
|
54
|
+
pickFreePort,
|
|
55
|
+
pidAlive,
|
|
56
|
+
prepareWorkspaceSnapshot,
|
|
57
|
+
prefValue,
|
|
58
|
+
probeHealth,
|
|
59
|
+
readPort,
|
|
60
|
+
readPrefs,
|
|
61
|
+
removeStamp,
|
|
62
|
+
requiredJava,
|
|
63
|
+
resolveEdtVm,
|
|
64
|
+
resolveWorkspaceTarget,
|
|
65
|
+
stateOf as workspaceState,
|
|
66
|
+
tailFile,
|
|
67
|
+
writePort,
|
|
68
|
+
writeStamp,
|
|
69
|
+
} from './workspace.mjs';
|
|
70
|
+
import { cmdInit, cmdStop as stopWorkspaceNow, lockState } from './workspace-run.mjs';
|
|
39
71
|
|
|
40
72
|
const HEADER = `## Работа с 1С через MCP
|
|
41
73
|
|
|
@@ -108,6 +140,20 @@ v8std — внешняя база стандартов 1С и диагности
|
|
|
108
140
|
- Найденный текст — справка, а не приговор: проверку даёт EDT или BSL, v8std объясняет, что она означает.
|
|
109
141
|
- v8std только читает; правки исходников — через EDT-MCP, по правилам выше.`;
|
|
110
142
|
|
|
143
|
+
/** Блок про отдельное EDT-окружение worktree. */
|
|
144
|
+
function workspaceRules(workspace) {
|
|
145
|
+
return `
|
|
146
|
+
### Окружение EDT в worktree
|
|
147
|
+
|
|
148
|
+
Сессия открыта в отдельном git worktree, поэтому у неё свой workspace EDT: ${workspace.wsDir}. Его поднимает плагин, отдельно от основного checkout.
|
|
149
|
+
|
|
150
|
+
- Первый запуск занимает 1–3 минуты, с импортом проектов — дольше. Готовность проверяй через \`edt_workspace\` (action=status): состояние \`ready\` и порт.
|
|
151
|
+
- Инструменты \`mcp__edt__*\` появляются после готовности сервера. Если вызов не удался из-за соединения — проверь статус, не перезапускай окружение самовольно.
|
|
152
|
+
- Правки исходников — через EDT по правилам выше; workspace EDT знает проекты этого worktree.
|
|
153
|
+
- Не удаляй \`${workspace.root}/build\` при живом EDT. Остановка — \`edt_workspace\` action=stop, перезапуск — action=restart.
|
|
154
|
+
- Логи: ${workspace.root}/build/logs (edt.log, edt-import.log, init.log).`;
|
|
155
|
+
}
|
|
156
|
+
|
|
111
157
|
/**
|
|
112
158
|
* Собирает блок правил для вики.
|
|
113
159
|
* @param roots результат normalizeWiki().roots
|
|
@@ -170,13 +216,15 @@ function vanessaRules(settings) {
|
|
|
170
216
|
}
|
|
171
217
|
|
|
172
218
|
/**
|
|
173
|
-
* Текст правил под фактически настроенные серверы, их состояние,
|
|
219
|
+
* Текст правил под фактически настроенные серверы, их состояние, вики, тесты
|
|
220
|
+
* и управляемое окружение worktree.
|
|
174
221
|
* @param env окружение процесса (для тестов передаётся явно).
|
|
175
222
|
* @param servers список записей { name, state }.
|
|
176
223
|
* @param wikiRoots корни вики из normalizeWiki().
|
|
177
224
|
* @param tests секция tests из настроек (флаги YAxUnit / Vanessa).
|
|
225
|
+
* @param workspace описание окружения worktree (describeWorkspace) или undefined.
|
|
178
226
|
*/
|
|
179
|
-
export function buildRulesText(env = process.env, servers = [], wikiRoots = [], tests = {}) {
|
|
227
|
+
export function buildRulesText(env = process.env, servers = [], wikiRoots = [], tests = {}, workspace = undefined) {
|
|
180
228
|
const usable = new Set(servers.filter((s) => s.state !== 'unavailable').map((s) => s.name));
|
|
181
229
|
const byEnv = servers.length === 0;
|
|
182
230
|
const blocks = [HEADER];
|
|
@@ -188,6 +236,7 @@ export function buildRulesText(env = process.env, servers = [], wikiRoots = [],
|
|
|
188
236
|
if (usable.has('bsl') || (byEnv && (env.BSL_LS_JAR || env.BSL_MCP_URL))) blocks.push(BSL_RULES);
|
|
189
237
|
if (usable.has('tasks') || (byEnv && env.TASKS_MCP_URL)) blocks.push(TASKS_RULES);
|
|
190
238
|
if (usable.has('v8std') || (byEnv && env.V8STD_MCP_URL)) blocks.push(V8STD_RULES);
|
|
239
|
+
if (workspace?.state === 'worktree') blocks.push(workspaceRules(workspace));
|
|
191
240
|
|
|
192
241
|
const lazy = servers.filter((s) => s.state === 'lazy').map((s) => s.name);
|
|
193
242
|
const unavailable = servers.filter((s) => s.state === 'unavailable').map((s) => s.name);
|
|
@@ -248,11 +297,23 @@ export function mountConfig(entry) {
|
|
|
248
297
|
return config;
|
|
249
298
|
}
|
|
250
299
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Настройки BSL LS из окружения, перекрытые файлами настроек.
|
|
302
|
+
* @param fallbackRoot корень проекта: работе в worktree нужен его каталог,
|
|
303
|
+
* а не каталог процесса DSH. Относительный `servers.bsl.root` считается
|
|
304
|
+
* от него же.
|
|
305
|
+
*/
|
|
306
|
+
function bslSettingsFor(effective, fallbackRoot = process.cwd()) {
|
|
253
307
|
const base = bslSettings(process.env);
|
|
254
308
|
const fromFile = effective.config?.servers?.bsl ?? {};
|
|
255
309
|
const pick = (key, fallback) => (fromFile[key] === undefined ? fallback : fromFile[key]);
|
|
310
|
+
const rawRoot = pick('root', base.root);
|
|
311
|
+
const root =
|
|
312
|
+
typeof rawRoot !== 'string' || rawRoot.trim() === ''
|
|
313
|
+
? fallbackRoot
|
|
314
|
+
: isAbsolute(rawRoot)
|
|
315
|
+
? rawRoot
|
|
316
|
+
: resolvePath(fallbackRoot, rawRoot);
|
|
256
317
|
return {
|
|
257
318
|
...base,
|
|
258
319
|
jar: pick('jar', base.jar),
|
|
@@ -260,8 +321,8 @@ function bslSettingsFor(effective) {
|
|
|
260
321
|
version: pick('version', base.version),
|
|
261
322
|
cacheDir: pick('cacheDir', base.cacheDir),
|
|
262
323
|
java: pick('java', base.java),
|
|
263
|
-
cwd: pick('cwd', base.cwd),
|
|
264
|
-
root
|
|
324
|
+
cwd: pick('cwd', base.cwd) || fallbackRoot,
|
|
325
|
+
root,
|
|
265
326
|
port: pick('port', base.port),
|
|
266
327
|
mode: pick('mode', base.mode),
|
|
267
328
|
mcpUrl: pick('mcpUrl', base.mcpUrl),
|
|
@@ -299,7 +360,9 @@ const activationReason = (activation) => {
|
|
|
299
360
|
case 'project-file':
|
|
300
361
|
return 'есть .dsh-1c.json проекта';
|
|
301
362
|
case 'allowlist':
|
|
302
|
-
return
|
|
363
|
+
return activation.inherited === true
|
|
364
|
+
? `путь в projects (${activation.entry}), наследовано из ${activation.path}`
|
|
365
|
+
: `путь в projects (${activation.entry})`;
|
|
303
366
|
default:
|
|
304
367
|
return 'проект не выбран: нет .dsh-1c.json и его нет в projects';
|
|
305
368
|
}
|
|
@@ -334,28 +397,139 @@ export async function apply(ctx, config) {
|
|
|
334
397
|
const loggedErrors = new Set();
|
|
335
398
|
let skillsControl;
|
|
336
399
|
|
|
337
|
-
const
|
|
400
|
+
const originCwdOf = (agent) => sessionCwd(agent, ctx.workingDirectory);
|
|
401
|
+
const workCwdOf = (agent) => {
|
|
402
|
+
const session = agent?.session;
|
|
403
|
+
if (session === undefined) return originCwdOf(agent);
|
|
404
|
+
try {
|
|
405
|
+
const dir = ctx.workingDirectory?.get?.(session);
|
|
406
|
+
return typeof dir === 'string' && dir !== '' ? dir : originCwdOf(agent);
|
|
407
|
+
} catch {
|
|
408
|
+
return originCwdOf(agent);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
// Контекст сессии — эффективный каталог, если это worktree того же проекта.
|
|
412
|
+
const cwdOf = (agent) =>
|
|
413
|
+
resolveContextCwd({ origin: originCwdOf(agent), work: workCwdOf(agent) });
|
|
414
|
+
|
|
415
|
+
/** Уникальные пути в порядке появления (для слоёв настроек и allowlist). */
|
|
416
|
+
const uniquePaths = (values) => {
|
|
417
|
+
const seen = new Set();
|
|
418
|
+
const out = [];
|
|
419
|
+
for (const value of values) {
|
|
420
|
+
if (typeof value !== 'string' || value === '') continue;
|
|
421
|
+
const key = value.toLowerCase();
|
|
422
|
+
if (seen.has(key)) continue;
|
|
423
|
+
seen.add(key);
|
|
424
|
+
out.push(value);
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
};
|
|
338
428
|
|
|
339
|
-
|
|
340
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Контекст проекта для каталога сессии: репозитории, слои настроек,
|
|
431
|
+
* активация и план EDT-окружения. Для worktree настройки берутся ещё и из
|
|
432
|
+
* основного checkout, а активация наследуется от него же.
|
|
433
|
+
*/
|
|
434
|
+
const projectContextForCwd = (cwd) => {
|
|
435
|
+
const repositories = repositoriesUnder(cwd);
|
|
436
|
+
// Порядок = порядок слияния: основной checkout слабее worktree, cwd сильнее всех.
|
|
437
|
+
const paths = uniquePaths([...repositories.flatMap((repo) => [repo.mainRoot, repo.root]), cwd]);
|
|
438
|
+
const effective = resolveConfig({ cwd, projectCwds: paths, env: process.env, base });
|
|
341
439
|
for (const error of effective.errors) {
|
|
342
440
|
if (loggedErrors.has(error)) continue;
|
|
343
441
|
loggedErrors.add(error);
|
|
344
442
|
ctx.logger?.warn?.(`mcp-hub: ${error}`);
|
|
345
443
|
}
|
|
346
|
-
|
|
444
|
+
const hasProjectFile = paths.some((dir) => projectConfigLoaded(dir));
|
|
445
|
+
const activation = activationForPaths({ cwd, config: effective.config, hasProjectFile, paths });
|
|
446
|
+
const enabledRepositories = repositories.filter(
|
|
447
|
+
(repo) =>
|
|
448
|
+
activationForPaths({
|
|
449
|
+
cwd: repo.root,
|
|
450
|
+
config: effective.config,
|
|
451
|
+
hasProjectFile: projectConfigLoaded(repo.root) || projectConfigLoaded(repo.mainRoot),
|
|
452
|
+
paths: [repo.root, repo.mainRoot],
|
|
453
|
+
}).enabled,
|
|
454
|
+
);
|
|
455
|
+
// Если проект включён самим каталогом сессии (файл в task space), явного
|
|
456
|
+
// списка включённых репозиториев может не быть — берём репозитории сессии.
|
|
457
|
+
const workspaceRepositories =
|
|
458
|
+
enabledRepositories.length > 0 ? enabledRepositories : activation.enabled ? repositories : [];
|
|
459
|
+
const workspace = resolveWorkspaceTarget({
|
|
460
|
+
cwd,
|
|
461
|
+
config: effective.config,
|
|
462
|
+
repositories: workspaceRepositories,
|
|
463
|
+
env: process.env,
|
|
464
|
+
});
|
|
465
|
+
return { cwd, repositories, enabledRepositories, paths, effective, activation, workspace };
|
|
347
466
|
};
|
|
348
467
|
|
|
349
|
-
const
|
|
468
|
+
const contextFor = (agent) => {
|
|
469
|
+
const base = projectContextForCwd(cwdOf(agent));
|
|
470
|
+
const override = agent === undefined ? undefined : agentStates.get(agent)?.workspaceRoot;
|
|
471
|
+
if (override === undefined) return base;
|
|
472
|
+
|
|
473
|
+
const repo = repositoryAt(override);
|
|
474
|
+
const repositories =
|
|
475
|
+
repo === undefined
|
|
476
|
+
? [{ root: override, mainRoot: base.workspace?.mainRoot ?? override, isWorktree: true }]
|
|
477
|
+
: [{ ...repo, isWorktree: true }];
|
|
478
|
+
const workspace = resolveWorkspaceTarget({
|
|
479
|
+
cwd: override,
|
|
480
|
+
config: base.effective.config,
|
|
481
|
+
repositories,
|
|
482
|
+
env: process.env,
|
|
483
|
+
});
|
|
484
|
+
return { ...base, cwd: override, repositories, workspace, workspaceOverride: true };
|
|
485
|
+
};
|
|
486
|
+
const effectiveFor = (agent) => contextFor(agent).effective;
|
|
350
487
|
|
|
351
|
-
|
|
352
|
-
|
|
488
|
+
/** Каталог, от которого считается корень BSL и относительный workspace.root. */
|
|
489
|
+
const fallbackRootFor = (context) => {
|
|
490
|
+
if (context.workspace.state === 'ok') return context.workspace.root;
|
|
491
|
+
if (context.enabledRepositories.length > 0) return context.enabledRepositories[0].root;
|
|
492
|
+
return context.cwd;
|
|
493
|
+
};
|
|
353
494
|
|
|
354
|
-
|
|
495
|
+
/** Дешёвая проверка живости: без tasklist, чтобы не звать её на каждом шаге. */
|
|
496
|
+
const cheapAlive = (pid) => pidAlive(pid);
|
|
355
497
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Описание окружения для правил, снапшота и инструмента edt_workspace.
|
|
500
|
+
* `state` — включено ли управление ('worktree'/'off'/'error'),
|
|
501
|
+
* `status` — фактическая фаза (absent/stopped/starting/ready/stale/failed).
|
|
502
|
+
*/
|
|
503
|
+
const describeWorkspace = (plan, deps = {}) => {
|
|
504
|
+
if (plan === undefined || plan.state === 'off') return { state: 'off', reason: plan?.reason };
|
|
505
|
+
if (plan.state === 'error') {
|
|
506
|
+
return { state: 'error', reason: plan.reason, message: plan.message, settings: plan.settings };
|
|
507
|
+
}
|
|
508
|
+
const current = workspaceState(plan.buildDir, deps);
|
|
509
|
+
const phase = current.progress?.edt;
|
|
510
|
+
return {
|
|
511
|
+
state: 'worktree',
|
|
512
|
+
status: current.label,
|
|
513
|
+
root: plan.root,
|
|
514
|
+
buildDir: plan.buildDir,
|
|
515
|
+
wsDir: plan.wsDir,
|
|
516
|
+
mainRoot: plan.mainRoot,
|
|
517
|
+
port: current.port ?? undefined,
|
|
518
|
+
pid: current.pid?.pid ?? undefined,
|
|
519
|
+
phase: phase?.phase,
|
|
520
|
+
since: phase?.since,
|
|
521
|
+
detail: phase?.detail,
|
|
522
|
+
warning: phase?.warning,
|
|
523
|
+
imported: current.imported?.projects?.length,
|
|
524
|
+
settings: plan.settings,
|
|
525
|
+
warnings: plan.warnings ?? [],
|
|
526
|
+
rootSource: plan.rootSource,
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const entriesFor = (context) => {
|
|
531
|
+
const bsl = bslSettingsFor(context.effective, fallbackRootFor(context));
|
|
532
|
+
const entries = normalizeServers(context.effective.config?.servers).map((entry) => {
|
|
359
533
|
if (entry.raw.provision === true) {
|
|
360
534
|
entry.provisioned = true;
|
|
361
535
|
entry.transport = bsl.mcpUrl === undefined ? 'stdio' : 'streamable-http';
|
|
@@ -363,6 +537,16 @@ export async function apply(ctx, config) {
|
|
|
363
537
|
}
|
|
364
538
|
return entry;
|
|
365
539
|
});
|
|
540
|
+
|
|
541
|
+
// В worktree EDT монтируется на порт поднятого для него окружения.
|
|
542
|
+
if (context.workspace.state === 'ok') {
|
|
543
|
+
const port = readPort(context.workspace.buildDir) ?? context.workspace.port;
|
|
544
|
+
const edt = entries.find((entry) => entry.name === 'edt');
|
|
545
|
+
if (edt !== undefined && Number.isInteger(port)) {
|
|
546
|
+
edt.raw = { ...edt.raw, url: `http://127.0.0.1:${port}/mcp` };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return entries;
|
|
366
550
|
};
|
|
367
551
|
|
|
368
552
|
/** Состояние активации одного агента (сессии): инструменты и смонтированные серверы. */
|
|
@@ -401,14 +585,20 @@ export async function apply(ctx, config) {
|
|
|
401
585
|
}
|
|
402
586
|
};
|
|
403
587
|
|
|
404
|
-
const snapshot = (agent) => {
|
|
405
|
-
const
|
|
406
|
-
|
|
588
|
+
const snapshot = (agent, deps = {}) => {
|
|
589
|
+
const context = contextFor(agent);
|
|
590
|
+
const workspace = describeWorkspace(context.workspace, deps);
|
|
591
|
+
return entriesFor(context).map((entry) => {
|
|
407
592
|
const item = mapFor(agent).get(entry.name);
|
|
408
593
|
let value = 'lazy';
|
|
409
594
|
if (entry.unavailable) value = 'unavailable';
|
|
410
595
|
if (item?.fiber && !item.error) value = 'mounted';
|
|
411
596
|
if (item?.error) value = 'failed';
|
|
597
|
+
// Пока EDT-окружение не готово, mounted с нулём инструментов выглядит
|
|
598
|
+
// как поломка: показываем фазу подъёма.
|
|
599
|
+
if (entry.name === 'edt' && item?.fiber && !item.error && workspace.state === 'worktree' && workspace.status !== 'ready') {
|
|
600
|
+
value = workspace.status === 'failed' || workspace.status === 'stale' ? 'failed' : 'starting';
|
|
601
|
+
}
|
|
412
602
|
return {
|
|
413
603
|
name: entry.name,
|
|
414
604
|
state: value,
|
|
@@ -418,6 +608,7 @@ export async function apply(ctx, config) {
|
|
|
418
608
|
endpoint: item?.supervisor?.url ?? entry.raw?.url,
|
|
419
609
|
tools: toolNamesOf(agent, entry.name).length,
|
|
420
610
|
error: item?.error,
|
|
611
|
+
workspace: entry.name === 'edt' ? workspace : undefined,
|
|
421
612
|
};
|
|
422
613
|
});
|
|
423
614
|
};
|
|
@@ -434,8 +625,8 @@ export async function apply(ctx, config) {
|
|
|
434
625
|
};
|
|
435
626
|
|
|
436
627
|
const mount = async (agent, serverName, options = {}) => {
|
|
437
|
-
const
|
|
438
|
-
const entries = entriesFor(
|
|
628
|
+
const context = contextFor(agent);
|
|
629
|
+
const entries = entriesFor(context);
|
|
439
630
|
const entry = entries.find((candidate) => candidate.name === serverName);
|
|
440
631
|
if (entry === undefined) {
|
|
441
632
|
throw new Error(`unknown MCP server "${serverName}"; configured: ${entries.map((item) => item.name).join(', ') || 'none'}`);
|
|
@@ -445,7 +636,7 @@ export async function apply(ctx, config) {
|
|
|
445
636
|
}
|
|
446
637
|
|
|
447
638
|
const item = itemFor(agent, entry);
|
|
448
|
-
const bsl = bslSettingsFor(effective);
|
|
639
|
+
const bsl = bslSettingsFor(context.effective, fallbackRootFor(context));
|
|
449
640
|
const root = options.root ?? entry.raw.root ?? bsl.root;
|
|
450
641
|
|
|
451
642
|
if (item.fiber && !item.error) {
|
|
@@ -515,8 +706,8 @@ export async function apply(ctx, config) {
|
|
|
515
706
|
};
|
|
516
707
|
|
|
517
708
|
const unmount = async (agent, serverName) => {
|
|
518
|
-
const
|
|
519
|
-
const entry = entriesFor(
|
|
709
|
+
const context = contextFor(agent);
|
|
710
|
+
const entry = entriesFor(context).find((candidate) => candidate.name === serverName);
|
|
520
711
|
const map = mapFor(agent);
|
|
521
712
|
const item = map.get(serverName);
|
|
522
713
|
if (item === undefined || !item.fiber) {
|
|
@@ -531,8 +722,8 @@ export async function apply(ctx, config) {
|
|
|
531
722
|
|
|
532
723
|
/** Перечитывает настройки и перезапускает серверы, у которых изменился конфиг. */
|
|
533
724
|
const reload = async (agent, serverName) => {
|
|
534
|
-
const
|
|
535
|
-
const entries = entriesFor(
|
|
725
|
+
const context = contextFor(agent);
|
|
726
|
+
const entries = entriesFor(context);
|
|
536
727
|
const targets = serverName === undefined ? entries : entries.filter((entry) => entry.name === serverName);
|
|
537
728
|
if (serverName !== undefined && targets.length === 0) {
|
|
538
729
|
throw new Error(`unknown MCP server "${serverName}"; configured: ${entries.map((item) => item.name).join(', ') || 'none'}`);
|
|
@@ -542,9 +733,8 @@ export async function apply(ctx, config) {
|
|
|
542
733
|
for (const entry of targets) {
|
|
543
734
|
const item = mapFor(agent).get(entry.name);
|
|
544
735
|
if (item === undefined || !item.fiber) continue;
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const fresh = signatureOf(entry, root);
|
|
736
|
+
const freshRoot = entry.raw.root ?? bslSettingsFor(context.effective, fallbackRootFor(context)).root;
|
|
737
|
+
const fresh = signatureOf(entry, freshRoot);
|
|
548
738
|
if (fresh === item.signature) continue;
|
|
549
739
|
await disposeItem(item);
|
|
550
740
|
const result = await mount(agent, entry.name, {});
|
|
@@ -582,17 +772,189 @@ export async function apply(ctx, config) {
|
|
|
582
772
|
}
|
|
583
773
|
};
|
|
584
774
|
|
|
775
|
+
// --- окружение EDT в worktree ----------------------------------------------
|
|
776
|
+
const initializerPath = fileURLToPath(new URL('./workspace-run.mjs', import.meta.url));
|
|
777
|
+
/** buildDir -> Promise подъёма (одна на процесс, чтобы не поднять два EDT). */
|
|
778
|
+
const ensuring = new Map();
|
|
779
|
+
/** buildDir -> { agents, settings, startedByUs } для остановки после последней сессии. */
|
|
780
|
+
const workspaceSessions = new Map();
|
|
781
|
+
|
|
782
|
+
const spawnInitializer = (root, action = 'init') => {
|
|
783
|
+
try {
|
|
784
|
+
const child = spawn(process.execPath, [initializerPath, action, '--root', root], {
|
|
785
|
+
cwd: root,
|
|
786
|
+
detached: true,
|
|
787
|
+
stdio: 'ignore',
|
|
788
|
+
windowsHide: true,
|
|
789
|
+
});
|
|
790
|
+
child.unref();
|
|
791
|
+
return true;
|
|
792
|
+
} catch (error) {
|
|
793
|
+
ctx.logger?.warn?.(`mcp-hub: не удалось запустить ${action} окружения EDT: ${error instanceof Error ? error.message : String(error)}`);
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const registerWorkspaceSession = (buildDir, agent, settings) => {
|
|
799
|
+
let record = workspaceSessions.get(buildDir);
|
|
800
|
+
if (record === undefined) {
|
|
801
|
+
record = { agents: new Set(), settings, startedByUs: false };
|
|
802
|
+
workspaceSessions.set(buildDir, record);
|
|
803
|
+
}
|
|
804
|
+
if (agent !== undefined) record.agents.add(agent);
|
|
805
|
+
if (settings !== undefined) record.settings = settings;
|
|
806
|
+
return record;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
/** Фаза подъёма без pid (идёт импорт или старт GUI) тоже считается запуском. */
|
|
810
|
+
const busyLaunching = (current, settings = {}) => {
|
|
811
|
+
if (current.label === 'starting') return true;
|
|
812
|
+
const phase = current.progress?.edt;
|
|
813
|
+
if (phase?.phase === undefined || ['failed', 'ready'].includes(phase.phase)) return false;
|
|
814
|
+
const updated = Date.parse(phase.updated ?? phase.since ?? '');
|
|
815
|
+
if (Number.isNaN(updated)) return true;
|
|
816
|
+
const budget = Math.max(settings.startTimeout ?? 1800, settings.importTimeout ?? 3600) * 1000 + 5 * 60_000;
|
|
817
|
+
return Date.now() - updated < budget;
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
/** Убеждается, что фоновый run действительно начал работу (а не упал на lock). */
|
|
821
|
+
const launchStarted = async (buildDir, settings = {}, timeoutMs = 5000) => {
|
|
822
|
+
// Тот же срок устаревания, что в cmdInit/cmdRun: иначе lock, который они
|
|
823
|
+
// уже считают осиротевшим, здесь выглядел бы как живой запуск.
|
|
824
|
+
const maxAgeMs = (Math.max(settings.startTimeout ?? 1800, settings.importTimeout ?? 3600) + 600) * 1000;
|
|
825
|
+
const deadline = Date.now() + timeoutMs;
|
|
826
|
+
while (Date.now() < deadline) {
|
|
827
|
+
const current = workspaceState(buildDir, { alive: cheapAlive });
|
|
828
|
+
const phase = current.progress?.edt?.phase;
|
|
829
|
+
if (current.label === 'starting' || current.label === 'ready' || current.label === 'failed') return true;
|
|
830
|
+
if (phase !== undefined && phase !== 'ready' && phase !== 'failed') return true;
|
|
831
|
+
// run мог только взяться за lock, ещё не записав ни progress, ни pid.
|
|
832
|
+
const runLock = lockState(join(buildDir, 'init-run.lock'), { maxAgeMs });
|
|
833
|
+
if (runLock.exists === true && runLock.stale === false) return true;
|
|
834
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Гарантирует подъём окружения: занимает порт, пишет снимок и запускает
|
|
841
|
+
* фоновый init. Готовое/стартующее окружение только принимается.
|
|
842
|
+
*/
|
|
843
|
+
const ensureWorkspace = async (agent, context = contextFor(agent), options = {}) => {
|
|
844
|
+
const plan = context.workspace;
|
|
845
|
+
if (plan?.state !== 'ok') return undefined;
|
|
846
|
+
const key = plan.buildDir;
|
|
847
|
+
const record = registerWorkspaceSession(key, agent, plan.settings);
|
|
848
|
+
// После edt_workspace stop не поднимаем заново, пока не попросят init.
|
|
849
|
+
if (record.suppressed === true && options.force !== true) {
|
|
850
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
851
|
+
}
|
|
852
|
+
const pending = ensuring.get(key);
|
|
853
|
+
if (pending !== undefined) return pending;
|
|
854
|
+
|
|
855
|
+
const promise = (async () => {
|
|
856
|
+
let current = workspaceState(key, { alive: cheapAlive });
|
|
857
|
+
if (current.label === 'failed' && options.force === true) {
|
|
858
|
+
removeStamp(key, 'failed');
|
|
859
|
+
current = workspaceState(key, { alive: cheapAlive });
|
|
860
|
+
}
|
|
861
|
+
if (current.label === 'ready') return current;
|
|
862
|
+
if (current.label === 'failed') {
|
|
863
|
+
// EDT мог подняться позже таймаута health: проверяем и принимаем.
|
|
864
|
+
if (await probeHealth(current.port)) {
|
|
865
|
+
removeStamp(key, 'failed');
|
|
866
|
+
writeStamp(key, 'ready');
|
|
867
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
868
|
+
}
|
|
869
|
+
return current;
|
|
870
|
+
}
|
|
871
|
+
if (busyLaunching(current, plan.settings)) return current;
|
|
872
|
+
if (current.label === 'stale') removeStamp(key, 'ready');
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
let port = readPort(key);
|
|
876
|
+
if (port !== null && (await isPortOpen(port))) {
|
|
877
|
+
// Кто-то уже слушает порт: если это наш MCP-сервер (DSH перезапустили,
|
|
878
|
+
// маркер потерялся), принимаем его и не поднимаем второй EDT на тот же
|
|
879
|
+
// workspace.
|
|
880
|
+
if (await probeHealth(port)) {
|
|
881
|
+
removeStamp(key, 'failed');
|
|
882
|
+
writeStamp(key, 'ready');
|
|
883
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
884
|
+
}
|
|
885
|
+
port = null;
|
|
886
|
+
}
|
|
887
|
+
if (port === null) {
|
|
888
|
+
port = await pickFreePort(plan.settings.ports);
|
|
889
|
+
writePort(key, port);
|
|
890
|
+
}
|
|
891
|
+
const workspaceSnapshot = prepareWorkspaceSnapshot({
|
|
892
|
+
root: plan.root,
|
|
893
|
+
mainRoot: plan.mainRoot,
|
|
894
|
+
settings: plan.settings,
|
|
895
|
+
});
|
|
896
|
+
if (options.import === true) workspaceSnapshot.forceImport = true;
|
|
897
|
+
const initLog = join(key, 'logs', 'init.log');
|
|
898
|
+
const code = await cmdInit(plan.root, {}, { snapshot: workspaceSnapshot });
|
|
899
|
+
if (code !== 0) {
|
|
900
|
+
const tail = tailFile(initLog);
|
|
901
|
+
return {
|
|
902
|
+
...workspaceState(key, { alive: cheapAlive }),
|
|
903
|
+
launchError: `init завершился с кодом ${code}${tail === null ? '' : `: ${tail}`}`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
record.startedByUs = true;
|
|
907
|
+
if (!(await launchStarted(key, plan.settings))) {
|
|
908
|
+
const tail = tailFile(initLog);
|
|
909
|
+
return {
|
|
910
|
+
...workspaceState(key, { alive: cheapAlive }),
|
|
911
|
+
launchError: `run не стартовал${tail === null ? '' : `: ${tail}`}`,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
915
|
+
} catch (error) {
|
|
916
|
+
ctx.logger?.warn?.(`mcp-hub: окружение EDT не поднялось: ${error instanceof Error ? error.message : String(error)}`);
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
})().finally(() => ensuring.delete(key));
|
|
920
|
+
|
|
921
|
+
ensuring.set(key, promise);
|
|
922
|
+
return promise;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
/** Снимает сессию с окружения; после последней — останавливает поднятое нами EDT. */
|
|
926
|
+
const releaseWorkspaceSession = (agent) => {
|
|
927
|
+
for (const [buildDir, record] of workspaceSessions) {
|
|
928
|
+
if (!record.agents.delete(agent)) continue;
|
|
929
|
+
if (record.agents.size > 0) continue;
|
|
930
|
+
workspaceSessions.delete(buildDir);
|
|
931
|
+
if (record.startedByUs !== true) continue;
|
|
932
|
+
if (record.settings?.stopOnSessionEnd === false) continue;
|
|
933
|
+
spawnInitializer(resolvePath(buildDir, '..'), 'stop');
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
|
|
585
937
|
const activate = async (agent) => {
|
|
586
938
|
const state = stateOf(agent);
|
|
587
939
|
if (state.active) return;
|
|
588
940
|
state.active = true;
|
|
589
|
-
|
|
941
|
+
const context = contextFor(agent);
|
|
942
|
+
for (const definition of [serversTool(), pinTool(), unpinTool(), reloadTool(), workspaceTool()]) {
|
|
590
943
|
state.disposers.push(agent.ctx.tools.register(definition));
|
|
591
944
|
}
|
|
592
|
-
watchFile(projectConfigPath(
|
|
945
|
+
for (const dir of context.paths) watchFile(projectConfigPath(dir));
|
|
593
946
|
|
|
594
|
-
|
|
595
|
-
|
|
947
|
+
// Окружение поднимается до монтирования edt: нужен порт для адреса.
|
|
948
|
+
if (context.workspace.state === 'ok') {
|
|
949
|
+
try {
|
|
950
|
+
const launched = await ensureWorkspace(agent, context);
|
|
951
|
+
if (launched?.launchError !== undefined) ctx.logger?.warn?.(`mcp-hub: ${launched.launchError}`);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
ctx.logger?.warn?.(`mcp-hub: окружение EDT не поднялось: ${error instanceof Error ? error.message : String(error)}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
for (const entry of entriesFor(context)) {
|
|
596
958
|
if (entry.lazy || entry.unavailable) continue;
|
|
597
959
|
try {
|
|
598
960
|
await mount(agent, entry.name);
|
|
@@ -606,6 +968,7 @@ export async function apply(ctx, config) {
|
|
|
606
968
|
const state = agentStates.get(agent);
|
|
607
969
|
if (state === undefined || !state.active) return;
|
|
608
970
|
state.active = false;
|
|
971
|
+
state.workspaceRoot = undefined;
|
|
609
972
|
for (const dispose of state.disposers.splice(0)) {
|
|
610
973
|
try {
|
|
611
974
|
dispose();
|
|
@@ -615,6 +978,7 @@ export async function apply(ctx, config) {
|
|
|
615
978
|
}
|
|
616
979
|
for (const item of state.items.values()) await disposeItem(item);
|
|
617
980
|
state.items.clear();
|
|
981
|
+
releaseWorkspaceSession(agent);
|
|
618
982
|
};
|
|
619
983
|
|
|
620
984
|
/**
|
|
@@ -623,13 +987,27 @@ export async function apply(ctx, config) {
|
|
|
623
987
|
*/
|
|
624
988
|
const reconcile = async (agent) => {
|
|
625
989
|
if (agent === undefined) return;
|
|
626
|
-
const
|
|
990
|
+
const context = contextFor(agent);
|
|
627
991
|
const state = agentStates.get(agent);
|
|
628
|
-
if (enabled) {
|
|
629
|
-
|
|
992
|
+
if (!context.activation.enabled) {
|
|
993
|
+
await deactivate(agent);
|
|
630
994
|
return;
|
|
631
995
|
}
|
|
632
|
-
|
|
996
|
+
if (state === undefined || !state.active) {
|
|
997
|
+
await activate(agent);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (context.workspace.state === 'ok') {
|
|
1002
|
+
const launched = await ensureWorkspace(agent, context);
|
|
1003
|
+
if (launched?.launchError !== undefined) ctx.logger?.warn?.(`mcp-hub: ${launched.launchError}`);
|
|
1004
|
+
// Порт появился после init — адрес edt изменился, перемонтируем.
|
|
1005
|
+
try {
|
|
1006
|
+
await reload(agent, 'edt');
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
ctx.logger?.warn?.(`mcp-hub: не удалось обновить адрес EDT: ${error instanceof Error ? error.message : String(error)}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
633
1011
|
};
|
|
634
1012
|
|
|
635
1013
|
ctx.on('agent/created', async ({ agent }) => {
|
|
@@ -649,6 +1027,15 @@ export async function apply(ctx, config) {
|
|
|
649
1027
|
return next();
|
|
650
1028
|
});
|
|
651
1029
|
|
|
1030
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
1031
|
+
releaseWorkspaceSession(agent);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
ctx.effect(() => () => {
|
|
1035
|
+
ensuring.clear();
|
|
1036
|
+
workspaceSessions.clear();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
652
1039
|
// --- вики: чтение разрешено, запись отклоняется ---
|
|
653
1040
|
ctx.tools.guard((exec) => {
|
|
654
1041
|
const target = targetPathOf(exec);
|
|
@@ -662,13 +1049,16 @@ export async function apply(ctx, config) {
|
|
|
662
1049
|
const serversTool = () =>
|
|
663
1050
|
textTool(
|
|
664
1051
|
'edt_servers',
|
|
665
|
-
'List MCP servers of the 1C plugin: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
|
|
1052
|
+
'List MCP servers of the 1C plugin: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint, plus the worktree EDT environment when one is managed. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
|
|
666
1053
|
{},
|
|
667
1054
|
async (_args, exec) => {
|
|
668
|
-
const rows = snapshot(exec?.agent).map((item) => {
|
|
1055
|
+
const rows = snapshot(exec?.agent, { alive: cheapAlive }).map((item) => {
|
|
1056
|
+
const workspace = item.workspace;
|
|
669
1057
|
const extra = [
|
|
670
1058
|
item.root === undefined ? null : `root: ${item.root}`,
|
|
671
1059
|
item.endpoint === undefined ? null : `endpoint: ${item.endpoint}`,
|
|
1060
|
+
workspace?.state === 'worktree' ? `worktree: ${workspace.status}${workspace.port === undefined ? '' : ` port ${workspace.port}`}` : null,
|
|
1061
|
+
workspace?.state === 'error' ? `worktree: ${workspace.message}` : null,
|
|
672
1062
|
item.error === undefined ? null : `error: ${item.error}`,
|
|
673
1063
|
]
|
|
674
1064
|
.filter(Boolean)
|
|
@@ -727,6 +1117,156 @@ export async function apply(ctx, config) {
|
|
|
727
1117
|
},
|
|
728
1118
|
);
|
|
729
1119
|
|
|
1120
|
+
/** План окружения с необязательным переопределением корня (параметр root). */
|
|
1121
|
+
const workspacePlanFor = (exec, rootOverride) => {
|
|
1122
|
+
const context = contextFor(exec?.agent);
|
|
1123
|
+
if (rootOverride === undefined || rootOverride === '') return { context, plan: context.workspace };
|
|
1124
|
+
const settings = context.workspace?.settings ?? normalizeWorkspaceSettings(context.effective.config?.workspace, process.env);
|
|
1125
|
+
const root = isAbsolute(rootOverride) ? rootOverride : resolvePath(context.cwd, rootOverride);
|
|
1126
|
+
const buildDir = join(root, BUILD_DIR);
|
|
1127
|
+
return {
|
|
1128
|
+
context,
|
|
1129
|
+
plan: {
|
|
1130
|
+
state: 'ok',
|
|
1131
|
+
settings,
|
|
1132
|
+
root,
|
|
1133
|
+
buildDir,
|
|
1134
|
+
wsDir: join(buildDir, 'ws'),
|
|
1135
|
+
mainRoot: context.workspace?.mainRoot,
|
|
1136
|
+
warnings: context.workspace?.warnings ?? [],
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const workspaceText = (plan, deps = {}) => {
|
|
1142
|
+
const info = describeWorkspace(plan, deps);
|
|
1143
|
+
if (info.state === 'off') {
|
|
1144
|
+
const why =
|
|
1145
|
+
info.reason === 'disabled'
|
|
1146
|
+
? 'workspace.enabled=false'
|
|
1147
|
+
: 'основной checkout: окружение поднимается только в git worktree';
|
|
1148
|
+
return `Автоподъём EDT-окружения выключен — ${why}.`;
|
|
1149
|
+
}
|
|
1150
|
+
if (info.state === 'error') return `EDT-окружение недоступно: ${info.message}`;
|
|
1151
|
+
|
|
1152
|
+
const lines = [
|
|
1153
|
+
`Worktree: ${info.root}`,
|
|
1154
|
+
`Workspace EDT: ${info.wsDir}`,
|
|
1155
|
+
`Состояние: ${info.status}${info.port === undefined ? '' : `, порт ${info.port}`}${info.pid === undefined ? '' : `, pid ${info.pid}`}`,
|
|
1156
|
+
];
|
|
1157
|
+
if (info.phase !== undefined) {
|
|
1158
|
+
const elapsed = info.since === undefined ? '' : ` (${formatElapsed(info.since)})`;
|
|
1159
|
+
lines.push(`Фаза: ${info.phase}${elapsed}${info.detail === undefined ? '' : ` — ${info.detail}`}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (info.imported !== undefined) lines.push(`Импортировано проектов: ${info.imported}`);
|
|
1162
|
+
const home = detectEdtHome(info.settings, { platform: process.platform, env: process.env });
|
|
1163
|
+
const required = requiredJava(home.home, { platform: process.platform });
|
|
1164
|
+
const vm = resolveEdtVm(info.settings, home.home, { platform: process.platform, env: process.env });
|
|
1165
|
+
lines.push(`EDT: ${home.home ?? 'не найден'}${home.error === undefined ? '' : ` — ${home.error}`}`);
|
|
1166
|
+
lines.push(`Java: ${vm ?? 'авто'}${required.major === undefined ? '' : ` (EDT требует ${required.major})`}`);
|
|
1167
|
+
if (info.status !== 'ready') {
|
|
1168
|
+
if (home.mcpBundle === false) {
|
|
1169
|
+
lines.push('EDT-MCP: по файлам не подтверждён; готовность проверит /health');
|
|
1170
|
+
} else if (Array.isArray(home.mcpEvidence) && home.mcpEvidence.length > 0) {
|
|
1171
|
+
lines.push(`EDT-MCP: ${home.mcpEvidence[0]}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const importMode = info.settings.import;
|
|
1175
|
+
lines.push(`Импорт проектов: ${importMode === true ? 'да' : importMode === false ? 'нет' : 'авто (если workspace пуст)'}`);
|
|
1176
|
+
lines.push(`Plain text: ${info.settings.plainText === false ? 'нет' : 'да'}; скриншоты форм: ${info.settings.formScreenshots === false ? 'нет' : 'да'}`);
|
|
1177
|
+
const prefsText = readPrefs(info.buildDir);
|
|
1178
|
+
const plainPref = prefsText === undefined ? undefined : prefValue(prefsText, 'mcpPlainTextMode');
|
|
1179
|
+
lines.push(
|
|
1180
|
+
`Plain text в prefs: ${prefsText === undefined ? 'файла ещё нет' : plainPref === undefined ? 'ключа нет' : plainPref === 'true' ? 'да' : 'нет'}`,
|
|
1181
|
+
);
|
|
1182
|
+
if (prefsText !== undefined && info.settings.plainText !== false && plainPref !== 'true') {
|
|
1183
|
+
lines.push('! prefs не содержат mcpPlainTextMode=true — выполните restart окружения');
|
|
1184
|
+
}
|
|
1185
|
+
if (info.warning !== undefined) {
|
|
1186
|
+
const warning = info.warning;
|
|
1187
|
+
const memory = warning.used === undefined ? '' : ` (${warning.used}/${warning.total} MB)`;
|
|
1188
|
+
lines.push(`! импорт шёл на пределе памяти${memory} — поднимите workspace.xmxImport`);
|
|
1189
|
+
}
|
|
1190
|
+
for (const warning of info.warnings) lines.push(`! ${warning}`);
|
|
1191
|
+
lines.push(`Логи: ${join(info.buildDir, 'logs')}; импорт: ${join(info.wsDir, '.metadata', '.log')}`);
|
|
1192
|
+
return lines.join('\n');
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const workspaceTool = () =>
|
|
1196
|
+
textTool(
|
|
1197
|
+
'edt_workspace',
|
|
1198
|
+
'Manage the EDT workspace of this git worktree: action=status reports where build/ws is, whether EDT is ready, its port, phase and warnings; init starts it (or retries a failed start); stop stops the EDT process; restart stops and starts again, optionally re-importing projects; import imports EDT projects while EDT is stopped (it runs in the background).',
|
|
1199
|
+
{
|
|
1200
|
+
action: {
|
|
1201
|
+
type: 'string',
|
|
1202
|
+
description: 'status | init | stop | restart | import',
|
|
1203
|
+
enum: ['status', 'init', 'stop', 'restart', 'import'],
|
|
1204
|
+
},
|
|
1205
|
+
root: { type: 'string', description: 'Worktree root to manage; defaults to the project worktree of this session.' },
|
|
1206
|
+
import: { type: 'boolean', description: 'For init/restart: import EDT projects even if the workspace already has some.' },
|
|
1207
|
+
},
|
|
1208
|
+
async (args, exec) => {
|
|
1209
|
+
const action = args.action ?? 'status';
|
|
1210
|
+
const { context, plan } = workspacePlanFor(exec, args.root);
|
|
1211
|
+
if (action === 'status') return { text: workspaceText(plan, { alive: (pid) => pidAlive(pid, ['1cedt']) }) };
|
|
1212
|
+
|
|
1213
|
+
if (plan === undefined || plan.state !== 'ok') {
|
|
1214
|
+
throw new Error(`EDT-окружение недоступно: ${plan?.message ?? workspaceText(plan)}`);
|
|
1215
|
+
}
|
|
1216
|
+
// root=… не только поднимает окружение, но и становится контекстом
|
|
1217
|
+
// сессии: entriesFor подменит URL edt на порт этого workspace.
|
|
1218
|
+
if (args.root !== undefined && args.root !== '') stateOf(exec?.agent).workspaceRoot = plan.root;
|
|
1219
|
+
const record = registerWorkspaceSession(plan.buildDir, exec?.agent, plan.settings);
|
|
1220
|
+
|
|
1221
|
+
if (action === 'stop') {
|
|
1222
|
+
const result = await stopWorkspaceNow(plan.root);
|
|
1223
|
+
record.suppressed = true;
|
|
1224
|
+
return { text: result.stopped ? `EDT остановлен (pid ${result.pid}).` : `EDT не остановлен: ${result.reason}.` };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
record.suppressed = false;
|
|
1228
|
+
if (action === 'restart' || action === 'import') {
|
|
1229
|
+
if (action === 'import') {
|
|
1230
|
+
const current = workspaceState(plan.buildDir, { alive: cheapAlive });
|
|
1231
|
+
if (current.label === 'ready' || busyLaunching(current)) {
|
|
1232
|
+
throw new Error('EDT запущен: импорт возможен только при остановленном EDT. Используйте action=restart с import: true.');
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (action === 'restart') await stopWorkspaceNow(plan.root);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const forceImport = args.import === true || action === 'import';
|
|
1239
|
+
const result = await ensureWorkspace(exec?.agent, { ...context, workspace: plan }, { force: true, import: forceImport });
|
|
1240
|
+
if (result === undefined) throw new Error('Не удалось запустить подъём окружения: смотрите логи build/logs.');
|
|
1241
|
+
if (result.launchError !== undefined) throw new Error(`${result.launchError}\n\n${workspaceText(plan)}`);
|
|
1242
|
+
if (result.label === 'failed') {
|
|
1243
|
+
const info = describeWorkspace(plan, { alive: cheapAlive });
|
|
1244
|
+
const warning = info.warning;
|
|
1245
|
+
const memory =
|
|
1246
|
+
warning === undefined
|
|
1247
|
+
? ''
|
|
1248
|
+
: `\n! импорт шёл на пределе памяти${warning.used === undefined ? '' : ` (${warning.used}/${warning.total} MB)`} — поднимите workspace.xmxImport`;
|
|
1249
|
+
throw new Error(`EDT-окружение не поднялось: ${info.detail ?? 'см. build/logs/edt.log'}${memory}`);
|
|
1250
|
+
}
|
|
1251
|
+
if (result.label === 'ready') {
|
|
1252
|
+
return { text: `EDT уже готов (порт ${result.port ?? '-'}).\n\n${workspaceText(plan)}` };
|
|
1253
|
+
}
|
|
1254
|
+
const headline =
|
|
1255
|
+
action === 'import'
|
|
1256
|
+
? 'Импорт проектов запущен в фоне.'
|
|
1257
|
+
: action === 'restart'
|
|
1258
|
+
? 'EDT перезапускается в фоне.'
|
|
1259
|
+
: 'Подъём EDT-окружения запущен в фоне.';
|
|
1260
|
+
return {
|
|
1261
|
+
text: [
|
|
1262
|
+
headline,
|
|
1263
|
+
'Первый запуск занимает 1–3 минуты (с импортом дольше). Проверяйте готовность через action=status.',
|
|
1264
|
+
workspaceText(plan),
|
|
1265
|
+
].join('\n'),
|
|
1266
|
+
};
|
|
1267
|
+
},
|
|
1268
|
+
);
|
|
1269
|
+
|
|
730
1270
|
const configTool = () =>
|
|
731
1271
|
textTool(
|
|
732
1272
|
'edt_config',
|
|
@@ -747,12 +1287,27 @@ export async function apply(ctx, config) {
|
|
|
747
1287
|
if (path === undefined) throw new Error('edt_config: cannot resolve the settings path');
|
|
748
1288
|
|
|
749
1289
|
if (args.action === 'show') {
|
|
750
|
-
const
|
|
1290
|
+
const context = contextFor(exec?.agent);
|
|
1291
|
+
const effective = context.effective;
|
|
751
1292
|
const { errors, warnings } = validateConfig(effective.config);
|
|
752
|
-
const activation =
|
|
1293
|
+
const activation = context.activation;
|
|
1294
|
+
const workspace = describeWorkspace(context.workspace, { alive: cheapAlive });
|
|
1295
|
+
const origin = originCwdOf(exec?.agent);
|
|
1296
|
+
const dirsLine =
|
|
1297
|
+
origin === context.cwd
|
|
1298
|
+
? `Каталог: ${context.cwd}`
|
|
1299
|
+
: `Каталог: исходный ${origin}, рабочий ${context.cwd}${context.workspaceOverride === true ? ' (через edt_workspace root)' : ''}`;
|
|
1300
|
+
const workspaceLine =
|
|
1301
|
+
workspace.state === 'worktree'
|
|
1302
|
+
? `EDT-окружение worktree: ${workspace.status}, порт ${workspace.port ?? '-'}, ${workspace.root}`
|
|
1303
|
+
: workspace.state === 'error'
|
|
1304
|
+
? `EDT-окружение worktree: ошибка — ${workspace.message}`
|
|
1305
|
+
: 'EDT-окружение worktree: выключено (поднимается только в git worktree)';
|
|
753
1306
|
const head = [
|
|
754
1307
|
`Проект: ${activation.enabled ? 'включён' : 'выключен'} — ${activationReason(activation)}`,
|
|
1308
|
+
dirsLine,
|
|
755
1309
|
`Источники: ${effective.sources.length > 0 ? effective.sources.join(', ') : 'только cordis.patch.yml'}`,
|
|
1310
|
+
workspaceLine,
|
|
756
1311
|
`Действующие настройки:\n${stringifyConfig(effective.config)}`,
|
|
757
1312
|
];
|
|
758
1313
|
if (errors.length > 0) head.push(`Ошибки:\n${errors.map((line) => ` ✗ ${line}`).join('\n')}`);
|
|
@@ -808,8 +1363,10 @@ export async function apply(ctx, config) {
|
|
|
808
1363
|
inner.skills.registerProvider((control) => {
|
|
809
1364
|
skillsControl = control;
|
|
810
1365
|
return createSkillsProvider({
|
|
811
|
-
|
|
812
|
-
|
|
1366
|
+
// Навыки гейтятся тем же контекстом проекта, что и инструменты: в
|
|
1367
|
+
// worktree включённость наследуется от основного checkout.
|
|
1368
|
+
resolveConfig: (cwd) => projectContextForCwd(cwd ?? process.cwd()).effective.config,
|
|
1369
|
+
isEnabled: (cwd) => projectContextForCwd(cwd ?? process.cwd()).activation.enabled,
|
|
813
1370
|
logger: ctx.logger,
|
|
814
1371
|
});
|
|
815
1372
|
});
|
|
@@ -827,10 +1384,17 @@ export async function apply(ctx, config) {
|
|
|
827
1384
|
const agent = context?.agent;
|
|
828
1385
|
// В выключенном проекте секция пустая: renderPrompt выбрасывает её,
|
|
829
1386
|
// поэтому в промпте не остаётся ни строчки про 1С.
|
|
830
|
-
if (agent === undefined
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
1387
|
+
if (agent === undefined) return '';
|
|
1388
|
+
const projectContext = contextFor(agent);
|
|
1389
|
+
if (!projectContext.activation.enabled) return '';
|
|
1390
|
+
const { roots } = normalizeWiki(projectContext.effective.config?.wiki, { cwd: projectContext.cwd });
|
|
1391
|
+
return buildRulesText(
|
|
1392
|
+
process.env,
|
|
1393
|
+
snapshot(agent, { alive: cheapAlive }),
|
|
1394
|
+
roots,
|
|
1395
|
+
projectContext.effective.config?.tests,
|
|
1396
|
+
describeWorkspace(projectContext.workspace, { alive: cheapAlive }),
|
|
1397
|
+
);
|
|
834
1398
|
},
|
|
835
1399
|
});
|
|
836
1400
|
});
|