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/repo.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Git-репозиторий и worktree по файлам, без вызова git.
|
|
2
|
+
//
|
|
3
|
+
// Нужно это для двух вещей:
|
|
4
|
+
// - понять, что каталог сессии — linked worktree, и найти его основной checkout;
|
|
5
|
+
// - увидеть репозитории task space, когда каталог сессии сам репозиторием не
|
|
6
|
+
// является (layout `<task>/<repo>` у dsh-worktree-space).
|
|
7
|
+
//
|
|
8
|
+
// Git-команды здесь не используются намеренно: функция вызывается при рендере
|
|
9
|
+
// промпта (синхронно, на каждом шаге) и должна быть дешёвой и предсказуемой.
|
|
10
|
+
// Формат `.git`-файла и `commondir` — стабильный контракт git для linked worktree.
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { normalizeProjectPath, matchesProject } from './enable.mjs';
|
|
15
|
+
|
|
16
|
+
/** Сколько родителей просматривать в поисках `.git`. */
|
|
17
|
+
const MAX_UP = 8;
|
|
18
|
+
|
|
19
|
+
/** Каталоги task space, которые заведомо не репозитории. */
|
|
20
|
+
const SKIP_CHILDREN = new Set(['build', 'node_modules', 'archived-docs', '.git', '.worktree-space']);
|
|
21
|
+
|
|
22
|
+
/** realpath, но для отсутствующего пути — сам путь. */
|
|
23
|
+
function realOrSelf(value) {
|
|
24
|
+
try {
|
|
25
|
+
return realpathSync.native(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Файл `.git` со строкой `gitdir: <путь>` — так выглядит linked worktree. */
|
|
32
|
+
export function gitFileAt(root) {
|
|
33
|
+
const dotGit = path.join(root, '.git');
|
|
34
|
+
let stat;
|
|
35
|
+
try {
|
|
36
|
+
stat = statSync(dotGit);
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (!stat.isFile()) return undefined;
|
|
41
|
+
let text;
|
|
42
|
+
try {
|
|
43
|
+
text = readFileSync(dotGit, 'utf8');
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const match = /^gitdir:\s*(.+)$/m.exec(text);
|
|
48
|
+
if (!match) return undefined;
|
|
49
|
+
const raw = match[1].trim().replace(/^"(.*)"$/, '$1');
|
|
50
|
+
return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(root, raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Общий git-каталог для worktree: в `<main>/.git/worktrees/<name>` лежит
|
|
55
|
+
* файл `commondir` (обычно `../..`). Без него каталог считается общим.
|
|
56
|
+
*/
|
|
57
|
+
export function commonDirOf(gitDir) {
|
|
58
|
+
try {
|
|
59
|
+
const text = readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim();
|
|
60
|
+
if (text === '') return gitDir;
|
|
61
|
+
return path.resolve(gitDir, text);
|
|
62
|
+
} catch {
|
|
63
|
+
return gitDir;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Основной checkout по общему git-каталогу (`<main>/.git` -> `<main>`). */
|
|
68
|
+
export function mainRootOf(commonDir) {
|
|
69
|
+
return path.basename(commonDir).toLowerCase() === '.git' ? path.dirname(commonDir) : commonDir;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Совпадают ли пути (нормализация как в настройках проектов). */
|
|
73
|
+
export function isSamePath(left, right, options = {}) {
|
|
74
|
+
const a = normalizeProjectPath(left, options);
|
|
75
|
+
const b = normalizeProjectPath(right, options);
|
|
76
|
+
return a !== undefined && a === b;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ближайший git-репозиторий для каталога.
|
|
81
|
+
* @returns {{ root: string, gitDir: string, commonDir: string, mainRoot: string, isWorktree: boolean }|undefined}
|
|
82
|
+
*/
|
|
83
|
+
export function repositoryAt(startDir, { maxUp = MAX_UP, platform = process.platform } = {}) {
|
|
84
|
+
if (typeof startDir !== 'string' || startDir.trim() === '') return undefined;
|
|
85
|
+
let dir = path.resolve(startDir);
|
|
86
|
+
for (let depth = 0; depth <= maxUp; depth++) {
|
|
87
|
+
const gitDirFile = gitFileAt(dir);
|
|
88
|
+
if (gitDirFile !== undefined) {
|
|
89
|
+
const commonDir = realOrSelf(commonDirOf(gitDirFile));
|
|
90
|
+
const root = realOrSelf(dir);
|
|
91
|
+
// Submodule тоже живёт через `.git`-файл, но worktree-ом не является:
|
|
92
|
+
// его общий каталог лежит в `.git/modules/…` основного репозитория.
|
|
93
|
+
const isSubmodule = commonDir.split(/[\\/]+/).includes('modules');
|
|
94
|
+
if (isSubmodule) {
|
|
95
|
+
return { root, gitDir: gitDirFile, commonDir, mainRoot: root, isWorktree: false };
|
|
96
|
+
}
|
|
97
|
+
const mainRoot = realOrSelf(mainRootOf(commonDir));
|
|
98
|
+
return {
|
|
99
|
+
root,
|
|
100
|
+
gitDir: gitDirFile,
|
|
101
|
+
commonDir,
|
|
102
|
+
mainRoot,
|
|
103
|
+
isWorktree: !isSamePath(root, mainRoot, { platform }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (existsSync(path.join(dir, '.git'))) {
|
|
107
|
+
const root = realOrSelf(dir);
|
|
108
|
+
const gitDir = realOrSelf(path.join(dir, '.git'));
|
|
109
|
+
return { root, gitDir, commonDir: gitDir, mainRoot: root, isWorktree: false };
|
|
110
|
+
}
|
|
111
|
+
const parent = path.dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Репозитории каталога сессии: сам репозиторий либо репозитории первого уровня
|
|
120
|
+
* (task space). Сортировка детерминированная — от неё зависит выбор окружения.
|
|
121
|
+
*/
|
|
122
|
+
export function repositoriesUnder(cwd, { platform = process.platform, maxChildren = 64 } = {}) {
|
|
123
|
+
const direct = repositoryAt(cwd, { platform });
|
|
124
|
+
if (direct !== undefined) return [direct];
|
|
125
|
+
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(cwd, { withFileTypes: true });
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const found = [];
|
|
134
|
+
for (const entry of entries.slice(0, maxChildren)) {
|
|
135
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || SKIP_CHILDREN.has(entry.name)) continue;
|
|
136
|
+
const repo = repositoryAt(path.join(cwd, entry.name), { maxUp: 0, platform });
|
|
137
|
+
if (repo !== undefined) found.push(repo);
|
|
138
|
+
}
|
|
139
|
+
return found.sort((left, right) => left.root.localeCompare(right.root, undefined, { sensitivity: 'base' }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Уникальные репозитории по нормализованному корню (для `stop-all` и тестов). */
|
|
143
|
+
export function uniqueRepositories(repositories, { platform = process.platform } = {}) {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const repo of repositories ?? []) {
|
|
147
|
+
const key = normalizeProjectPath(repo.root, { platform });
|
|
148
|
+
if (key === undefined || seen.has(key)) continue;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
out.push(repo);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Каталог, от которого плагин считает проект.
|
|
157
|
+
*
|
|
158
|
+
* `origin` — неизменяемый `session.header.cwd`, `work` — эффективный каталог
|
|
159
|
+
* сессии (`ctx.workingDirectory`). Переключаемся на work только если это
|
|
160
|
+
* linked worktree, связанный с исходным репозиторием: `cd` в подкаталог или в
|
|
161
|
+
* чужой репозиторий проект не переключает, а worktree того же репозитория —
|
|
162
|
+
* переключает. Возвращается корень worktree, а не подкаталог, чтобы настройки
|
|
163
|
+
* и окружение не зависели от `cd` внутри него.
|
|
164
|
+
* @returns {string} абсолютный каталог проекта.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveContextCwd({ origin, work, platform = process.platform } = {}) {
|
|
167
|
+
if (typeof origin !== 'string' || origin === '') {
|
|
168
|
+
return typeof work === 'string' && work !== '' ? work : process.cwd();
|
|
169
|
+
}
|
|
170
|
+
if (typeof work !== 'string' || work === '' || isSamePath(work, origin, { platform })) return origin;
|
|
171
|
+
|
|
172
|
+
const workRepo = repositoryAt(work, { platform });
|
|
173
|
+
if (workRepo === undefined || workRepo.isWorktree !== true) return origin;
|
|
174
|
+
|
|
175
|
+
const originRepo = repositoryAt(origin, { platform });
|
|
176
|
+
if (originRepo === undefined) return origin;
|
|
177
|
+
|
|
178
|
+
const related =
|
|
179
|
+
isSamePath(workRepo.mainRoot, originRepo.root, { platform }) ||
|
|
180
|
+
isSamePath(workRepo.mainRoot, origin, { platform }) ||
|
|
181
|
+
matchesProject(originRepo.root, workRepo.mainRoot, { platform }) ||
|
|
182
|
+
matchesProject(origin, workRepo.mainRoot, { platform });
|
|
183
|
+
|
|
184
|
+
return related ? workRepo.root : origin;
|
|
185
|
+
}
|
package/lib/schema.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Правила апгрейда: существующие значения не меняются, недостающие ключи
|
|
5
5
|
// добавляются, неизвестные ключи сохраняются, версия поднимается.
|
|
6
|
-
export const CONFIG_VERSION =
|
|
6
|
+
export const CONFIG_VERSION = 3;
|
|
7
7
|
|
|
8
8
|
/** Значения по умолчанию для нового файла. Пустые строки и массивы
|
|
9
9
|
* нормализаторы игнорируют, поэтому init ничего не включает сам по себе.
|
|
@@ -18,6 +18,26 @@ export const DEFAULTS = {
|
|
|
18
18
|
bsl: { root: '' },
|
|
19
19
|
tasks: { url: '' },
|
|
20
20
|
},
|
|
21
|
+
// Окружение EDT для git worktree: workspace в <worktree>/build/ws и свой
|
|
22
|
+
// MCP-сервер на своём порту. Поднимается только в linked worktree.
|
|
23
|
+
workspace: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
root: '',
|
|
26
|
+
home: { win32: '', linux: '' },
|
|
27
|
+
vm: '',
|
|
28
|
+
xmxIde: '6g',
|
|
29
|
+
xmxImport: '8g',
|
|
30
|
+
import: 'auto',
|
|
31
|
+
importTimeout: 3600,
|
|
32
|
+
startTimeout: 1800,
|
|
33
|
+
projects: [],
|
|
34
|
+
ports: { min: 20000, max: 39999 },
|
|
35
|
+
stopOnSessionEnd: true,
|
|
36
|
+
// MCP-инструменты EDT-MCP, которые нам нужны в worktree: plain text
|
|
37
|
+
// вместо embedded resource (DSH их не рендерит) и скриншоты форм.
|
|
38
|
+
plainText: true,
|
|
39
|
+
formScreenshots: true,
|
|
40
|
+
},
|
|
21
41
|
tests: {
|
|
22
42
|
yaxunit: { enabled: false, path: '', project: '' },
|
|
23
43
|
vanessa: { enabled: false, path: '' },
|
|
@@ -37,12 +57,24 @@ export const KNOWN = {
|
|
|
37
57
|
'servers.*.url': { type: 'string', description: 'Адрес MCP-сервера (streamable-http).' },
|
|
38
58
|
'servers.*.headers': { type: 'object', description: 'Заголовки запросов, например Authorization.' },
|
|
39
59
|
'servers.*.root': { type: 'string', description: 'Корень рабочего пространства (для bsl).' },
|
|
60
|
+
'servers.*.transport': { type: 'string', description: 'Транспорт: stdio или streamable-http.' },
|
|
61
|
+
'servers.*.disabled': { type: 'boolean', description: 'Не монтировать сервер.' },
|
|
62
|
+
'servers.*.unavailable': { type: 'boolean', description: 'Сервер описан, но не настроен (нет переменных окружения).' },
|
|
63
|
+
'servers.*.command': { type: 'string', description: 'Команда запуска для stdio-сервера.' },
|
|
64
|
+
'servers.*.args': { type: 'array', description: 'Аргументы команды stdio-сервера.' },
|
|
65
|
+
'servers.*.env': { type: 'object', description: 'Переменные окружения stdio-сервера.' },
|
|
66
|
+
'servers.*.cwd': { type: 'string', description: 'Рабочий каталог stdio-сервера.' },
|
|
67
|
+
'servers.*.maxInstructionBytes': { type: 'number', description: 'Предел текста инструкций сервера.' },
|
|
40
68
|
'servers.*.lazy': { type: 'boolean', description: 'Не подключать на старте, только по edt_pin.' },
|
|
41
69
|
'servers.*.shared': { type: 'boolean', description: 'Устарело: серверы всегда монтируются в скоупе сессии.' },
|
|
42
70
|
'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на edt_pin.' },
|
|
43
71
|
'servers.*.toolCallTimeoutMs': { type: 'number', description: 'Таймаут вызова инструмента.' },
|
|
44
72
|
'servers.*.failOnStartupError': { type: 'boolean', description: 'Падать, если стартовое подключение не удалось.' },
|
|
45
73
|
'servers.*.reconnect': { type: 'object', description: 'Политика переподключения.' },
|
|
74
|
+
'servers.*.reconnect.enabled': { type: 'boolean', description: 'Включено ли переподключение.' },
|
|
75
|
+
'servers.*.reconnect.initialDelayMs': { type: 'number', description: 'Начальная задержка переподключения, мс.' },
|
|
76
|
+
'servers.*.reconnect.maxDelayMs': { type: 'number', description: 'Максимальная задержка переподключения, мс.' },
|
|
77
|
+
'servers.*.reconnect.maxAttempts': { type: 'number', description: 'Максимум попыток переподключения.' },
|
|
46
78
|
'servers.bsl.jar': { type: 'string', description: 'Готовый jar BSL Language Server.' },
|
|
47
79
|
'servers.bsl.url': { type: 'string', description: 'Прямой URL на jar вместо GitHub.' },
|
|
48
80
|
'servers.bsl.version': { type: 'string', description: 'Тег релиза BSL LS, например v1.0.7.' },
|
|
@@ -53,6 +85,25 @@ export const KNOWN = {
|
|
|
53
85
|
'servers.bsl.mode': { type: 'string', description: 'hybrid (LSP+MCP) или stdio.' },
|
|
54
86
|
'servers.bsl.mcpUrl': { type: 'string', description: 'Адрес уже запущенного BSL LS по HTTP.' },
|
|
55
87
|
'servers.bsl.download': { type: 'boolean', description: 'Разрешено ли скачивание jar.' },
|
|
88
|
+
'workspace': { type: 'object', description: 'Окружение EDT для git worktree: workspace в <worktree>/build/ws и свой MCP-порт.' },
|
|
89
|
+
'workspace.enabled': { type: 'boolean', description: 'Поднимать ли EDT-окружение в worktree автоматически.' },
|
|
90
|
+
'workspace.root': { type: 'string', description: 'Корень окружения относительно корня worktree; пусто — сам worktree.' },
|
|
91
|
+
'workspace.home': { type: 'object', description: 'Каталог установки 1C:EDT по платформам.' },
|
|
92
|
+
'workspace.home.win32': { type: 'string', description: 'Каталог 1C:EDT в Windows (где 1cedt.exe).' },
|
|
93
|
+
'workspace.home.linux': { type: 'string', description: 'Каталог 1C:EDT в Linux (где 1cedt).' },
|
|
94
|
+
'workspace.vm': { type: 'string', description: 'Java для EDT; пусто — автопоиск jdk-full-17 рядом с установкой.' },
|
|
95
|
+
'workspace.xmxIde': { type: 'string', description: 'Xmx для запуска EDT, например 6g.' },
|
|
96
|
+
'workspace.xmxImport': { type: 'string', description: 'Xmx для 1cedtcli при импорте проектов.' },
|
|
97
|
+
'workspace.import': { type: 'string', description: 'Импорт проектов в новый workspace: true, false или auto.' },
|
|
98
|
+
'workspace.importTimeout': { type: 'number', description: 'Таймаут импорта 1cedtcli, секунды.' },
|
|
99
|
+
'workspace.startTimeout': { type: 'number', description: 'Ожидание готовности MCP-сервера EDT, секунды.' },
|
|
100
|
+
'workspace.projects': { type: 'array', description: 'Шаблоны EDT-проектов для импорта; пусто — автопоиск каталогов с .project.' },
|
|
101
|
+
'workspace.ports': { type: 'object', description: 'Диапазон портов MCP-сервера EDT.' },
|
|
102
|
+
'workspace.ports.min': { type: 'number', description: 'Нижняя граница диапазона портов.' },
|
|
103
|
+
'workspace.ports.max': { type: 'number', description: 'Верхняя граница диапазона портов.' },
|
|
104
|
+
'workspace.stopOnSessionEnd': { type: 'boolean', description: 'Останавливать EDT, когда закрылась последняя сессия на этом worktree.' },
|
|
105
|
+
'workspace.plainText': { type: 'boolean', description: 'Plain text mode EDT-MCP вместо embedded resource (DSH их не рендерит).' },
|
|
106
|
+
'workspace.formScreenshots': { type: 'boolean', description: 'Включить get_form_screenshot и JVM-флаг nativeFormBufferedLayoutRender.' },
|
|
56
107
|
'tests.yaxunit.enabled': { type: 'boolean', description: 'Включить правила тестируемости YAxUnit.' },
|
|
57
108
|
'tests.yaxunit.path': { type: 'string', description: 'Каталог тестов как образец стиля.' },
|
|
58
109
|
'tests.yaxunit.project': { type: 'string', description: 'EDT-проект, в который писать тесты.' },
|
|
@@ -165,6 +216,8 @@ export function validateConfig(config) {
|
|
|
165
216
|
warnings.push(`неизвестный ключ: ${path}`);
|
|
166
217
|
return;
|
|
167
218
|
}
|
|
219
|
+
// У import три допустимых значения (true, false, "auto") — своя проверка ниже.
|
|
220
|
+
if (path === 'workspace.import') return;
|
|
168
221
|
const expected = KNOWN[path]?.type ?? Object.entries(KNOWN).find(([pattern]) => pattern.includes('*') && matchesPattern(path, pattern))?.[1]?.type;
|
|
169
222
|
if (expected === undefined) return;
|
|
170
223
|
const actual = Array.isArray(value) ? 'array' : typeof value;
|
|
@@ -182,6 +235,36 @@ export function validateConfig(config) {
|
|
|
182
235
|
errors.push('projects: ожидался массив строк');
|
|
183
236
|
}
|
|
184
237
|
|
|
238
|
+
if (isPlainObject(config.servers)) {
|
|
239
|
+
for (const [name, server] of Object.entries(config.servers)) {
|
|
240
|
+
if (!isPlainObject(server)) continue;
|
|
241
|
+
if (Array.isArray(server.args) && !server.args.every((item) => typeof item === 'string')) {
|
|
242
|
+
errors.push(`servers.${name}.args: ожидался массив строк`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isPlainObject(config.workspace)) {
|
|
248
|
+
const workspace = config.workspace;
|
|
249
|
+
if (workspace.import !== undefined && ![true, false, 'auto'].includes(workspace.import)) {
|
|
250
|
+
errors.push(`workspace.import: ожидалось true, false или "auto", получено ${JSON.stringify(workspace.import)}`);
|
|
251
|
+
}
|
|
252
|
+
if (workspace.projects !== undefined) {
|
|
253
|
+
if (!Array.isArray(workspace.projects)) errors.push('workspace.projects: ожидался массив шаблонов');
|
|
254
|
+
else if (!workspace.projects.every((item) => typeof item === 'string')) errors.push('workspace.projects: ожидался массив строк');
|
|
255
|
+
}
|
|
256
|
+
for (const key of ['importTimeout', 'startTimeout']) {
|
|
257
|
+
const value = workspace[key];
|
|
258
|
+
if (value !== undefined && (!Number.isFinite(value) || value <= 0)) errors.push(`workspace.${key}: ожидалось положительное число`);
|
|
259
|
+
}
|
|
260
|
+
if (isPlainObject(workspace.ports)) {
|
|
261
|
+
const { min, max } = workspace.ports;
|
|
262
|
+
if (min !== undefined && (!Number.isInteger(min) || min < 1 || min > 65535)) errors.push('workspace.ports.min: ожидался порт 1..65535');
|
|
263
|
+
if (max !== undefined && (!Number.isInteger(max) || max < 1 || max > 65535)) errors.push('workspace.ports.max: ожидался порт 1..65535');
|
|
264
|
+
if (Number.isInteger(min) && Number.isInteger(max) && min > max) errors.push('workspace.ports: min больше max');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
185
268
|
return { errors, warnings };
|
|
186
269
|
}
|
|
187
270
|
|