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/repo.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
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 } 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
|
+
}
|
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,22 @@ 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
|
+
},
|
|
21
37
|
tests: {
|
|
22
38
|
yaxunit: { enabled: false, path: '', project: '' },
|
|
23
39
|
vanessa: { enabled: false, path: '' },
|
|
@@ -53,6 +69,23 @@ export const KNOWN = {
|
|
|
53
69
|
'servers.bsl.mode': { type: 'string', description: 'hybrid (LSP+MCP) или stdio.' },
|
|
54
70
|
'servers.bsl.mcpUrl': { type: 'string', description: 'Адрес уже запущенного BSL LS по HTTP.' },
|
|
55
71
|
'servers.bsl.download': { type: 'boolean', description: 'Разрешено ли скачивание jar.' },
|
|
72
|
+
'workspace': { type: 'object', description: 'Окружение EDT для git worktree: workspace в <worktree>/build/ws и свой MCP-порт.' },
|
|
73
|
+
'workspace.enabled': { type: 'boolean', description: 'Поднимать ли EDT-окружение в worktree автоматически.' },
|
|
74
|
+
'workspace.root': { type: 'string', description: 'Корень окружения относительно корня worktree; пусто — сам worktree.' },
|
|
75
|
+
'workspace.home': { type: 'object', description: 'Каталог установки 1C:EDT по платформам.' },
|
|
76
|
+
'workspace.home.win32': { type: 'string', description: 'Каталог 1C:EDT в Windows (где 1cedt.exe).' },
|
|
77
|
+
'workspace.home.linux': { type: 'string', description: 'Каталог 1C:EDT в Linux (где 1cedt).' },
|
|
78
|
+
'workspace.vm': { type: 'string', description: 'Java для EDT; пусто — автопоиск jdk-full-17 рядом с установкой.' },
|
|
79
|
+
'workspace.xmxIde': { type: 'string', description: 'Xmx для запуска EDT, например 6g.' },
|
|
80
|
+
'workspace.xmxImport': { type: 'string', description: 'Xmx для 1cedtcli при импорте проектов.' },
|
|
81
|
+
'workspace.import': { type: 'string', description: 'Импорт проектов в новый workspace: true, false или auto.' },
|
|
82
|
+
'workspace.importTimeout': { type: 'number', description: 'Таймаут импорта 1cedtcli, секунды.' },
|
|
83
|
+
'workspace.startTimeout': { type: 'number', description: 'Ожидание готовности MCP-сервера EDT, секунды.' },
|
|
84
|
+
'workspace.projects': { type: 'array', description: 'Шаблоны EDT-проектов для импорта; пусто — автопоиск каталогов с .project.' },
|
|
85
|
+
'workspace.ports': { type: 'object', description: 'Диапазон портов MCP-сервера EDT.' },
|
|
86
|
+
'workspace.ports.min': { type: 'number', description: 'Нижняя граница диапазона портов.' },
|
|
87
|
+
'workspace.ports.max': { type: 'number', description: 'Верхняя граница диапазона портов.' },
|
|
88
|
+
'workspace.stopOnSessionEnd': { type: 'boolean', description: 'Останавливать EDT, когда закрылась последняя сессия на этом worktree.' },
|
|
56
89
|
'tests.yaxunit.enabled': { type: 'boolean', description: 'Включить правила тестируемости YAxUnit.' },
|
|
57
90
|
'tests.yaxunit.path': { type: 'string', description: 'Каталог тестов как образец стиля.' },
|
|
58
91
|
'tests.yaxunit.project': { type: 'string', description: 'EDT-проект, в который писать тесты.' },
|
|
@@ -165,6 +198,8 @@ export function validateConfig(config) {
|
|
|
165
198
|
warnings.push(`неизвестный ключ: ${path}`);
|
|
166
199
|
return;
|
|
167
200
|
}
|
|
201
|
+
// У import три допустимых значения (true, false, "auto") — своя проверка ниже.
|
|
202
|
+
if (path === 'workspace.import') return;
|
|
168
203
|
const expected = KNOWN[path]?.type ?? Object.entries(KNOWN).find(([pattern]) => pattern.includes('*') && matchesPattern(path, pattern))?.[1]?.type;
|
|
169
204
|
if (expected === undefined) return;
|
|
170
205
|
const actual = Array.isArray(value) ? 'array' : typeof value;
|
|
@@ -182,6 +217,27 @@ export function validateConfig(config) {
|
|
|
182
217
|
errors.push('projects: ожидался массив строк');
|
|
183
218
|
}
|
|
184
219
|
|
|
220
|
+
if (isPlainObject(config.workspace)) {
|
|
221
|
+
const workspace = config.workspace;
|
|
222
|
+
if (workspace.import !== undefined && ![true, false, 'auto'].includes(workspace.import)) {
|
|
223
|
+
errors.push(`workspace.import: ожидалось true, false или "auto", получено ${JSON.stringify(workspace.import)}`);
|
|
224
|
+
}
|
|
225
|
+
if (workspace.projects !== undefined) {
|
|
226
|
+
if (!Array.isArray(workspace.projects)) errors.push('workspace.projects: ожидался массив шаблонов');
|
|
227
|
+
else if (!workspace.projects.every((item) => typeof item === 'string')) errors.push('workspace.projects: ожидался массив строк');
|
|
228
|
+
}
|
|
229
|
+
for (const key of ['importTimeout', 'startTimeout']) {
|
|
230
|
+
const value = workspace[key];
|
|
231
|
+
if (value !== undefined && (!Number.isFinite(value) || value <= 0)) errors.push(`workspace.${key}: ожидалось положительное число`);
|
|
232
|
+
}
|
|
233
|
+
if (isPlainObject(workspace.ports)) {
|
|
234
|
+
const { min, max } = workspace.ports;
|
|
235
|
+
if (min !== undefined && (!Number.isInteger(min) || min < 1 || min > 65535)) errors.push('workspace.ports.min: ожидался порт 1..65535');
|
|
236
|
+
if (max !== undefined && (!Number.isInteger(max) || max < 1 || max > 65535)) errors.push('workspace.ports.max: ожидался порт 1..65535');
|
|
237
|
+
if (Number.isInteger(min) && Number.isInteger(max) && min > max) errors.push('workspace.ports: min больше max');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
185
241
|
return { errors, warnings };
|
|
186
242
|
}
|
|
187
243
|
|