dsh-edt-projects 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/cordis.patch.yml +26 -0
- package/lib/bsl.mjs +3 -1
- package/lib/config.mjs +41 -8
- package/lib/enable.mjs +28 -0
- package/lib/index.js +511 -51
- package/lib/repo.mjs +153 -0
- package/lib/schema.mjs +57 -1
- package/lib/workspace-run.mjs +521 -0
- package/lib/workspace.mjs +776 -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,40 @@ 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 } from './repo.mjs';
|
|
48
|
+
import {
|
|
49
|
+
BUILD_DIR,
|
|
50
|
+
detectEdtHome,
|
|
51
|
+
formatElapsed,
|
|
52
|
+
isPortOpen,
|
|
53
|
+
normalizeWorkspaceSettings,
|
|
54
|
+
pickFreePort,
|
|
55
|
+
pidAlive,
|
|
56
|
+
prepareWorkspaceSnapshot,
|
|
57
|
+
probeHealth,
|
|
58
|
+
readPort,
|
|
59
|
+
removeStamp,
|
|
60
|
+
resolveWorkspaceTarget,
|
|
61
|
+
stateOf as workspaceState,
|
|
62
|
+
writePort,
|
|
63
|
+
writeStamp,
|
|
64
|
+
writeWorkspaceSnapshot,
|
|
65
|
+
} from './workspace.mjs';
|
|
66
|
+
import { cmdStop as stopWorkspaceNow } from './workspace-run.mjs';
|
|
39
67
|
|
|
40
68
|
const HEADER = `## Работа с 1С через MCP
|
|
41
69
|
|
|
@@ -51,7 +79,7 @@ const PIN_RULES = `
|
|
|
51
79
|
|
|
52
80
|
Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`edt_servers\`; подключить — \`edt_pin(server)\`, отключить — \`edt_unpin(server)\`.
|
|
53
81
|
|
|
54
|
-
- Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны задачи или статусы — \`tasks\`.
|
|
82
|
+
- Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны стандарты или расшифровка кода ошибки — \`v8std\`, нужны задачи или статусы — \`tasks\`.
|
|
55
83
|
- Для \`bsl\` передавай корень: \`edt_pin("bsl", root: "<каталог конфигурации>")\`. Без корня берётся корень из настроек проекта, иначе каталог запуска DSH.
|
|
56
84
|
- Первый \`edt_pin("bsl")\` может занять время: при отсутствии готового jar сервер скачивается (~130 МБ). Предупреди пользователя, если это долго.
|
|
57
85
|
- После \`edt_unpin\` инструменты сервера исчезают из запроса. Не отключай сервер, пока работа с ним не закончена.
|
|
@@ -96,6 +124,32 @@ const TASKS_RULES = `
|
|
|
96
124
|
- Задачи, их статусы и комментарии — только через \`tasks__*\`. Не выдумывай номера задач, статусы и тексты комментариев.
|
|
97
125
|
- Состояние задачи меняй только по явной просьбе пользователя, а перед этим покажи, что именно изменится.`;
|
|
98
126
|
|
|
127
|
+
const V8STD_RULES = `
|
|
128
|
+
### Стандарты и ошибки (v8std)
|
|
129
|
+
|
|
130
|
+
v8std — внешняя база стандартов 1С и диагностик. Она объясняет правило и код ошибки, но не анализирует проект и не меняет код. Подключается по требованию: \`edt_pin("v8std")\`; в списке инструментов видна как \`mcp__v8std__*\`.
|
|
131
|
+
|
|
132
|
+
- EDT (\`get_project_errors\`) или BSL LS (\`analyze_file\`) вернул код — разреши его через \`v8std_explain_diagnostics\` (принимает \`acc 1245\`, \`АПК:361\`, \`bslls:AssignAliasFieldsInQuery\`, коды EDT), затем прочитай правило: \`v8std_get_page\`.
|
|
133
|
+
- Вопрос «как правильно по стандартам» или неясная тема — \`v8std_search\`; один метод BSL или фрагмент SDBL — \`v8std_explain_snippet\`. Это эвристика, а не подтверждённое нарушение.
|
|
134
|
+
- Переходы между стандартом и связанными диагностиками — \`v8std_get_related\`.
|
|
135
|
+
- В ответе называй точный код, заголовок, номер стандарта и ссылку. Не выдумывай номера стандартов: если v8std ничего не нашёл, так и скажи.
|
|
136
|
+
- Найденный текст — справка, а не приговор: проверку даёт EDT или BSL, v8std объясняет, что она означает.
|
|
137
|
+
- v8std только читает; правки исходников — через EDT-MCP, по правилам выше.`;
|
|
138
|
+
|
|
139
|
+
/** Блок про отдельное EDT-окружение worktree. */
|
|
140
|
+
function workspaceRules(workspace) {
|
|
141
|
+
return `
|
|
142
|
+
### Окружение EDT в worktree
|
|
143
|
+
|
|
144
|
+
Сессия открыта в отдельном git worktree, поэтому у неё свой workspace EDT: ${workspace.wsDir}. Его поднимает плагин, отдельно от основного checkout.
|
|
145
|
+
|
|
146
|
+
- Первый запуск занимает 1–3 минуты, с импортом проектов — дольше. Готовность проверяй через \`edt_workspace\` (action=status): состояние \`ready\` и порт.
|
|
147
|
+
- Инструменты \`mcp__edt__*\` появляются после готовности сервера. Если вызов не удался из-за соединения — проверь статус, не перезапускай окружение самовольно.
|
|
148
|
+
- Правки исходников — через EDT по правилам выше; workspace EDT знает проекты этого worktree.
|
|
149
|
+
- Не удаляй \`${workspace.root}/build\` при живом EDT. Остановка — \`edt_workspace\` action=stop, перезапуск — action=restart.
|
|
150
|
+
- Логи: ${workspace.root}/build/logs (edt.log, edt-import.log, init.log).`;
|
|
151
|
+
}
|
|
152
|
+
|
|
99
153
|
/**
|
|
100
154
|
* Собирает блок правил для вики.
|
|
101
155
|
* @param roots результат normalizeWiki().roots
|
|
@@ -158,13 +212,15 @@ function vanessaRules(settings) {
|
|
|
158
212
|
}
|
|
159
213
|
|
|
160
214
|
/**
|
|
161
|
-
* Текст правил под фактически настроенные серверы, их состояние,
|
|
215
|
+
* Текст правил под фактически настроенные серверы, их состояние, вики, тесты
|
|
216
|
+
* и управляемое окружение worktree.
|
|
162
217
|
* @param env окружение процесса (для тестов передаётся явно).
|
|
163
218
|
* @param servers список записей { name, state }.
|
|
164
219
|
* @param wikiRoots корни вики из normalizeWiki().
|
|
165
220
|
* @param tests секция tests из настроек (флаги YAxUnit / Vanessa).
|
|
221
|
+
* @param workspace описание окружения worktree (describeWorkspace) или undefined.
|
|
166
222
|
*/
|
|
167
|
-
export function buildRulesText(env = process.env, servers = [], wikiRoots = [], tests = {}) {
|
|
223
|
+
export function buildRulesText(env = process.env, servers = [], wikiRoots = [], tests = {}, workspace = undefined) {
|
|
168
224
|
const usable = new Set(servers.filter((s) => s.state !== 'unavailable').map((s) => s.name));
|
|
169
225
|
const byEnv = servers.length === 0;
|
|
170
226
|
const blocks = [HEADER];
|
|
@@ -175,6 +231,8 @@ export function buildRulesText(env = process.env, servers = [], wikiRoots = [],
|
|
|
175
231
|
}
|
|
176
232
|
if (usable.has('bsl') || (byEnv && (env.BSL_LS_JAR || env.BSL_MCP_URL))) blocks.push(BSL_RULES);
|
|
177
233
|
if (usable.has('tasks') || (byEnv && env.TASKS_MCP_URL)) blocks.push(TASKS_RULES);
|
|
234
|
+
if (usable.has('v8std') || (byEnv && env.V8STD_MCP_URL)) blocks.push(V8STD_RULES);
|
|
235
|
+
if (workspace?.state === 'worktree') blocks.push(workspaceRules(workspace));
|
|
178
236
|
|
|
179
237
|
const lazy = servers.filter((s) => s.state === 'lazy').map((s) => s.name);
|
|
180
238
|
const unavailable = servers.filter((s) => s.state === 'unavailable').map((s) => s.name);
|
|
@@ -235,11 +293,23 @@ export function mountConfig(entry) {
|
|
|
235
293
|
return config;
|
|
236
294
|
}
|
|
237
295
|
|
|
238
|
-
/**
|
|
239
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Настройки BSL LS из окружения, перекрытые файлами настроек.
|
|
298
|
+
* @param fallbackRoot корень проекта: работе в worktree нужен его каталог,
|
|
299
|
+
* а не каталог процесса DSH. Относительный `servers.bsl.root` считается
|
|
300
|
+
* от него же.
|
|
301
|
+
*/
|
|
302
|
+
function bslSettingsFor(effective, fallbackRoot = process.cwd()) {
|
|
240
303
|
const base = bslSettings(process.env);
|
|
241
304
|
const fromFile = effective.config?.servers?.bsl ?? {};
|
|
242
305
|
const pick = (key, fallback) => (fromFile[key] === undefined ? fallback : fromFile[key]);
|
|
306
|
+
const rawRoot = pick('root', base.root);
|
|
307
|
+
const root =
|
|
308
|
+
typeof rawRoot !== 'string' || rawRoot.trim() === ''
|
|
309
|
+
? fallbackRoot
|
|
310
|
+
: isAbsolute(rawRoot)
|
|
311
|
+
? rawRoot
|
|
312
|
+
: resolvePath(fallbackRoot, rawRoot);
|
|
243
313
|
return {
|
|
244
314
|
...base,
|
|
245
315
|
jar: pick('jar', base.jar),
|
|
@@ -247,8 +317,8 @@ function bslSettingsFor(effective) {
|
|
|
247
317
|
version: pick('version', base.version),
|
|
248
318
|
cacheDir: pick('cacheDir', base.cacheDir),
|
|
249
319
|
java: pick('java', base.java),
|
|
250
|
-
cwd: pick('cwd', base.cwd),
|
|
251
|
-
root
|
|
320
|
+
cwd: pick('cwd', base.cwd) || fallbackRoot,
|
|
321
|
+
root,
|
|
252
322
|
port: pick('port', base.port),
|
|
253
323
|
mode: pick('mode', base.mode),
|
|
254
324
|
mcpUrl: pick('mcpUrl', base.mcpUrl),
|
|
@@ -286,7 +356,9 @@ const activationReason = (activation) => {
|
|
|
286
356
|
case 'project-file':
|
|
287
357
|
return 'есть .dsh-1c.json проекта';
|
|
288
358
|
case 'allowlist':
|
|
289
|
-
return
|
|
359
|
+
return activation.inherited === true
|
|
360
|
+
? `путь в projects (${activation.entry}), наследовано из ${activation.path}`
|
|
361
|
+
: `путь в projects (${activation.entry})`;
|
|
290
362
|
default:
|
|
291
363
|
return 'проект не выбран: нет .dsh-1c.json и его нет в projects';
|
|
292
364
|
}
|
|
@@ -323,26 +395,106 @@ export async function apply(ctx, config) {
|
|
|
323
395
|
|
|
324
396
|
const cwdOf = (agent) => sessionCwd(agent, ctx.workingDirectory);
|
|
325
397
|
|
|
326
|
-
|
|
327
|
-
|
|
398
|
+
/** Уникальные пути в порядке появления (для слоёв настроек и allowlist). */
|
|
399
|
+
const uniquePaths = (values) => {
|
|
400
|
+
const seen = new Set();
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const value of values) {
|
|
403
|
+
if (typeof value !== 'string' || value === '') continue;
|
|
404
|
+
const key = value.toLowerCase();
|
|
405
|
+
if (seen.has(key)) continue;
|
|
406
|
+
seen.add(key);
|
|
407
|
+
out.push(value);
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Контекст проекта для каталога сессии: репозитории, слои настроек,
|
|
414
|
+
* активация и план EDT-окружения. Для worktree настройки берутся ещё и из
|
|
415
|
+
* основного checkout, а активация наследуется от него же.
|
|
416
|
+
*/
|
|
417
|
+
const projectContextForCwd = (cwd) => {
|
|
418
|
+
const repositories = repositoriesUnder(cwd);
|
|
419
|
+
// Порядок = порядок слияния: основной checkout слабее worktree, cwd сильнее всех.
|
|
420
|
+
const paths = uniquePaths([...repositories.flatMap((repo) => [repo.mainRoot, repo.root]), cwd]);
|
|
421
|
+
const effective = resolveConfig({ cwd, projectCwds: paths, env: process.env, base });
|
|
328
422
|
for (const error of effective.errors) {
|
|
329
423
|
if (loggedErrors.has(error)) continue;
|
|
330
424
|
loggedErrors.add(error);
|
|
331
425
|
ctx.logger?.warn?.(`mcp-hub: ${error}`);
|
|
332
426
|
}
|
|
333
|
-
|
|
427
|
+
const hasProjectFile = paths.some((dir) => projectConfigLoaded(dir));
|
|
428
|
+
const activation = activationForPaths({ cwd, config: effective.config, hasProjectFile, paths });
|
|
429
|
+
const enabledRepositories = repositories.filter(
|
|
430
|
+
(repo) =>
|
|
431
|
+
activationForPaths({
|
|
432
|
+
cwd: repo.root,
|
|
433
|
+
config: effective.config,
|
|
434
|
+
hasProjectFile: projectConfigLoaded(repo.root) || projectConfigLoaded(repo.mainRoot),
|
|
435
|
+
paths: [repo.root, repo.mainRoot],
|
|
436
|
+
}).enabled,
|
|
437
|
+
);
|
|
438
|
+
// Если проект включён самим каталогом сессии (файл в task space), явного
|
|
439
|
+
// списка включённых репозиториев может не быть — берём репозитории сессии.
|
|
440
|
+
const workspaceRepositories =
|
|
441
|
+
enabledRepositories.length > 0 ? enabledRepositories : activation.enabled ? repositories : [];
|
|
442
|
+
const workspace = resolveWorkspaceTarget({
|
|
443
|
+
cwd,
|
|
444
|
+
config: effective.config,
|
|
445
|
+
repositories: workspaceRepositories,
|
|
446
|
+
env: process.env,
|
|
447
|
+
});
|
|
448
|
+
return { cwd, repositories, enabledRepositories, paths, effective, activation, workspace };
|
|
334
449
|
};
|
|
335
450
|
|
|
336
|
-
const
|
|
451
|
+
const contextFor = (agent) => projectContextForCwd(cwdOf(agent));
|
|
452
|
+
const effectiveFor = (agent) => contextFor(agent).effective;
|
|
337
453
|
|
|
338
|
-
|
|
339
|
-
|
|
454
|
+
/** Каталог, от которого считается корень BSL и относительный workspace.root. */
|
|
455
|
+
const fallbackRootFor = (context) => {
|
|
456
|
+
if (context.workspace.state === 'ok') return context.workspace.root;
|
|
457
|
+
if (context.enabledRepositories.length > 0) return context.enabledRepositories[0].root;
|
|
458
|
+
return context.cwd;
|
|
459
|
+
};
|
|
340
460
|
|
|
341
|
-
|
|
461
|
+
/** Дешёвая проверка живости: без tasklist, чтобы не звать её на каждом шаге. */
|
|
462
|
+
const cheapAlive = (pid) => pidAlive(pid);
|
|
342
463
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Описание окружения для правил, снапшота и инструмента edt_workspace.
|
|
466
|
+
* `state` — включено ли управление ('worktree'/'off'/'error'),
|
|
467
|
+
* `status` — фактическая фаза (absent/stopped/starting/ready/stale/failed).
|
|
468
|
+
*/
|
|
469
|
+
const describeWorkspace = (plan, deps = {}) => {
|
|
470
|
+
if (plan === undefined || plan.state === 'off') return { state: 'off', reason: plan?.reason };
|
|
471
|
+
if (plan.state === 'error') {
|
|
472
|
+
return { state: 'error', reason: plan.reason, message: plan.message, settings: plan.settings };
|
|
473
|
+
}
|
|
474
|
+
const current = workspaceState(plan.buildDir, deps);
|
|
475
|
+
const phase = current.progress?.edt;
|
|
476
|
+
return {
|
|
477
|
+
state: 'worktree',
|
|
478
|
+
status: current.label,
|
|
479
|
+
root: plan.root,
|
|
480
|
+
buildDir: plan.buildDir,
|
|
481
|
+
wsDir: plan.wsDir,
|
|
482
|
+
mainRoot: plan.mainRoot,
|
|
483
|
+
port: current.port ?? undefined,
|
|
484
|
+
pid: current.pid?.pid ?? undefined,
|
|
485
|
+
phase: phase?.phase,
|
|
486
|
+
since: phase?.since,
|
|
487
|
+
detail: phase?.detail,
|
|
488
|
+
imported: current.imported?.projects?.length,
|
|
489
|
+
settings: plan.settings,
|
|
490
|
+
warnings: plan.warnings ?? [],
|
|
491
|
+
rootSource: plan.rootSource,
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const entriesFor = (context) => {
|
|
496
|
+
const bsl = bslSettingsFor(context.effective, fallbackRootFor(context));
|
|
497
|
+
const entries = normalizeServers(context.effective.config?.servers).map((entry) => {
|
|
346
498
|
if (entry.raw.provision === true) {
|
|
347
499
|
entry.provisioned = true;
|
|
348
500
|
entry.transport = bsl.mcpUrl === undefined ? 'stdio' : 'streamable-http';
|
|
@@ -350,6 +502,16 @@ export async function apply(ctx, config) {
|
|
|
350
502
|
}
|
|
351
503
|
return entry;
|
|
352
504
|
});
|
|
505
|
+
|
|
506
|
+
// В worktree EDT монтируется на порт поднятого для него окружения.
|
|
507
|
+
if (context.workspace.state === 'ok') {
|
|
508
|
+
const port = readPort(context.workspace.buildDir) ?? context.workspace.port;
|
|
509
|
+
const edt = entries.find((entry) => entry.name === 'edt');
|
|
510
|
+
if (edt !== undefined && Number.isInteger(port)) {
|
|
511
|
+
edt.raw = { ...edt.raw, url: `http://127.0.0.1:${port}/mcp` };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return entries;
|
|
353
515
|
};
|
|
354
516
|
|
|
355
517
|
/** Состояние активации одного агента (сессии): инструменты и смонтированные серверы. */
|
|
@@ -388,9 +550,10 @@ export async function apply(ctx, config) {
|
|
|
388
550
|
}
|
|
389
551
|
};
|
|
390
552
|
|
|
391
|
-
const snapshot = (agent) => {
|
|
392
|
-
const
|
|
393
|
-
|
|
553
|
+
const snapshot = (agent, deps = {}) => {
|
|
554
|
+
const context = contextFor(agent);
|
|
555
|
+
const workspace = describeWorkspace(context.workspace, deps);
|
|
556
|
+
return entriesFor(context).map((entry) => {
|
|
394
557
|
const item = mapFor(agent).get(entry.name);
|
|
395
558
|
let value = 'lazy';
|
|
396
559
|
if (entry.unavailable) value = 'unavailable';
|
|
@@ -405,6 +568,7 @@ export async function apply(ctx, config) {
|
|
|
405
568
|
endpoint: item?.supervisor?.url ?? entry.raw?.url,
|
|
406
569
|
tools: toolNamesOf(agent, entry.name).length,
|
|
407
570
|
error: item?.error,
|
|
571
|
+
workspace: entry.name === 'edt' ? workspace : undefined,
|
|
408
572
|
};
|
|
409
573
|
});
|
|
410
574
|
};
|
|
@@ -421,8 +585,8 @@ export async function apply(ctx, config) {
|
|
|
421
585
|
};
|
|
422
586
|
|
|
423
587
|
const mount = async (agent, serverName, options = {}) => {
|
|
424
|
-
const
|
|
425
|
-
const entries = entriesFor(
|
|
588
|
+
const context = contextFor(agent);
|
|
589
|
+
const entries = entriesFor(context);
|
|
426
590
|
const entry = entries.find((candidate) => candidate.name === serverName);
|
|
427
591
|
if (entry === undefined) {
|
|
428
592
|
throw new Error(`unknown MCP server "${serverName}"; configured: ${entries.map((item) => item.name).join(', ') || 'none'}`);
|
|
@@ -432,7 +596,7 @@ export async function apply(ctx, config) {
|
|
|
432
596
|
}
|
|
433
597
|
|
|
434
598
|
const item = itemFor(agent, entry);
|
|
435
|
-
const bsl = bslSettingsFor(effective);
|
|
599
|
+
const bsl = bslSettingsFor(context.effective, fallbackRootFor(context));
|
|
436
600
|
const root = options.root ?? entry.raw.root ?? bsl.root;
|
|
437
601
|
|
|
438
602
|
if (item.fiber && !item.error) {
|
|
@@ -502,8 +666,8 @@ export async function apply(ctx, config) {
|
|
|
502
666
|
};
|
|
503
667
|
|
|
504
668
|
const unmount = async (agent, serverName) => {
|
|
505
|
-
const
|
|
506
|
-
const entry = entriesFor(
|
|
669
|
+
const context = contextFor(agent);
|
|
670
|
+
const entry = entriesFor(context).find((candidate) => candidate.name === serverName);
|
|
507
671
|
const map = mapFor(agent);
|
|
508
672
|
const item = map.get(serverName);
|
|
509
673
|
if (item === undefined || !item.fiber) {
|
|
@@ -518,8 +682,8 @@ export async function apply(ctx, config) {
|
|
|
518
682
|
|
|
519
683
|
/** Перечитывает настройки и перезапускает серверы, у которых изменился конфиг. */
|
|
520
684
|
const reload = async (agent, serverName) => {
|
|
521
|
-
const
|
|
522
|
-
const entries = entriesFor(
|
|
685
|
+
const context = contextFor(agent);
|
|
686
|
+
const entries = entriesFor(context);
|
|
523
687
|
const targets = serverName === undefined ? entries : entries.filter((entry) => entry.name === serverName);
|
|
524
688
|
if (serverName !== undefined && targets.length === 0) {
|
|
525
689
|
throw new Error(`unknown MCP server "${serverName}"; configured: ${entries.map((item) => item.name).join(', ') || 'none'}`);
|
|
@@ -529,9 +693,8 @@ export async function apply(ctx, config) {
|
|
|
529
693
|
for (const entry of targets) {
|
|
530
694
|
const item = mapFor(agent).get(entry.name);
|
|
531
695
|
if (item === undefined || !item.fiber) continue;
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const fresh = signatureOf(entry, root);
|
|
696
|
+
const freshRoot = entry.raw.root ?? bslSettingsFor(context.effective, fallbackRootFor(context)).root;
|
|
697
|
+
const fresh = signatureOf(entry, freshRoot);
|
|
535
698
|
if (fresh === item.signature) continue;
|
|
536
699
|
await disposeItem(item);
|
|
537
700
|
const result = await mount(agent, entry.name, {});
|
|
@@ -569,17 +732,155 @@ export async function apply(ctx, config) {
|
|
|
569
732
|
}
|
|
570
733
|
};
|
|
571
734
|
|
|
735
|
+
// --- окружение EDT в worktree ----------------------------------------------
|
|
736
|
+
const initializerPath = fileURLToPath(new URL('./workspace-run.mjs', import.meta.url));
|
|
737
|
+
/** buildDir -> Promise подъёма (одна на процесс, чтобы не поднять два EDT). */
|
|
738
|
+
const ensuring = new Map();
|
|
739
|
+
/** buildDir -> { agents, settings, startedByUs } для остановки после последней сессии. */
|
|
740
|
+
const workspaceSessions = new Map();
|
|
741
|
+
|
|
742
|
+
const spawnInitializer = (root, action = 'init') => {
|
|
743
|
+
try {
|
|
744
|
+
const child = spawn(process.execPath, [initializerPath, action, '--root', root], {
|
|
745
|
+
cwd: root,
|
|
746
|
+
detached: true,
|
|
747
|
+
stdio: 'ignore',
|
|
748
|
+
windowsHide: true,
|
|
749
|
+
});
|
|
750
|
+
child.unref();
|
|
751
|
+
return true;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
ctx.logger?.warn?.(`mcp-hub: не удалось запустить ${action} окружения EDT: ${error instanceof Error ? error.message : String(error)}`);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const registerWorkspaceSession = (buildDir, agent, settings) => {
|
|
759
|
+
let record = workspaceSessions.get(buildDir);
|
|
760
|
+
if (record === undefined) {
|
|
761
|
+
record = { agents: new Set(), settings, startedByUs: false };
|
|
762
|
+
workspaceSessions.set(buildDir, record);
|
|
763
|
+
}
|
|
764
|
+
if (agent !== undefined) record.agents.add(agent);
|
|
765
|
+
if (settings !== undefined) record.settings = settings;
|
|
766
|
+
return record;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
/** Фаза подъёма без pid (идёт импорт или старт GUI) тоже считается запуском. */
|
|
770
|
+
const busyLaunching = (current, settings = {}) => {
|
|
771
|
+
if (current.label === 'starting') return true;
|
|
772
|
+
const phase = current.progress?.edt;
|
|
773
|
+
if (phase?.phase === undefined || ['failed', 'ready'].includes(phase.phase)) return false;
|
|
774
|
+
const updated = Date.parse(phase.updated ?? phase.since ?? '');
|
|
775
|
+
if (Number.isNaN(updated)) return true;
|
|
776
|
+
const budget = Math.max(settings.startTimeout ?? 1800, settings.importTimeout ?? 3600) * 1000 + 5 * 60_000;
|
|
777
|
+
return Date.now() - updated < budget;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Гарантирует подъём окружения: занимает порт, пишет снимок и запускает
|
|
782
|
+
* фоновый init. Готовое/стартующее окружение только принимается.
|
|
783
|
+
*/
|
|
784
|
+
const ensureWorkspace = async (agent, context = contextFor(agent), options = {}) => {
|
|
785
|
+
const plan = context.workspace;
|
|
786
|
+
if (plan?.state !== 'ok') return undefined;
|
|
787
|
+
const key = plan.buildDir;
|
|
788
|
+
const record = registerWorkspaceSession(key, agent, plan.settings);
|
|
789
|
+
// После edt_workspace stop не поднимаем заново, пока не попросят init.
|
|
790
|
+
if (record.suppressed === true && options.force !== true) {
|
|
791
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
792
|
+
}
|
|
793
|
+
const pending = ensuring.get(key);
|
|
794
|
+
if (pending !== undefined) return pending;
|
|
795
|
+
|
|
796
|
+
const promise = (async () => {
|
|
797
|
+
let current = workspaceState(key, { alive: cheapAlive });
|
|
798
|
+
if (current.label === 'failed' && options.force === true) {
|
|
799
|
+
removeStamp(key, 'failed');
|
|
800
|
+
current = workspaceState(key, { alive: cheapAlive });
|
|
801
|
+
}
|
|
802
|
+
if (current.label === 'ready') return current;
|
|
803
|
+
if (current.label === 'failed') {
|
|
804
|
+
// EDT мог подняться позже таймаута health: проверяем и принимаем.
|
|
805
|
+
if (await probeHealth(current.port)) {
|
|
806
|
+
removeStamp(key, 'failed');
|
|
807
|
+
writeStamp(key, 'ready');
|
|
808
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
809
|
+
}
|
|
810
|
+
return current;
|
|
811
|
+
}
|
|
812
|
+
if (busyLaunching(current, plan.settings)) return current;
|
|
813
|
+
if (current.label === 'stale') removeStamp(key, 'ready');
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
let port = readPort(key);
|
|
817
|
+
if (port !== null && (await isPortOpen(port))) {
|
|
818
|
+
// Кто-то уже слушает порт: если это наш MCP-сервер (DSH перезапустили,
|
|
819
|
+
// маркер потерялся), принимаем его и не поднимаем второй EDT на тот же
|
|
820
|
+
// workspace.
|
|
821
|
+
if (await probeHealth(port)) {
|
|
822
|
+
removeStamp(key, 'failed');
|
|
823
|
+
writeStamp(key, 'ready');
|
|
824
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
825
|
+
}
|
|
826
|
+
port = null;
|
|
827
|
+
}
|
|
828
|
+
if (port === null) {
|
|
829
|
+
port = await pickFreePort(plan.settings.ports);
|
|
830
|
+
writePort(key, port);
|
|
831
|
+
}
|
|
832
|
+
const workspaceSnapshot = prepareWorkspaceSnapshot({
|
|
833
|
+
root: plan.root,
|
|
834
|
+
mainRoot: plan.mainRoot,
|
|
835
|
+
settings: plan.settings,
|
|
836
|
+
});
|
|
837
|
+
if (options.import === true) workspaceSnapshot.forceImport = true;
|
|
838
|
+
writeWorkspaceSnapshot(key, workspaceSnapshot);
|
|
839
|
+
record.startedByUs = true;
|
|
840
|
+
spawnInitializer(plan.root, 'init');
|
|
841
|
+
return workspaceState(key, { alive: cheapAlive });
|
|
842
|
+
} catch (error) {
|
|
843
|
+
ctx.logger?.warn?.(`mcp-hub: окружение EDT не поднялось: ${error instanceof Error ? error.message : String(error)}`);
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
})().finally(() => ensuring.delete(key));
|
|
847
|
+
|
|
848
|
+
ensuring.set(key, promise);
|
|
849
|
+
return promise;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
/** Снимает сессию с окружения; после последней — останавливает поднятое нами EDT. */
|
|
853
|
+
const releaseWorkspaceSession = (agent) => {
|
|
854
|
+
for (const [buildDir, record] of workspaceSessions) {
|
|
855
|
+
if (!record.agents.delete(agent)) continue;
|
|
856
|
+
if (record.agents.size > 0) continue;
|
|
857
|
+
workspaceSessions.delete(buildDir);
|
|
858
|
+
if (record.startedByUs !== true) continue;
|
|
859
|
+
if (record.settings?.stopOnSessionEnd === false) continue;
|
|
860
|
+
spawnInitializer(resolvePath(buildDir, '..'), 'stop');
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
572
864
|
const activate = async (agent) => {
|
|
573
865
|
const state = stateOf(agent);
|
|
574
866
|
if (state.active) return;
|
|
575
867
|
state.active = true;
|
|
576
|
-
|
|
868
|
+
const context = contextFor(agent);
|
|
869
|
+
for (const definition of [serversTool(), pinTool(), unpinTool(), reloadTool(), workspaceTool()]) {
|
|
577
870
|
state.disposers.push(agent.ctx.tools.register(definition));
|
|
578
871
|
}
|
|
579
|
-
watchFile(projectConfigPath(
|
|
872
|
+
for (const dir of context.paths) watchFile(projectConfigPath(dir));
|
|
580
873
|
|
|
581
|
-
|
|
582
|
-
|
|
874
|
+
// Окружение поднимается до монтирования edt: нужен порт для адреса.
|
|
875
|
+
if (context.workspace.state === 'ok') {
|
|
876
|
+
try {
|
|
877
|
+
await ensureWorkspace(agent, context);
|
|
878
|
+
} catch (error) {
|
|
879
|
+
ctx.logger?.warn?.(`mcp-hub: окружение EDT не поднялось: ${error instanceof Error ? error.message : String(error)}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
for (const entry of entriesFor(context)) {
|
|
583
884
|
if (entry.lazy || entry.unavailable) continue;
|
|
584
885
|
try {
|
|
585
886
|
await mount(agent, entry.name);
|
|
@@ -602,6 +903,7 @@ export async function apply(ctx, config) {
|
|
|
602
903
|
}
|
|
603
904
|
for (const item of state.items.values()) await disposeItem(item);
|
|
604
905
|
state.items.clear();
|
|
906
|
+
releaseWorkspaceSession(agent);
|
|
605
907
|
};
|
|
606
908
|
|
|
607
909
|
/**
|
|
@@ -610,13 +912,26 @@ export async function apply(ctx, config) {
|
|
|
610
912
|
*/
|
|
611
913
|
const reconcile = async (agent) => {
|
|
612
914
|
if (agent === undefined) return;
|
|
613
|
-
const
|
|
915
|
+
const context = contextFor(agent);
|
|
614
916
|
const state = agentStates.get(agent);
|
|
615
|
-
if (enabled) {
|
|
616
|
-
|
|
917
|
+
if (!context.activation.enabled) {
|
|
918
|
+
await deactivate(agent);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (state === undefined || !state.active) {
|
|
922
|
+
await activate(agent);
|
|
617
923
|
return;
|
|
618
924
|
}
|
|
619
|
-
|
|
925
|
+
|
|
926
|
+
if (context.workspace.state === 'ok') {
|
|
927
|
+
await ensureWorkspace(agent, context);
|
|
928
|
+
// Порт появился после init — адрес edt изменился, перемонтируем.
|
|
929
|
+
try {
|
|
930
|
+
await reload(agent, 'edt');
|
|
931
|
+
} catch (error) {
|
|
932
|
+
ctx.logger?.warn?.(`mcp-hub: не удалось обновить адрес EDT: ${error instanceof Error ? error.message : String(error)}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
620
935
|
};
|
|
621
936
|
|
|
622
937
|
ctx.on('agent/created', async ({ agent }) => {
|
|
@@ -636,6 +951,15 @@ export async function apply(ctx, config) {
|
|
|
636
951
|
return next();
|
|
637
952
|
});
|
|
638
953
|
|
|
954
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
955
|
+
releaseWorkspaceSession(agent);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
ctx.effect(() => () => {
|
|
959
|
+
ensuring.clear();
|
|
960
|
+
workspaceSessions.clear();
|
|
961
|
+
});
|
|
962
|
+
|
|
639
963
|
// --- вики: чтение разрешено, запись отклоняется ---
|
|
640
964
|
ctx.tools.guard((exec) => {
|
|
641
965
|
const target = targetPathOf(exec);
|
|
@@ -649,13 +973,16 @@ export async function apply(ctx, config) {
|
|
|
649
973
|
const serversTool = () =>
|
|
650
974
|
textTool(
|
|
651
975
|
'edt_servers',
|
|
652
|
-
'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.',
|
|
976
|
+
'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.',
|
|
653
977
|
{},
|
|
654
978
|
async (_args, exec) => {
|
|
655
|
-
const rows = snapshot(exec?.agent).map((item) => {
|
|
979
|
+
const rows = snapshot(exec?.agent, { alive: cheapAlive }).map((item) => {
|
|
980
|
+
const workspace = item.workspace;
|
|
656
981
|
const extra = [
|
|
657
982
|
item.root === undefined ? null : `root: ${item.root}`,
|
|
658
983
|
item.endpoint === undefined ? null : `endpoint: ${item.endpoint}`,
|
|
984
|
+
workspace?.state === 'worktree' ? `worktree: ${workspace.status}${workspace.port === undefined ? '' : ` port ${workspace.port}`}` : null,
|
|
985
|
+
workspace?.state === 'error' ? `worktree: ${workspace.message}` : null,
|
|
659
986
|
item.error === undefined ? null : `error: ${item.error}`,
|
|
660
987
|
]
|
|
661
988
|
.filter(Boolean)
|
|
@@ -714,6 +1041,121 @@ export async function apply(ctx, config) {
|
|
|
714
1041
|
},
|
|
715
1042
|
);
|
|
716
1043
|
|
|
1044
|
+
/** План окружения с необязательным переопределением корня (параметр root). */
|
|
1045
|
+
const workspacePlanFor = (exec, rootOverride) => {
|
|
1046
|
+
const context = contextFor(exec?.agent);
|
|
1047
|
+
if (rootOverride === undefined || rootOverride === '') return { context, plan: context.workspace };
|
|
1048
|
+
const settings = context.workspace?.settings ?? normalizeWorkspaceSettings(context.effective.config?.workspace, process.env);
|
|
1049
|
+
const root = isAbsolute(rootOverride) ? rootOverride : resolvePath(context.cwd, rootOverride);
|
|
1050
|
+
const buildDir = join(root, BUILD_DIR);
|
|
1051
|
+
return {
|
|
1052
|
+
context,
|
|
1053
|
+
plan: {
|
|
1054
|
+
state: 'ok',
|
|
1055
|
+
settings,
|
|
1056
|
+
root,
|
|
1057
|
+
buildDir,
|
|
1058
|
+
wsDir: join(buildDir, 'ws'),
|
|
1059
|
+
mainRoot: context.workspace?.mainRoot,
|
|
1060
|
+
warnings: context.workspace?.warnings ?? [],
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const workspaceText = (plan, deps = {}) => {
|
|
1066
|
+
const info = describeWorkspace(plan, deps);
|
|
1067
|
+
if (info.state === 'off') {
|
|
1068
|
+
const why =
|
|
1069
|
+
info.reason === 'disabled'
|
|
1070
|
+
? 'workspace.enabled=false'
|
|
1071
|
+
: 'основной checkout: окружение поднимается только в git worktree';
|
|
1072
|
+
return `Автоподъём EDT-окружения выключен — ${why}.`;
|
|
1073
|
+
}
|
|
1074
|
+
if (info.state === 'error') return `EDT-окружение недоступно: ${info.message}`;
|
|
1075
|
+
|
|
1076
|
+
const lines = [
|
|
1077
|
+
`Worktree: ${info.root}`,
|
|
1078
|
+
`Workspace EDT: ${info.wsDir}`,
|
|
1079
|
+
`Состояние: ${info.status}${info.port === undefined ? '' : `, порт ${info.port}`}${info.pid === undefined ? '' : `, pid ${info.pid}`}`,
|
|
1080
|
+
];
|
|
1081
|
+
if (info.phase !== undefined) {
|
|
1082
|
+
const elapsed = info.since === undefined ? '' : ` (${formatElapsed(info.since)})`;
|
|
1083
|
+
lines.push(`Фаза: ${info.phase}${elapsed}${info.detail === undefined ? '' : ` — ${info.detail}`}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (info.imported !== undefined) lines.push(`Импортировано проектов: ${info.imported}`);
|
|
1086
|
+
const home = detectEdtHome(info.settings, { platform: process.platform, env: process.env });
|
|
1087
|
+
lines.push(
|
|
1088
|
+
`EDT: ${home.home ?? 'не найден'}${home.error === undefined ? '' : ` — ${home.error}`}${home.mcpBundle === false ? ' (бандл EDT-MCP не найден в plugins/features)' : ''}`,
|
|
1089
|
+
);
|
|
1090
|
+
const importMode = info.settings.import;
|
|
1091
|
+
lines.push(`Импорт проектов: ${importMode === true ? 'да' : importMode === false ? 'нет' : 'авто (если workspace пуст)'}`);
|
|
1092
|
+
for (const warning of info.warnings) lines.push(`! ${warning}`);
|
|
1093
|
+
lines.push(`Логи: ${join(info.buildDir, 'logs')}`);
|
|
1094
|
+
return lines.join('\n');
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
const workspaceTool = () =>
|
|
1098
|
+
textTool(
|
|
1099
|
+
'edt_workspace',
|
|
1100
|
+
'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).',
|
|
1101
|
+
{
|
|
1102
|
+
action: {
|
|
1103
|
+
type: 'string',
|
|
1104
|
+
description: 'status | init | stop | restart | import',
|
|
1105
|
+
enum: ['status', 'init', 'stop', 'restart', 'import'],
|
|
1106
|
+
},
|
|
1107
|
+
root: { type: 'string', description: 'Worktree root to manage; defaults to the project worktree of this session.' },
|
|
1108
|
+
import: { type: 'boolean', description: 'For init/restart: import EDT projects even if the workspace already has some.' },
|
|
1109
|
+
},
|
|
1110
|
+
async (args, exec) => {
|
|
1111
|
+
const action = args.action ?? 'status';
|
|
1112
|
+
const { context, plan } = workspacePlanFor(exec, args.root);
|
|
1113
|
+
if (action === 'status') return { text: workspaceText(plan, { alive: (pid) => pidAlive(pid, ['1cedt']) }) };
|
|
1114
|
+
|
|
1115
|
+
if (plan === undefined || plan.state !== 'ok') {
|
|
1116
|
+
throw new Error(`EDT-окружение недоступно: ${plan?.message ?? workspaceText(plan)}`);
|
|
1117
|
+
}
|
|
1118
|
+
const record = registerWorkspaceSession(plan.buildDir, exec?.agent, plan.settings);
|
|
1119
|
+
|
|
1120
|
+
if (action === 'stop') {
|
|
1121
|
+
const result = await stopWorkspaceNow(plan.root);
|
|
1122
|
+
record.suppressed = true;
|
|
1123
|
+
return { text: result.stopped ? `EDT остановлен (pid ${result.pid}).` : `EDT не остановлен: ${result.reason}.` };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
record.suppressed = false;
|
|
1127
|
+
if (action === 'restart' || action === 'import') {
|
|
1128
|
+
if (action === 'import') {
|
|
1129
|
+
const current = workspaceState(plan.buildDir, { alive: cheapAlive });
|
|
1130
|
+
if (current.label === 'ready' || busyLaunching(current)) {
|
|
1131
|
+
throw new Error('EDT запущен: импорт возможен только при остановленном EDT. Используйте action=restart с import: true.');
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (action === 'restart') await stopWorkspaceNow(plan.root);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const forceImport = args.import === true || action === 'import';
|
|
1138
|
+
const result = await ensureWorkspace(exec?.agent, { ...context, workspace: plan }, { force: true, import: forceImport });
|
|
1139
|
+
if (result === undefined) throw new Error('Не удалось запустить подъём окружения: смотрите логи build/logs.');
|
|
1140
|
+
if (result.label === 'ready') {
|
|
1141
|
+
return { text: `EDT уже готов (порт ${result.port ?? '-'}).\n\n${workspaceText(plan)}` };
|
|
1142
|
+
}
|
|
1143
|
+
const headline =
|
|
1144
|
+
action === 'import'
|
|
1145
|
+
? 'Импорт проектов запущен в фоне.'
|
|
1146
|
+
: action === 'restart'
|
|
1147
|
+
? 'EDT перезапускается в фоне.'
|
|
1148
|
+
: 'Подъём EDT-окружения запущен в фоне.';
|
|
1149
|
+
return {
|
|
1150
|
+
text: [
|
|
1151
|
+
headline,
|
|
1152
|
+
'Первый запуск занимает 1–3 минуты (с импортом дольше). Проверяйте готовность через action=status.',
|
|
1153
|
+
workspaceText(plan),
|
|
1154
|
+
].join('\n'),
|
|
1155
|
+
};
|
|
1156
|
+
},
|
|
1157
|
+
);
|
|
1158
|
+
|
|
717
1159
|
const configTool = () =>
|
|
718
1160
|
textTool(
|
|
719
1161
|
'edt_config',
|
|
@@ -734,12 +1176,21 @@ export async function apply(ctx, config) {
|
|
|
734
1176
|
if (path === undefined) throw new Error('edt_config: cannot resolve the settings path');
|
|
735
1177
|
|
|
736
1178
|
if (args.action === 'show') {
|
|
737
|
-
const
|
|
1179
|
+
const context = contextFor(exec?.agent);
|
|
1180
|
+
const effective = context.effective;
|
|
738
1181
|
const { errors, warnings } = validateConfig(effective.config);
|
|
739
|
-
const activation =
|
|
1182
|
+
const activation = context.activation;
|
|
1183
|
+
const workspace = describeWorkspace(context.workspace, { alive: cheapAlive });
|
|
1184
|
+
const workspaceLine =
|
|
1185
|
+
workspace.state === 'worktree'
|
|
1186
|
+
? `EDT-окружение worktree: ${workspace.status}, порт ${workspace.port ?? '-'}, ${workspace.root}`
|
|
1187
|
+
: workspace.state === 'error'
|
|
1188
|
+
? `EDT-окружение worktree: ошибка — ${workspace.message}`
|
|
1189
|
+
: 'EDT-окружение worktree: выключено (поднимается только в git worktree)';
|
|
740
1190
|
const head = [
|
|
741
1191
|
`Проект: ${activation.enabled ? 'включён' : 'выключен'} — ${activationReason(activation)}`,
|
|
742
1192
|
`Источники: ${effective.sources.length > 0 ? effective.sources.join(', ') : 'только cordis.patch.yml'}`,
|
|
1193
|
+
workspaceLine,
|
|
743
1194
|
`Действующие настройки:\n${stringifyConfig(effective.config)}`,
|
|
744
1195
|
];
|
|
745
1196
|
if (errors.length > 0) head.push(`Ошибки:\n${errors.map((line) => ` ✗ ${line}`).join('\n')}`);
|
|
@@ -795,8 +1246,10 @@ export async function apply(ctx, config) {
|
|
|
795
1246
|
inner.skills.registerProvider((control) => {
|
|
796
1247
|
skillsControl = control;
|
|
797
1248
|
return createSkillsProvider({
|
|
798
|
-
|
|
799
|
-
|
|
1249
|
+
// Навыки гейтятся тем же контекстом проекта, что и инструменты: в
|
|
1250
|
+
// worktree включённость наследуется от основного checkout.
|
|
1251
|
+
resolveConfig: (cwd) => projectContextForCwd(cwd ?? process.cwd()).effective.config,
|
|
1252
|
+
isEnabled: (cwd) => projectContextForCwd(cwd ?? process.cwd()).activation.enabled,
|
|
800
1253
|
logger: ctx.logger,
|
|
801
1254
|
});
|
|
802
1255
|
});
|
|
@@ -814,10 +1267,17 @@ export async function apply(ctx, config) {
|
|
|
814
1267
|
const agent = context?.agent;
|
|
815
1268
|
// В выключенном проекте секция пустая: renderPrompt выбрасывает её,
|
|
816
1269
|
// поэтому в промпте не остаётся ни строчки про 1С.
|
|
817
|
-
if (agent === undefined
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
1270
|
+
if (agent === undefined) return '';
|
|
1271
|
+
const projectContext = contextFor(agent);
|
|
1272
|
+
if (!projectContext.activation.enabled) return '';
|
|
1273
|
+
const { roots } = normalizeWiki(projectContext.effective.config?.wiki, { cwd: projectContext.cwd });
|
|
1274
|
+
return buildRulesText(
|
|
1275
|
+
process.env,
|
|
1276
|
+
snapshot(agent, { alive: cheapAlive }),
|
|
1277
|
+
roots,
|
|
1278
|
+
projectContext.effective.config?.tests,
|
|
1279
|
+
describeWorkspace(projectContext.workspace, { alive: cheapAlive }),
|
|
1280
|
+
);
|
|
821
1281
|
},
|
|
822
1282
|
});
|
|
823
1283
|
});
|