@gian-tiaga/eda 0.8.6 → 1.0.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/MIGRATION.md +119 -0
- package/README.md +324 -71
- package/agents/eda-aim-executor/agent.json +15 -0
- package/agents/eda-aim-executor/prompt.md +61 -0
- package/agents/eda-aim-planner/agent.json +15 -0
- package/agents/eda-aim-planner/prompt.md +43 -0
- package/agents/eda-aim-verifier/agent.json +15 -0
- package/agents/eda-aim-verifier/prompt.md +57 -0
- package/agents/eda-commit-context/agent.json +15 -0
- package/agents/eda-commit-context/prompt.md +58 -0
- package/agents/eda-commit-executor/agent.json +15 -0
- package/agents/eda-commit-executor/prompt.md +87 -0
- package/agents/eda-merge-worktree-executor/agent.json +15 -0
- package/agents/eda-merge-worktree-executor/prompt.md +57 -0
- package/agents/eda-review-api/agent.json +15 -0
- package/agents/eda-review-api/prompt.md +34 -0
- package/agents/eda-review-architecture/agent.json +15 -0
- package/agents/eda-review-architecture/prompt.md +34 -0
- package/agents/eda-review-code-quality/agent.json +15 -0
- package/agents/eda-review-code-quality/prompt.md +34 -0
- package/agents/eda-review-correctness/agent.json +15 -0
- package/agents/eda-review-correctness/prompt.md +34 -0
- package/agents/eda-review-database/agent.json +15 -0
- package/agents/eda-review-database/prompt.md +34 -0
- package/agents/eda-review-documentation/agent.json +15 -0
- package/agents/eda-review-documentation/prompt.md +34 -0
- package/agents/eda-review-frontend/agent.json +15 -0
- package/agents/eda-review-frontend/prompt.md +34 -0
- package/agents/eda-review-performance/agent.json +15 -0
- package/agents/eda-review-performance/prompt.md +34 -0
- package/agents/eda-review-plan-alignment/agent.json +15 -0
- package/agents/eda-review-plan-alignment/prompt.md +34 -0
- package/agents/eda-review-previous-reviews/agent.json +15 -0
- package/agents/eda-review-previous-reviews/prompt.md +44 -0
- package/agents/eda-review-references/agent.json +15 -0
- package/agents/eda-review-references/prompt.md +34 -0
- package/agents/eda-review-rules/agent.json +15 -0
- package/agents/eda-review-rules/prompt.md +34 -0
- package/agents/eda-review-security/agent.json +15 -0
- package/agents/eda-review-security/prompt.md +34 -0
- package/agents/eda-review-tests/agent.json +15 -0
- package/agents/eda-review-tests/prompt.md +34 -0
- package/agents/eda-worktree-executor/agent.json +15 -0
- package/agents/eda-worktree-executor/prompt.md +49 -0
- package/bin/cli.js +4 -4
- package/lib/catalog.js +118 -0
- package/lib/install.js +921 -119
- package/lib/renderers/claude-agent.js +24 -0
- package/lib/renderers/claude-skill.js +11 -0
- package/lib/renderers/codex-agent.js +24 -0
- package/lib/renderers/codex-skill.js +3 -0
- package/package.json +5 -3
- package/skills/eda-aim/SKILL.md +212 -0
- package/skills/eda-commit/SKILL.md +124 -0
- package/skills/eda-commit/skill.json +6 -0
- package/skills/eda-discover-automations/SKILL.md +184 -0
- package/skills/{eda-explore.md → eda-explore/SKILL.md} +11 -8
- package/skills/{eda-fix.md → eda-fix/SKILL.md} +7 -6
- package/skills/{eda-fix-by-review.md → eda-fix-by-review/SKILL.md} +5 -4
- package/skills/{eda-manual-test.md → eda-manual-test/SKILL.md} +1 -1
- package/skills/eda-merge-worktree/SKILL.md +72 -0
- package/skills/eda-merge-worktree/skill.json +7 -0
- package/skills/eda-new-project/SKILL.md +163 -0
- package/skills/eda-orhestra/SKILL.md +146 -0
- package/skills/{eda-plan.md → eda-plan/SKILL.md} +39 -29
- package/skills/eda-plan-execute/SKILL.md +186 -0
- package/skills/{eda-plan-polish.md → eda-plan-polish/SKILL.md} +14 -13
- package/skills/eda-polish/SKILL.md +80 -0
- package/skills/eda-prepare-ai/SKILL.md +227 -0
- package/skills/eda-review/SKILL.md +212 -0
- package/skills/{eda-send-review.md → eda-send-review/SKILL.md} +66 -19
- package/skills/eda-send-review/skill.json +7 -0
- package/skills/eda-worktree/SKILL.md +73 -0
- package/skills/eda-worktree/skill.json +7 -0
- package/skills/eda-automate.md +0 -185
- package/skills/eda-commit.md +0 -91
- package/skills/eda-docs.md +0 -123
- package/skills/eda-execute.md +0 -140
- package/skills/eda-merge-worktree.md +0 -116
- package/skills/eda-polish.md +0 -113
- package/skills/eda-review-check.md +0 -149
- package/skills/eda-review.md +0 -173
- package/skills/eda-start.md +0 -233
- package/skills/eda-worktree.md +0 -113
- /package/skills/{eda-roadmap.md → eda-roadmap/SKILL.md} +0 -0
package/lib/install.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import checkbox from '@inquirer/checkbox';
|
|
2
2
|
import select from '@inquirer/select';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
|
|
8
|
+
import { listAgents, listSkills } from './catalog.js';
|
|
9
|
+
import { renderClaudeAgent } from './renderers/claude-agent.js';
|
|
10
|
+
import { renderClaudeSkill } from './renderers/claude-skill.js';
|
|
11
|
+
import { renderCodexAgent } from './renderers/codex-agent.js';
|
|
12
|
+
import { renderCodexSkill } from './renderers/codex-skill.js';
|
|
13
|
+
|
|
7
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
15
|
const __dirname = path.dirname(__filename);
|
|
9
16
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
10
17
|
const SKILLS_SRC = path.join(PACKAGE_ROOT, 'skills');
|
|
18
|
+
const AGENTS_SRC = path.join(PACKAGE_ROOT, 'agents');
|
|
19
|
+
const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json');
|
|
11
20
|
const SETTINGS_RELATIVE_PATH = 'docs/settings.yaml';
|
|
12
21
|
const TARGET_CHOICES = [
|
|
13
22
|
{ value: 'claude', label: 'Claude Code', dir: '.claude/skills/' },
|
|
@@ -15,33 +24,260 @@ const TARGET_CHOICES = [
|
|
|
15
24
|
];
|
|
16
25
|
const UPDATE_ALL_MAX_DEPTH = 2;
|
|
17
26
|
const UPDATE_ALL_SKIP_DIRS = new Set(['.git', '.claude', '.codex', 'node_modules']);
|
|
18
|
-
const RETIRED_SKILLS = [
|
|
27
|
+
const RETIRED_SKILLS = [
|
|
28
|
+
'eda-research',
|
|
29
|
+
'eda-review-check',
|
|
30
|
+
'eda-execute',
|
|
31
|
+
'eda-automate',
|
|
32
|
+
'eda-docs',
|
|
33
|
+
'eda-start'
|
|
34
|
+
];
|
|
35
|
+
const RETIRED_AGENTS = [];
|
|
36
|
+
const MANIFEST_FILE = 'eda-manifest.json';
|
|
37
|
+
const REVIEW_AGENT_DEFINITIONS = [
|
|
38
|
+
{
|
|
39
|
+
key: 'correctness',
|
|
40
|
+
description: 'Проверяет ошибки в логике, крайние случаи и обработку ошибок.',
|
|
41
|
+
auto: 'Эта базовая проверка по умолчанию запускается всегда.',
|
|
42
|
+
mode: 'always',
|
|
43
|
+
claude: 'sonnet',
|
|
44
|
+
codex: 'gpt-5.6-terra'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'architecture',
|
|
48
|
+
description: 'Проверяет архитектурные границы, зависимости и разделение ответственности.',
|
|
49
|
+
auto: 'В auto запускается при изменении модулей, зависимостей, публичных контрактов или границ системы.',
|
|
50
|
+
mode: 'auto',
|
|
51
|
+
claude: 'opus',
|
|
52
|
+
codex: 'gpt-5.6-sol'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'rules',
|
|
56
|
+
description: 'Проверяет соблюдение AGENTS.md, CLAUDE.md и docs/rules.md.',
|
|
57
|
+
auto: 'Эта базовая проверка по умолчанию запускается всегда.',
|
|
58
|
+
mode: 'always',
|
|
59
|
+
claude: 'haiku',
|
|
60
|
+
codex: 'gpt-5.6-luna'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: 'references',
|
|
64
|
+
description: 'Проверяет соответствие применимым карточкам из docs/references/.',
|
|
65
|
+
auto: 'В auto запускается только когда для изменений выбраны применимые карточки.',
|
|
66
|
+
mode: 'auto',
|
|
67
|
+
claude: 'haiku',
|
|
68
|
+
codex: 'gpt-5.6-luna'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'plan_alignment',
|
|
72
|
+
description: 'Проверяет, что реализация соответствует указанному или однозначно найденному плану.',
|
|
73
|
+
auto: 'В auto запускается только когда у ревью есть конкретный план.',
|
|
74
|
+
mode: 'auto',
|
|
75
|
+
claude: 'sonnet',
|
|
76
|
+
codex: 'gpt-5.6-terra'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'code_quality',
|
|
80
|
+
description: 'Проверяет читаемость, сложность, дублирование и поддерживаемость кода.',
|
|
81
|
+
auto: 'Эта базовая проверка по умолчанию запускается всегда.',
|
|
82
|
+
mode: 'always',
|
|
83
|
+
claude: 'sonnet',
|
|
84
|
+
codex: 'gpt-5.6-terra'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'tests',
|
|
88
|
+
description: 'Проверяет достаточность, корректность и устойчивость тестов.',
|
|
89
|
+
auto: 'Эта базовая проверка по умолчанию запускается всегда.',
|
|
90
|
+
mode: 'always',
|
|
91
|
+
claude: 'sonnet',
|
|
92
|
+
codex: 'gpt-5.6-terra'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'security',
|
|
96
|
+
description: 'Проверяет уязвимости, права доступа, валидацию и работу с чувствительными данными.',
|
|
97
|
+
auto: 'В auto запускается для auth, permissions, внешнего ввода, файлов, сети, секретов и криптографии.',
|
|
98
|
+
mode: 'auto',
|
|
99
|
+
claude: 'opus',
|
|
100
|
+
codex: 'gpt-5.6-sol'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: 'performance',
|
|
104
|
+
description: 'Проверяет запросы, циклы, память, конкурентность и другие риски производительности.',
|
|
105
|
+
auto: 'В auto запускается при изменении запросов, пакетной обработки, кешей, конкурентности или горячих путей.',
|
|
106
|
+
mode: 'auto',
|
|
107
|
+
claude: 'sonnet',
|
|
108
|
+
codex: 'gpt-5.6-terra'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: 'frontend',
|
|
112
|
+
description: 'Проверяет фронтенд-код, UI, UX, адаптивность и состояния интерфейса.',
|
|
113
|
+
auto: 'В auto обязательно запускается при изменении компонентов, стилей, клиентских маршрутов или состояния UI.',
|
|
114
|
+
mode: 'auto',
|
|
115
|
+
claude: 'sonnet',
|
|
116
|
+
codex: 'gpt-5.6-terra'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: 'api',
|
|
120
|
+
description: 'Проверяет API-контракты, совместимость, валидацию и обработку ошибок.',
|
|
121
|
+
auto: 'В auto запускается при изменении маршрутов, handlers, DTO, схем API, GraphQL или RPC.',
|
|
122
|
+
mode: 'auto',
|
|
123
|
+
claude: 'sonnet',
|
|
124
|
+
codex: 'gpt-5.6-terra'
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'database',
|
|
128
|
+
description: 'Проверяет схемы, миграции, запросы, индексы и целостность данных.',
|
|
129
|
+
auto: 'В auto запускается при изменении миграций, моделей хранения, SQL, схем или транзакций.',
|
|
130
|
+
mode: 'auto',
|
|
131
|
+
claude: 'opus',
|
|
132
|
+
codex: 'gpt-5.6-sol'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: 'documentation',
|
|
136
|
+
description: 'Проверяет, обновлена ли документация для изменившегося пользовательского поведения.',
|
|
137
|
+
auto: 'В auto запускается при изменении публичного API, CLI, настроек или документируемого поведения.',
|
|
138
|
+
mode: 'auto',
|
|
139
|
+
claude: 'haiku',
|
|
140
|
+
codex: 'gpt-5.6-luna'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'previous_reviews',
|
|
144
|
+
description: 'Перепроверяет незакрытые замечания предыдущих ревью для удалённого PR или MR.',
|
|
145
|
+
auto: 'В auto запускается только для ссылки на PR/MR или другой цели с доступными удалёнными обсуждениями.',
|
|
146
|
+
mode: 'auto',
|
|
147
|
+
claude: 'haiku',
|
|
148
|
+
codex: 'gpt-5.6-luna'
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
const DEFAULT_REVIEW_AGENTS = Object.fromEntries(REVIEW_AGENT_DEFINITIONS.map(agent => [
|
|
152
|
+
agent.key,
|
|
153
|
+
{
|
|
154
|
+
mode: agent.mode,
|
|
155
|
+
model: {
|
|
156
|
+
claude: agent.claude,
|
|
157
|
+
codex: agent.codex
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]));
|
|
161
|
+
const ORHESTRA_STEP_DEFINITIONS = [
|
|
162
|
+
{
|
|
163
|
+
id: 'plan',
|
|
164
|
+
skill: 'eda-plan',
|
|
165
|
+
name: 'Планирование через eda-plan',
|
|
166
|
+
enabled: true,
|
|
167
|
+
args: ''
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'execute',
|
|
171
|
+
skill: 'eda-plan-execute',
|
|
172
|
+
name: 'Управляемое выполнение плана через eda-plan-execute',
|
|
173
|
+
enabled: true,
|
|
174
|
+
args: ''
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'polish',
|
|
178
|
+
skill: 'eda-polish',
|
|
179
|
+
name: 'Ревью и исправления через eda-polish',
|
|
180
|
+
enabled: true,
|
|
181
|
+
args: 'limit 5'
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'manual-test',
|
|
185
|
+
skill: 'eda-manual-test',
|
|
186
|
+
name: 'Ручная проверка через eda-manual-test',
|
|
187
|
+
enabled: true,
|
|
188
|
+
args: '',
|
|
189
|
+
onFailure: {
|
|
190
|
+
skill: 'eda-fix',
|
|
191
|
+
args: '',
|
|
192
|
+
then: ['polish', 'manual-test'],
|
|
193
|
+
maxCycles: 5
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
const DEFAULT_ORHESTRA_STEPS = ORHESTRA_STEP_DEFINITIONS.map(({ name, ...step }) => step);
|
|
19
198
|
const DEFAULT_SETTINGS = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
199
|
+
orhestra: {
|
|
200
|
+
mode: 'automatic',
|
|
201
|
+
steps: DEFAULT_ORHESTRA_STEPS
|
|
202
|
+
},
|
|
203
|
+
aim: {
|
|
204
|
+
mode: 'automatic'
|
|
205
|
+
},
|
|
206
|
+
explore: {
|
|
207
|
+
strict: false,
|
|
208
|
+
decisionMode: 'recommend_and_ask'
|
|
209
|
+
},
|
|
210
|
+
plan: {
|
|
211
|
+
strict: false,
|
|
212
|
+
size: 'normal',
|
|
213
|
+
decisionMode: 'recommend_and_ask',
|
|
214
|
+
testStrategy: 'ask_each_time',
|
|
215
|
+
loggingStrategy: 'ask_each_time'
|
|
216
|
+
},
|
|
217
|
+
planPolish: {
|
|
218
|
+
strict: false
|
|
219
|
+
},
|
|
220
|
+
review: {
|
|
221
|
+
agents: DEFAULT_REVIEW_AGENTS
|
|
222
|
+
},
|
|
223
|
+
sendReview: {
|
|
224
|
+
closePreviousReviews: false
|
|
225
|
+
},
|
|
226
|
+
discoverAutomations: {
|
|
227
|
+
includePlans: false
|
|
228
|
+
}
|
|
27
229
|
};
|
|
28
230
|
const SETTINGS_CHOICES = [
|
|
29
231
|
{
|
|
30
|
-
value: '
|
|
31
|
-
name: 'Strict по умолчанию для explore
|
|
32
|
-
checked: DEFAULT_SETTINGS.strict
|
|
232
|
+
value: 'exploreStrict',
|
|
233
|
+
name: 'Strict по умолчанию для eda-explore',
|
|
234
|
+
checked: DEFAULT_SETTINGS.explore.strict
|
|
33
235
|
},
|
|
34
236
|
{
|
|
35
|
-
value: '
|
|
36
|
-
name: '
|
|
37
|
-
checked: DEFAULT_SETTINGS.
|
|
237
|
+
value: 'planStrict',
|
|
238
|
+
name: 'Strict по умолчанию для eda-plan',
|
|
239
|
+
checked: DEFAULT_SETTINGS.plan.strict
|
|
38
240
|
},
|
|
39
241
|
{
|
|
40
|
-
value: '
|
|
41
|
-
name: '
|
|
42
|
-
checked: DEFAULT_SETTINGS.
|
|
242
|
+
value: 'planPolishStrict',
|
|
243
|
+
name: 'Strict по умолчанию для eda-plan-polish',
|
|
244
|
+
checked: DEFAULT_SETTINGS.planPolish.strict
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
value: 'sendReviewClosePreviousReviews',
|
|
248
|
+
name: 'Закрывать предыдущие ревью eda-send-review',
|
|
249
|
+
checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
value: 'discoverAutomationsIncludePlans',
|
|
253
|
+
name: 'Анализировать планы в eda-discover-automations по умолчанию',
|
|
254
|
+
checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
|
|
43
255
|
}
|
|
44
256
|
];
|
|
257
|
+
const ORHESTRA_MODE_CHOICES = [
|
|
258
|
+
{
|
|
259
|
+
value: 'automatic',
|
|
260
|
+
name: 'Автоматически пройти план, выполнение, ревью и ручные тесты'
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
value: 'manual',
|
|
264
|
+
name: 'Писать план вместе с человеком и передавать ему вопросы'
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
const AIM_MODE_CHOICES = [
|
|
268
|
+
{
|
|
269
|
+
value: 'automatic',
|
|
270
|
+
name: 'Автоматически отвечать на безопасные рабочие вопросы'
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
value: 'manual',
|
|
274
|
+
name: 'Передавать рабочие вопросы человеку'
|
|
275
|
+
}
|
|
276
|
+
];
|
|
277
|
+
const ORHESTRA_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
|
|
278
|
+
value,
|
|
279
|
+
name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
|
|
280
|
+
}));
|
|
45
281
|
const PLAN_SIZE_CHOICES = [
|
|
46
282
|
{
|
|
47
283
|
value: 'normal',
|
|
@@ -102,6 +338,21 @@ const LOGGING_STRATEGY_CHOICES = [
|
|
|
102
338
|
name: 'Спрашивать стратегию логирования каждый раз'
|
|
103
339
|
}
|
|
104
340
|
];
|
|
341
|
+
const REVIEW_MODE_CHOICES = [
|
|
342
|
+
{ value: 'always', name: 'always — запускать при каждом ревью' },
|
|
343
|
+
{ value: 'auto', name: 'auto — запускать только когда проверка применима' },
|
|
344
|
+
{ value: 'off', name: 'off — отключить проверку' }
|
|
345
|
+
];
|
|
346
|
+
const CLAUDE_REVIEW_MODEL_CHOICES = [
|
|
347
|
+
{ value: 'haiku', name: 'haiku — быстрая проверка' },
|
|
348
|
+
{ value: 'sonnet', name: 'sonnet — сильная проверка кода' },
|
|
349
|
+
{ value: 'opus', name: 'opus — максимальная глубина' }
|
|
350
|
+
];
|
|
351
|
+
const CODEX_REVIEW_MODEL_CHOICES = [
|
|
352
|
+
{ value: 'gpt-5.6-luna', name: 'gpt-5.6-luna — быстрая проверка' },
|
|
353
|
+
{ value: 'gpt-5.6-terra', name: 'gpt-5.6-terra — сильная проверка кода' },
|
|
354
|
+
{ value: 'gpt-5.6-sol', name: 'gpt-5.6-sol — максимальная глубина' }
|
|
355
|
+
];
|
|
105
356
|
|
|
106
357
|
export async function init({ cwd, input = process.stdin, output = process.stdout }) {
|
|
107
358
|
const targets = await askTargets({ input, output });
|
|
@@ -110,18 +361,18 @@ export async function init({ cwd, input = process.stdin, output = process.stdout
|
|
|
110
361
|
return;
|
|
111
362
|
}
|
|
112
363
|
await ensureSettings(cwd, { input, output });
|
|
113
|
-
await
|
|
364
|
+
await syncPackage(cwd, targets, output, { action: 'install' });
|
|
114
365
|
}
|
|
115
366
|
|
|
116
367
|
export async function update({ cwd, input = process.stdin, output = process.stdout }) {
|
|
117
368
|
const targets = await detectTargets(cwd);
|
|
118
369
|
if (targets.length === 0) {
|
|
119
|
-
output.write('Не нашёл
|
|
370
|
+
output.write('Не нашёл установленного пакета eda в этом проекте. Запусти `eda init`.\n');
|
|
120
371
|
return;
|
|
121
372
|
}
|
|
122
373
|
output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
|
|
123
374
|
await ensureSettings(cwd, { input, output });
|
|
124
|
-
await
|
|
375
|
+
await syncPackage(cwd, targets, output, { action: 'update' });
|
|
125
376
|
}
|
|
126
377
|
|
|
127
378
|
export async function updateAll({
|
|
@@ -139,11 +390,11 @@ export async function updateAll({
|
|
|
139
390
|
throw new Error(`Директория не найдена: ${rootDir}`);
|
|
140
391
|
}
|
|
141
392
|
|
|
142
|
-
output.write(`Ищу проекты с
|
|
393
|
+
output.write(`Ищу проекты с установленным пакетом eda в ${rootDir} (глубина ${maxDepth}).\n`);
|
|
143
394
|
|
|
144
395
|
const projects = await findInstalledProjects(rootDir, maxDepth);
|
|
145
396
|
if (projects.length === 0) {
|
|
146
|
-
output.write('Не нашёл проектов с
|
|
397
|
+
output.write('Не нашёл проектов с установленным пакетом eda.\n');
|
|
147
398
|
return {
|
|
148
399
|
root: rootDir,
|
|
149
400
|
maxDepth,
|
|
@@ -173,14 +424,16 @@ export async function updateAll({
|
|
|
173
424
|
}
|
|
174
425
|
|
|
175
426
|
output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
|
|
176
|
-
|
|
427
|
+
await migrateSettingsIfNeeded(projectDir, { output, reportExisting: false });
|
|
428
|
+
const result = await syncPackage(projectDir, targets, output, {
|
|
177
429
|
action: 'update',
|
|
178
430
|
writeDone: false
|
|
179
431
|
});
|
|
180
432
|
updatedProjects.push({
|
|
181
433
|
path: projectDir,
|
|
182
434
|
targets,
|
|
183
|
-
changedSkills: result.changedSkills
|
|
435
|
+
changedSkills: result.changedSkills,
|
|
436
|
+
changedAgents: result.changedAgents
|
|
184
437
|
});
|
|
185
438
|
} catch (err) {
|
|
186
439
|
failedProjects.push({ path: projectDir, error: err });
|
|
@@ -213,7 +466,7 @@ export async function askTargets({ input = process.stdin, output = process.stdou
|
|
|
213
466
|
}
|
|
214
467
|
|
|
215
468
|
return checkbox({
|
|
216
|
-
message: 'Куда устанавливать
|
|
469
|
+
message: 'Куда устанавливать пакет eda?',
|
|
217
470
|
instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
|
|
218
471
|
choices: TARGET_CHOICES.map(choice => ({
|
|
219
472
|
name: `${choice.label} (${choice.dir})`,
|
|
@@ -229,7 +482,7 @@ export async function askTargets({ input = process.stdin, output = process.stdou
|
|
|
229
482
|
export async function askSettings({ input = process.stdin, output = process.stdout } = {}) {
|
|
230
483
|
if (!input.isTTY || !output.isTTY) {
|
|
231
484
|
output.write(`Нет интерактивного терминала — создаю ${SETTINGS_RELATIVE_PATH} с настройками по умолчанию.\n`);
|
|
232
|
-
return
|
|
485
|
+
return structuredClone(DEFAULT_SETTINGS);
|
|
233
486
|
}
|
|
234
487
|
|
|
235
488
|
const selected = await checkbox({
|
|
@@ -241,19 +494,76 @@ export async function askSettings({ input = process.stdin, output = process.stdo
|
|
|
241
494
|
output
|
|
242
495
|
});
|
|
243
496
|
|
|
497
|
+
const orhestraMode = await select({
|
|
498
|
+
message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
|
|
499
|
+
choices: ORHESTRA_MODE_CHOICES,
|
|
500
|
+
default: DEFAULT_SETTINGS.orhestra.mode
|
|
501
|
+
}, {
|
|
502
|
+
input,
|
|
503
|
+
output
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const enabledOrhestraSteps = await checkbox({
|
|
507
|
+
message: 'Какие этапы eda-orhestra включить по умолчанию?',
|
|
508
|
+
instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
|
|
509
|
+
choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
|
|
510
|
+
name: step.name,
|
|
511
|
+
value: step.skill,
|
|
512
|
+
checked: step.enabled
|
|
513
|
+
}))
|
|
514
|
+
}, {
|
|
515
|
+
input,
|
|
516
|
+
output
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const polishLimit = enabledOrhestraSteps.includes('eda-polish')
|
|
520
|
+
? await select({
|
|
521
|
+
message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
|
|
522
|
+
choices: ORHESTRA_POLISH_LIMIT_CHOICES,
|
|
523
|
+
default: 5
|
|
524
|
+
}, {
|
|
525
|
+
input,
|
|
526
|
+
output
|
|
527
|
+
})
|
|
528
|
+
: 5;
|
|
529
|
+
|
|
530
|
+
const orhestraSteps = DEFAULT_ORHESTRA_STEPS.map(step => ({
|
|
531
|
+
...step,
|
|
532
|
+
enabled: enabledOrhestraSteps.includes(step.skill),
|
|
533
|
+
...(step.skill === 'eda-polish' ? { args: `limit ${polishLimit}` } : {})
|
|
534
|
+
}));
|
|
535
|
+
|
|
536
|
+
const aimMode = await select({
|
|
537
|
+
message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
|
|
538
|
+
choices: AIM_MODE_CHOICES,
|
|
539
|
+
default: DEFAULT_SETTINGS.aim.mode
|
|
540
|
+
}, {
|
|
541
|
+
input,
|
|
542
|
+
output
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const exploreDecisionMode = await select({
|
|
546
|
+
message: 'Как принимать важные решения в eda-explore?',
|
|
547
|
+
choices: DECISION_MODE_CHOICES,
|
|
548
|
+
default: DEFAULT_SETTINGS.explore.decisionMode
|
|
549
|
+
}, {
|
|
550
|
+
input,
|
|
551
|
+
output
|
|
552
|
+
});
|
|
553
|
+
|
|
244
554
|
const planSize = await select({
|
|
245
555
|
message: 'Какой размер плана eda-plan использовать по умолчанию?',
|
|
246
556
|
choices: PLAN_SIZE_CHOICES,
|
|
247
|
-
default: DEFAULT_SETTINGS.
|
|
557
|
+
default: DEFAULT_SETTINGS.plan.size
|
|
248
558
|
}, {
|
|
249
559
|
input,
|
|
250
560
|
output
|
|
251
561
|
});
|
|
252
562
|
|
|
253
|
-
const
|
|
254
|
-
message: 'Как принимать важные решения в eda-
|
|
563
|
+
const planDecisionMode = await select({
|
|
564
|
+
message: 'Как принимать важные решения в eda-plan?',
|
|
255
565
|
choices: DECISION_MODE_CHOICES,
|
|
256
|
-
default: DEFAULT_SETTINGS.decisionMode
|
|
566
|
+
default: DEFAULT_SETTINGS.plan.decisionMode
|
|
257
567
|
}, {
|
|
258
568
|
input,
|
|
259
569
|
output
|
|
@@ -262,7 +572,7 @@ export async function askSettings({ input = process.stdin, output = process.stdo
|
|
|
262
572
|
const testStrategy = await select({
|
|
263
573
|
message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
|
|
264
574
|
choices: TEST_STRATEGY_CHOICES,
|
|
265
|
-
default: DEFAULT_SETTINGS.testStrategy
|
|
575
|
+
default: DEFAULT_SETTINGS.plan.testStrategy
|
|
266
576
|
}, {
|
|
267
577
|
input,
|
|
268
578
|
output
|
|
@@ -271,40 +581,120 @@ export async function askSettings({ input = process.stdin, output = process.stdo
|
|
|
271
581
|
const loggingStrategy = await select({
|
|
272
582
|
message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
|
|
273
583
|
choices: LOGGING_STRATEGY_CHOICES,
|
|
274
|
-
default: DEFAULT_SETTINGS.loggingStrategy
|
|
584
|
+
default: DEFAULT_SETTINGS.plan.loggingStrategy
|
|
275
585
|
}, {
|
|
276
586
|
input,
|
|
277
587
|
output
|
|
278
588
|
});
|
|
279
589
|
|
|
590
|
+
const reviewAgents = await askReviewAgentSettings({ input, output });
|
|
591
|
+
|
|
280
592
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
593
|
+
orhestra: {
|
|
594
|
+
mode: orhestraMode,
|
|
595
|
+
steps: orhestraSteps
|
|
596
|
+
},
|
|
597
|
+
aim: {
|
|
598
|
+
mode: aimMode
|
|
599
|
+
},
|
|
600
|
+
explore: {
|
|
601
|
+
strict: selected.includes('exploreStrict'),
|
|
602
|
+
decisionMode: exploreDecisionMode
|
|
603
|
+
},
|
|
604
|
+
plan: {
|
|
605
|
+
strict: selected.includes('planStrict'),
|
|
606
|
+
size: planSize,
|
|
607
|
+
decisionMode: planDecisionMode,
|
|
608
|
+
testStrategy,
|
|
609
|
+
loggingStrategy
|
|
610
|
+
},
|
|
611
|
+
planPolish: {
|
|
612
|
+
strict: selected.includes('planPolishStrict')
|
|
613
|
+
},
|
|
614
|
+
review: {
|
|
615
|
+
agents: reviewAgents
|
|
616
|
+
},
|
|
617
|
+
sendReview: {
|
|
618
|
+
closePreviousReviews: selected.includes('sendReviewClosePreviousReviews')
|
|
619
|
+
},
|
|
620
|
+
discoverAutomations: {
|
|
621
|
+
includePlans: selected.includes('discoverAutomationsIncludePlans')
|
|
622
|
+
}
|
|
288
623
|
};
|
|
289
624
|
}
|
|
290
625
|
|
|
626
|
+
export async function askReviewAgentSettings({
|
|
627
|
+
input = process.stdin,
|
|
628
|
+
output = process.stdout,
|
|
629
|
+
selectPrompt = select
|
|
630
|
+
} = {}) {
|
|
631
|
+
const reviewAgents = {};
|
|
632
|
+
for (const agent of REVIEW_AGENT_DEFINITIONS) {
|
|
633
|
+
const mode = await selectPrompt({
|
|
634
|
+
message: `Когда запускать review-проверку ${agent.key}?`,
|
|
635
|
+
choices: REVIEW_MODE_CHOICES,
|
|
636
|
+
default: agent.mode
|
|
637
|
+
}, {
|
|
638
|
+
input,
|
|
639
|
+
output
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
let claude = agent.claude;
|
|
643
|
+
let codex = agent.codex;
|
|
644
|
+
if (mode !== 'off') {
|
|
645
|
+
claude = await selectPrompt({
|
|
646
|
+
message: `Какой моделью Claude проверять ${agent.key}?`,
|
|
647
|
+
choices: CLAUDE_REVIEW_MODEL_CHOICES,
|
|
648
|
+
default: agent.claude
|
|
649
|
+
}, {
|
|
650
|
+
input,
|
|
651
|
+
output
|
|
652
|
+
});
|
|
653
|
+
codex = await selectPrompt({
|
|
654
|
+
message: `Какой моделью Codex проверять ${agent.key}?`,
|
|
655
|
+
choices: CODEX_REVIEW_MODEL_CHOICES,
|
|
656
|
+
default: agent.codex
|
|
657
|
+
}, {
|
|
658
|
+
input,
|
|
659
|
+
output
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
reviewAgents[agent.key] = { mode, model: { claude, codex } };
|
|
663
|
+
}
|
|
664
|
+
return reviewAgents;
|
|
665
|
+
}
|
|
666
|
+
|
|
291
667
|
async function detectTargets(cwd) {
|
|
292
668
|
const targets = [];
|
|
293
|
-
if (await
|
|
294
|
-
if (await
|
|
669
|
+
if (await targetStructureExists(cwd, 'claude')) targets.push('claude');
|
|
670
|
+
if (await targetStructureExists(cwd, 'codex')) targets.push('codex');
|
|
295
671
|
return targets;
|
|
296
672
|
}
|
|
297
673
|
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// где папка .claude/skills или .codex/skills есть, но eda-скилов в ней нет.
|
|
674
|
+
// При массовом обходе среда считается установленной по eda-скилу, eda-агенту
|
|
675
|
+
// или manifest владения. Одних пустых/чужих каталогов skills и agents недостаточно.
|
|
301
676
|
async function detectInstalledTargets(cwd) {
|
|
302
677
|
const targets = [];
|
|
303
|
-
if (await
|
|
304
|
-
if (await
|
|
678
|
+
if (await hasInstalledPackage(cwd, 'claude')) targets.push('claude');
|
|
679
|
+
if (await hasInstalledPackage(cwd, 'codex')) targets.push('codex');
|
|
305
680
|
return targets;
|
|
306
681
|
}
|
|
307
682
|
|
|
683
|
+
async function targetStructureExists(cwd, target) {
|
|
684
|
+
const root = path.join(cwd, `.${target}`);
|
|
685
|
+
return await dirExists(path.join(root, 'skills'))
|
|
686
|
+
|| await dirExists(path.join(root, 'agents'))
|
|
687
|
+
|| await fileExists(path.join(root, MANIFEST_FILE));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function hasInstalledPackage(cwd, target) {
|
|
691
|
+
const root = path.join(cwd, `.${target}`);
|
|
692
|
+
const agentExtension = target === 'claude' ? '.md' : '.toml';
|
|
693
|
+
return await fileExists(path.join(root, MANIFEST_FILE))
|
|
694
|
+
|| await hasInstalledSkill(path.join(root, 'skills'))
|
|
695
|
+
|| await hasInstalledAgent(path.join(root, 'agents'), agentExtension);
|
|
696
|
+
}
|
|
697
|
+
|
|
308
698
|
async function hasInstalledSkill(skillsDir) {
|
|
309
699
|
let entries;
|
|
310
700
|
try {
|
|
@@ -317,31 +707,53 @@ async function hasInstalledSkill(skillsDir) {
|
|
|
317
707
|
return entries.some(entry => entry.name.startsWith('eda-'));
|
|
318
708
|
}
|
|
319
709
|
|
|
320
|
-
async function
|
|
321
|
-
|
|
710
|
+
async function hasInstalledAgent(agentsDir, extension) {
|
|
711
|
+
let entries;
|
|
712
|
+
try {
|
|
713
|
+
entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
714
|
+
} catch (err) {
|
|
715
|
+
if (['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM'].includes(err?.code)) return false;
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
return entries.some(entry => entry.isFile() && entry.name.startsWith('eda-') && entry.name.endsWith(extension));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function syncPackage(cwd, targets, output = process.stdout, { action = 'update', writeDone = true } = {}) {
|
|
722
|
+
const skills = await listSkills(SKILLS_SRC);
|
|
322
723
|
if (skills.length === 0) {
|
|
323
724
|
throw new Error(`В пакете нет скилов (искал в ${SKILLS_SRC}).`);
|
|
324
725
|
}
|
|
726
|
+
const agents = await listAgents(AGENTS_SRC);
|
|
727
|
+
const packageVersion = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, 'utf8')).version;
|
|
325
728
|
output.write(`Скилы для установки: ${skills.map(s => s.name).join(', ')}\n`);
|
|
729
|
+
if (agents.length > 0) {
|
|
730
|
+
output.write(`Агенты для установки: ${agents.map(agent => agent.name).join(', ')}\n`);
|
|
731
|
+
}
|
|
326
732
|
|
|
327
733
|
const changedSkills = new Set();
|
|
734
|
+
const changedAgents = new Set();
|
|
328
735
|
if (targets.includes('claude')) {
|
|
329
|
-
const result = await
|
|
736
|
+
const result = await installTarget(cwd, 'claude', skills, agents, packageVersion, output);
|
|
330
737
|
for (const skillName of result.changedSkills) changedSkills.add(skillName);
|
|
738
|
+
for (const agentName of result.changedAgents) changedAgents.add(agentName);
|
|
331
739
|
}
|
|
332
740
|
if (targets.includes('codex')) {
|
|
333
|
-
const result = await
|
|
741
|
+
const result = await installTarget(cwd, 'codex', skills, agents, packageVersion, output);
|
|
334
742
|
for (const skillName of result.changedSkills) changedSkills.add(skillName);
|
|
743
|
+
for (const agentName of result.changedAgents) changedAgents.add(agentName);
|
|
335
744
|
}
|
|
336
|
-
await removeRetiredSkills(cwd, targets);
|
|
337
745
|
|
|
338
746
|
const actionLabel = action === 'install' ? 'Установлено' : 'Обновлено';
|
|
339
747
|
const changedSkillNames = skills
|
|
340
748
|
.map(skill => skill.name)
|
|
341
749
|
.filter(skillName => changedSkills.has(skillName));
|
|
750
|
+
const changedAgentNames = agents
|
|
751
|
+
.map(agent => agent.name)
|
|
752
|
+
.filter(agentName => changedAgents.has(agentName));
|
|
342
753
|
output.write(formatChangedSkills(actionLabel, changedSkillNames));
|
|
754
|
+
output.write(formatChangedAgents(actionLabel, changedAgentNames));
|
|
343
755
|
if (writeDone) output.write('\nГотово.\n');
|
|
344
|
-
return { changedSkills: changedSkillNames };
|
|
756
|
+
return { changedSkills: changedSkillNames, changedAgents: changedAgentNames };
|
|
345
757
|
}
|
|
346
758
|
|
|
347
759
|
async function findInstalledProjects(rootDir, maxDepth) {
|
|
@@ -373,22 +785,10 @@ async function findInstalledProjects(rootDir, maxDepth) {
|
|
|
373
785
|
return projects;
|
|
374
786
|
}
|
|
375
787
|
|
|
376
|
-
async function listSkills() {
|
|
377
|
-
const entries = await fs.readdir(SKILLS_SRC);
|
|
378
|
-
return entries
|
|
379
|
-
.filter(f => f.endsWith('.md'))
|
|
380
|
-
.map(f => ({ name: f.replace(/\.md$/, ''), path: path.join(SKILLS_SRC, f) }))
|
|
381
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
788
|
async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
385
789
|
const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
output.write('Существующий файл не перезаписываю. Актуальный формат:\n\n');
|
|
389
|
-
output.write(formatSettings(DEFAULT_SETTINGS));
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
790
|
+
const existing = await migrateSettingsIfNeeded(cwd, { output });
|
|
791
|
+
if (existing.exists) return;
|
|
392
792
|
|
|
393
793
|
const settings = await askSettings({ input, output });
|
|
394
794
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
@@ -396,73 +796,408 @@ async function ensureSettings(cwd, { input = process.stdin, output = process.std
|
|
|
396
796
|
output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
397
797
|
}
|
|
398
798
|
|
|
799
|
+
async function migrateSettingsIfNeeded(cwd, {
|
|
800
|
+
output = process.stdout,
|
|
801
|
+
reportExisting = true
|
|
802
|
+
} = {}) {
|
|
803
|
+
const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
|
|
804
|
+
const content = await readFileIfExists(settingsPath);
|
|
805
|
+
if (content === null) return { exists: false, migrated: false };
|
|
806
|
+
|
|
807
|
+
const version = readTopLevelYamlScalar(content, 'version');
|
|
808
|
+
if (version !== '1') {
|
|
809
|
+
if (reportExisting) {
|
|
810
|
+
output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
811
|
+
if (version !== '2') {
|
|
812
|
+
output.write('Автомиграция не выполнена: безопасно поддерживается только переход version: 1 → version: 2.\n');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return { exists: true, migrated: false, version };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const migratedSettings = migrateSettingsV1(content);
|
|
819
|
+
await writeFileAtomically(settingsPath, formatSettings(migratedSettings));
|
|
820
|
+
output.write(`Мигрированы настройки ${SETTINGS_RELATIVE_PATH}: version: 1 → version: 2.\n`);
|
|
821
|
+
return { exists: true, migrated: true, version: '2' };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function migrateSettingsV1(content) {
|
|
825
|
+
const defaultsStrict = readLegacyBoolean(content, 'defaults', 'strict', false);
|
|
826
|
+
const decisionMode = readLegacyEnum(
|
|
827
|
+
content,
|
|
828
|
+
'defaults',
|
|
829
|
+
'decision_mode',
|
|
830
|
+
['autonomous', 'recommend_and_ask', 'ask_each_time'],
|
|
831
|
+
DEFAULT_SETTINGS.explore.decisionMode
|
|
832
|
+
);
|
|
833
|
+
const reviewAgents = structuredClone(DEFAULT_REVIEW_AGENTS);
|
|
834
|
+
reviewAgents.code_quality.mode = readLegacyBoolean(
|
|
835
|
+
content,
|
|
836
|
+
'review',
|
|
837
|
+
'include_code_quality',
|
|
838
|
+
true
|
|
839
|
+
) ? 'always' : 'off';
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
...structuredClone(DEFAULT_SETTINGS),
|
|
843
|
+
explore: {
|
|
844
|
+
strict: defaultsStrict,
|
|
845
|
+
decisionMode
|
|
846
|
+
},
|
|
847
|
+
plan: {
|
|
848
|
+
strict: defaultsStrict,
|
|
849
|
+
size: readLegacyEnum(
|
|
850
|
+
content,
|
|
851
|
+
'defaults',
|
|
852
|
+
'plan_size',
|
|
853
|
+
['normal', 'short', 'ask_each_time'],
|
|
854
|
+
DEFAULT_SETTINGS.plan.size
|
|
855
|
+
),
|
|
856
|
+
decisionMode,
|
|
857
|
+
testStrategy: readLegacyEnum(
|
|
858
|
+
content,
|
|
859
|
+
'defaults',
|
|
860
|
+
'test_strategy',
|
|
861
|
+
['after_each_phase', 'tdd_each_phase', 'end_of_plan', 'ask_each_time'],
|
|
862
|
+
DEFAULT_SETTINGS.plan.testStrategy
|
|
863
|
+
),
|
|
864
|
+
loggingStrategy: readLegacyEnum(
|
|
865
|
+
content,
|
|
866
|
+
'defaults',
|
|
867
|
+
'logging_strategy',
|
|
868
|
+
['debug_precise', 'standard', 'ask_each_time'],
|
|
869
|
+
DEFAULT_SETTINGS.plan.loggingStrategy
|
|
870
|
+
)
|
|
871
|
+
},
|
|
872
|
+
planPolish: {
|
|
873
|
+
strict: defaultsStrict
|
|
874
|
+
},
|
|
875
|
+
review: {
|
|
876
|
+
agents: reviewAgents
|
|
877
|
+
},
|
|
878
|
+
discoverAutomations: {
|
|
879
|
+
includePlans: readLegacyBoolean(content, 'automate', 'include_plans', false)
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function readLegacyBoolean(content, section, key, fallback) {
|
|
885
|
+
const value = readNestedYamlScalar(content, section, key);
|
|
886
|
+
if (value === 'true') return true;
|
|
887
|
+
if (value === 'false') return false;
|
|
888
|
+
return fallback;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function readLegacyEnum(content, section, key, allowedValues, fallback) {
|
|
892
|
+
const value = readNestedYamlScalar(content, section, key);
|
|
893
|
+
return allowedValues.includes(value) ? value : fallback;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function readTopLevelYamlScalar(content, key) {
|
|
897
|
+
for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
898
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
899
|
+
if (match?.[1] === key) return normalizeYamlScalar(match[2]);
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function readNestedYamlScalar(content, section, key) {
|
|
905
|
+
let currentSection = null;
|
|
906
|
+
for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
907
|
+
if (/^\s*(?:#.*)?$/.test(line)) continue;
|
|
908
|
+
|
|
909
|
+
const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*(?:#.*)?$/);
|
|
910
|
+
if (sectionMatch) {
|
|
911
|
+
currentSection = sectionMatch[1];
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (/^\S/.test(line)) {
|
|
916
|
+
currentSection = null;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (currentSection !== section) continue;
|
|
921
|
+
const valueMatch = line.match(/^ {2}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
922
|
+
if (valueMatch?.[1] === key) return normalizeYamlScalar(valueMatch[2]);
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function normalizeYamlScalar(value) {
|
|
928
|
+
const withoutComment = value.replace(/\s+#.*$/, '').trim();
|
|
929
|
+
if (
|
|
930
|
+
withoutComment.length >= 2
|
|
931
|
+
&& ((withoutComment.startsWith('"') && withoutComment.endsWith('"'))
|
|
932
|
+
|| (withoutComment.startsWith("'") && withoutComment.endsWith("'")))
|
|
933
|
+
) {
|
|
934
|
+
return withoutComment.slice(1, -1);
|
|
935
|
+
}
|
|
936
|
+
return withoutComment;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function writeFileAtomically(filePath, content) {
|
|
940
|
+
const currentStat = await fs.stat(filePath);
|
|
941
|
+
const tempPath = path.join(
|
|
942
|
+
path.dirname(filePath),
|
|
943
|
+
`.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`
|
|
944
|
+
);
|
|
945
|
+
try {
|
|
946
|
+
await fs.writeFile(tempPath, content, { mode: currentStat.mode & 0o777 });
|
|
947
|
+
await fs.rename(tempPath, filePath);
|
|
948
|
+
} finally {
|
|
949
|
+
await fs.rm(tempPath, { force: true });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
399
953
|
function formatSettings(settings) {
|
|
400
|
-
|
|
954
|
+
const orhestra = settings.orhestra ?? DEFAULT_SETTINGS.orhestra;
|
|
955
|
+
const aim = settings.aim ?? DEFAULT_SETTINGS.aim;
|
|
956
|
+
const orhestraSteps = (orhestra.steps ?? DEFAULT_ORHESTRA_STEPS).map(step => {
|
|
957
|
+
const onFailureThen = step.onFailure?.then
|
|
958
|
+
?.map(stepId => ` - ${stepId}`)
|
|
959
|
+
.join('\n');
|
|
960
|
+
const onFailure = step.onFailure
|
|
961
|
+
? `
|
|
962
|
+
# Обработка failed-результата этого шага.
|
|
963
|
+
on_failure:
|
|
964
|
+
skill: ${step.onFailure.skill}
|
|
965
|
+
args: ${JSON.stringify(step.onFailure.args ?? '')}
|
|
966
|
+
# После исправления повторно запускаются включённые шаги с этими id.
|
|
967
|
+
then:
|
|
968
|
+
${onFailureThen}
|
|
969
|
+
max_cycles: ${step.onFailure.maxCycles ?? 5}`
|
|
970
|
+
: '';
|
|
971
|
+
return ` - id: ${step.id}
|
|
972
|
+
skill: ${step.skill}
|
|
973
|
+
enabled: ${step.enabled !== false ? 'true' : 'false'}
|
|
974
|
+
# Строка аргументов передаётся скиллу как часть текущего вызова.
|
|
975
|
+
args: ${JSON.stringify(step.args ?? '')}${onFailure}`;
|
|
976
|
+
}).join('\n');
|
|
977
|
+
const reviewAgents = REVIEW_AGENT_DEFINITIONS.map(agent => {
|
|
978
|
+
const configured = settings.review.agents[agent.key] ?? DEFAULT_REVIEW_AGENTS[agent.key];
|
|
979
|
+
return ` # ${agent.description}
|
|
980
|
+
# ${agent.auto}
|
|
981
|
+
${agent.key}:
|
|
982
|
+
# always — всегда, auto — когда применимо, off — отключено.
|
|
983
|
+
mode: ${configured.mode}
|
|
984
|
+
model:
|
|
985
|
+
# Модель для Claude Code.
|
|
986
|
+
claude: ${configured.model.claude}
|
|
987
|
+
# Модель для Codex.
|
|
988
|
+
codex: ${configured.model.codex}`;
|
|
989
|
+
}).join('\n\n');
|
|
990
|
+
|
|
991
|
+
return `version: 2
|
|
992
|
+
|
|
993
|
+
orhestra:
|
|
994
|
+
# Режим полного цикла eda-orhestra.
|
|
995
|
+
# automatic | manual
|
|
996
|
+
mode: ${orhestra.mode ?? 'automatic'}
|
|
997
|
+
# Упорядоченная цепочка. Шаги можно отключать, удалять и переставлять.
|
|
998
|
+
steps:
|
|
999
|
+
${orhestraSteps}
|
|
1000
|
+
|
|
1001
|
+
aim:
|
|
1002
|
+
# Режим ответов на рабочие вопросы eda-aim.
|
|
1003
|
+
# automatic | manual
|
|
1004
|
+
mode: ${aim.mode ?? 'automatic'}
|
|
1005
|
+
|
|
1006
|
+
explore:
|
|
1007
|
+
# Включает кросс-CLI ревью в eda-explore.
|
|
1008
|
+
# true | false
|
|
1009
|
+
strict: ${settings.explore.strict ? 'true' : 'false'}
|
|
1010
|
+
# Определяет, как eda-explore ведёт исследовательские развилки.
|
|
1011
|
+
# autonomous | recommend_and_ask | ask_each_time
|
|
1012
|
+
decision_mode: ${settings.explore.decisionMode}
|
|
401
1013
|
|
|
402
|
-
|
|
403
|
-
# Включает
|
|
1014
|
+
plan:
|
|
1015
|
+
# Включает кросс-CLI ревью в eda-plan.
|
|
404
1016
|
# true | false
|
|
405
|
-
strict: ${settings.strict ? 'true' : 'false'}
|
|
406
|
-
# Задаёт размер
|
|
1017
|
+
strict: ${settings.plan.strict ? 'true' : 'false'}
|
|
1018
|
+
# Задаёт размер плана.
|
|
407
1019
|
# normal | short | ask_each_time
|
|
408
|
-
|
|
409
|
-
# Определяет, как eda-
|
|
1020
|
+
size: ${settings.plan.size}
|
|
1021
|
+
# Определяет, как eda-plan принимает существенные решения.
|
|
410
1022
|
# autonomous | recommend_and_ask | ask_each_time
|
|
411
|
-
decision_mode: ${settings.decisionMode}
|
|
412
|
-
# Задаёт стратегию
|
|
1023
|
+
decision_mode: ${settings.plan.decisionMode}
|
|
1024
|
+
# Задаёт стратегию тестов.
|
|
413
1025
|
# after_each_phase | tdd_each_phase | end_of_plan | ask_each_time
|
|
414
|
-
test_strategy: ${settings.testStrategy}
|
|
415
|
-
# Задаёт стратегию
|
|
1026
|
+
test_strategy: ${settings.plan.testStrategy}
|
|
1027
|
+
# Задаёт стратегию логирования.
|
|
416
1028
|
# debug_precise | standard | ask_each_time
|
|
417
|
-
logging_strategy: ${settings.loggingStrategy}
|
|
1029
|
+
logging_strategy: ${settings.plan.loggingStrategy}
|
|
418
1030
|
|
|
419
|
-
|
|
420
|
-
#
|
|
1031
|
+
plan-polish:
|
|
1032
|
+
# Включает кросс-CLI ревью в eda-plan-polish.
|
|
421
1033
|
# true | false
|
|
422
|
-
|
|
1034
|
+
strict: ${settings.planPolish.strict ? 'true' : 'false'}
|
|
423
1035
|
|
|
424
1036
|
review:
|
|
425
|
-
#
|
|
1037
|
+
# Каждая проверка имеет собственный режим запуска и модели для обеих сред.
|
|
1038
|
+
agents:
|
|
1039
|
+
${reviewAgents}
|
|
1040
|
+
|
|
1041
|
+
send-review:
|
|
1042
|
+
# После успешной отправки скрывает предыдущие сводки eda-send-review и резолвит их inline-треды.
|
|
1043
|
+
# true | false
|
|
1044
|
+
close_previous_reviews: ${settings.sendReview.closePreviousReviews ? 'true' : 'false'}
|
|
1045
|
+
|
|
1046
|
+
discover-automations:
|
|
1047
|
+
# Добавляет docs/plans/ в обычный запуск eda-discover-automations.
|
|
426
1048
|
# true | false
|
|
427
|
-
|
|
1049
|
+
include_plans: ${settings.discoverAutomations.includePlans ? 'true' : 'false'}
|
|
428
1050
|
`;
|
|
429
1051
|
}
|
|
430
1052
|
|
|
431
|
-
async function
|
|
432
|
-
const
|
|
433
|
-
|
|
1053
|
+
async function installTarget(cwd, target, skills, agents, packageVersion, output = process.stdout) {
|
|
1054
|
+
const targetRoot = path.join(cwd, `.${target}`);
|
|
1055
|
+
const skillsDir = path.join(targetRoot, 'skills');
|
|
1056
|
+
const agentsDir = path.join(targetRoot, 'agents');
|
|
1057
|
+
const manifestPath = path.join(targetRoot, MANIFEST_FILE);
|
|
1058
|
+
await assertManagedDirectory(targetRoot);
|
|
1059
|
+
await assertManagedDirectory(skillsDir);
|
|
1060
|
+
await assertManagedDirectory(agentsDir);
|
|
1061
|
+
const previousManifest = await readManifest(manifestPath);
|
|
434
1062
|
const changedSkills = [];
|
|
1063
|
+
const changedAgents = [];
|
|
1064
|
+
|
|
1065
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
435
1066
|
for (const skill of skills) {
|
|
436
|
-
const
|
|
437
|
-
await
|
|
438
|
-
const content = await fs.readFile(skill.path, 'utf-8');
|
|
439
|
-
const changed = await writeSkillFile(path.join(skillDir, 'SKILL.md'), content);
|
|
1067
|
+
const desiredFiles = await buildSkillFiles(skill, target);
|
|
1068
|
+
const changed = await syncManagedDirectory(path.join(skillsDir, skill.name), desiredFiles);
|
|
440
1069
|
if (changed) changedSkills.push(skill.name);
|
|
1070
|
+
if (target === 'codex') await removeObsoleteCodexFile(skillsDir, skill.name);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (agents.length > 0) await fs.mkdir(agentsDir, { recursive: true });
|
|
1074
|
+
for (const agent of agents) {
|
|
1075
|
+
const extension = target === 'claude' ? '.md' : '.toml';
|
|
1076
|
+
const rendered = target === 'claude'
|
|
1077
|
+
? renderClaudeAgent(agent, agent.prompt)
|
|
1078
|
+
: renderCodexAgent(agent, agent.prompt);
|
|
1079
|
+
const changed = await writeManagedFile(path.join(agentsDir, `${agent.name}${extension}`), rendered);
|
|
1080
|
+
if (changed) changedAgents.push(agent.name);
|
|
441
1081
|
}
|
|
442
|
-
|
|
443
|
-
|
|
1082
|
+
|
|
1083
|
+
await removeRetiredComponents(targetRoot, target, previousManifest, skills, agents);
|
|
1084
|
+
await writeManifest(manifestPath, {
|
|
1085
|
+
schemaVersion: 1,
|
|
1086
|
+
packageVersion,
|
|
1087
|
+
skills: skills.map(skill => skill.name),
|
|
1088
|
+
agents: agents.map(agent => agent.name)
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
const label = target === 'claude' ? 'Claude Code' : 'Codex CLI';
|
|
1092
|
+
output.write(` ✓ ${label}: ${targetRoot} (скилы: ${formatSkillCount(changedSkills.length)}, агенты: ${formatAgentCount(changedAgents.length)})\n`);
|
|
1093
|
+
return { changedSkills, changedAgents };
|
|
444
1094
|
}
|
|
445
1095
|
|
|
446
|
-
async function
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1096
|
+
async function buildSkillFiles(skill, target) {
|
|
1097
|
+
const files = await readSourceDirectory(skill.sourceDir, new Set(['skill.json']));
|
|
1098
|
+
const entrypoint = files.get('SKILL.md');
|
|
1099
|
+
if (!entrypoint) throw new Error(`У скила ${skill.name} нет SKILL.md.`);
|
|
1100
|
+
|
|
1101
|
+
const content = entrypoint.toString('utf8');
|
|
1102
|
+
const rendered = target === 'claude'
|
|
1103
|
+
? renderClaudeSkill(content, skill.config)
|
|
1104
|
+
: renderCodexSkill(content, skill.config);
|
|
1105
|
+
files.set('SKILL.md', Buffer.from(rendered));
|
|
1106
|
+
return files;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function syncManagedDirectory(directoryPath, desiredFiles) {
|
|
1110
|
+
const currentFiles = await readInstalledDirectory(directoryPath);
|
|
1111
|
+
if (fileMapsEqual(currentFiles, desiredFiles)) return false;
|
|
1112
|
+
|
|
1113
|
+
await fs.rm(directoryPath, { recursive: true, force: true });
|
|
1114
|
+
for (const [relativePath, content] of desiredFiles) {
|
|
1115
|
+
const destination = path.join(directoryPath, relativePath);
|
|
1116
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
1117
|
+
await fs.writeFile(destination, content);
|
|
1118
|
+
}
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function readSourceDirectory(root, excludedFiles = new Set()) {
|
|
1123
|
+
const files = new Map();
|
|
1124
|
+
|
|
1125
|
+
async function walk(directory, relativeDirectory = '') {
|
|
1126
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
1127
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1128
|
+
for (const entry of entries) {
|
|
1129
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
1130
|
+
if (relativeDirectory === '' && excludedFiles.has(entry.name)) continue;
|
|
1131
|
+
if (entry.isSymbolicLink()) {
|
|
1132
|
+
throw new Error(`Symlink не поддерживается в исходниках пакета: ${path.join(root, relativePath)}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (entry.isDirectory()) {
|
|
1135
|
+
await walk(path.join(directory, entry.name), relativePath);
|
|
1136
|
+
} else if (entry.isFile()) {
|
|
1137
|
+
files.set(relativePath, await fs.readFile(path.join(directory, entry.name)));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
await walk(root);
|
|
1143
|
+
return files;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function readInstalledDirectory(root) {
|
|
1147
|
+
const files = new Map();
|
|
1148
|
+
let rootStat;
|
|
1149
|
+
try {
|
|
1150
|
+
rootStat = await fs.lstat(root);
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
if (err?.code === 'ENOENT') return files;
|
|
1153
|
+
throw err;
|
|
1154
|
+
}
|
|
1155
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
1156
|
+
files.set('', Buffer.from('__not_a_directory__'));
|
|
1157
|
+
return files;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function walk(directory, relativeDirectory = '') {
|
|
1161
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
1162
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1163
|
+
for (const entry of entries) {
|
|
1164
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
1165
|
+
if (entry.isSymbolicLink()) {
|
|
1166
|
+
files.set(relativePath, Buffer.from('__symlink__'));
|
|
1167
|
+
} else if (entry.isDirectory()) {
|
|
1168
|
+
await walk(path.join(directory, entry.name), relativePath);
|
|
1169
|
+
} else if (entry.isFile()) {
|
|
1170
|
+
files.set(relativePath, await fs.readFile(path.join(directory, entry.name)));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
457
1173
|
}
|
|
458
|
-
|
|
459
|
-
|
|
1174
|
+
|
|
1175
|
+
await walk(root);
|
|
1176
|
+
return files;
|
|
460
1177
|
}
|
|
461
1178
|
|
|
462
|
-
|
|
463
|
-
|
|
1179
|
+
function fileMapsEqual(left, right) {
|
|
1180
|
+
if (left.size !== right.size) return false;
|
|
1181
|
+
for (const [filePath, content] of right) {
|
|
1182
|
+
if (!left.get(filePath)?.equals(content)) return false;
|
|
1183
|
+
}
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
async function writeManagedFile(filePath, content) {
|
|
1188
|
+
const currentStat = await lstatIfExists(filePath);
|
|
1189
|
+
let previousContent = null;
|
|
1190
|
+
|
|
1191
|
+
if (currentStat?.isFile() && !currentStat.isSymbolicLink()) {
|
|
1192
|
+
previousContent = await fs.readFile(filePath, 'utf8');
|
|
1193
|
+
} else if (currentStat) {
|
|
1194
|
+
await fs.rm(filePath, { recursive: true, force: true });
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (previousContent === content) return false;
|
|
1198
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
464
1199
|
await fs.writeFile(filePath, content);
|
|
465
|
-
return
|
|
1200
|
+
return true;
|
|
466
1201
|
}
|
|
467
1202
|
|
|
468
1203
|
async function readFileIfExists(filePath) {
|
|
@@ -479,10 +1214,19 @@ function formatChangedSkills(actionLabel, skillNames) {
|
|
|
479
1214
|
return `${actionLabel} ${formatSkillCount(skillNames.length)}: ${skillNames.join(', ')}.\n`;
|
|
480
1215
|
}
|
|
481
1216
|
|
|
1217
|
+
function formatChangedAgents(actionLabel, agentNames) {
|
|
1218
|
+
if (agentNames.length === 0) return `${actionLabel} 0 агентов.\n`;
|
|
1219
|
+
return `${actionLabel} ${formatAgentCount(agentNames.length)}: ${agentNames.join(', ')}.\n`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
482
1222
|
function formatSkillCount(count) {
|
|
483
1223
|
return `${count} ${pluralizeSkill(count)}`;
|
|
484
1224
|
}
|
|
485
1225
|
|
|
1226
|
+
function formatAgentCount(count) {
|
|
1227
|
+
return `${count} ${pluralizeAgent(count)}`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
486
1230
|
function formatProjectPath(rootDir, projectDir) {
|
|
487
1231
|
const relative = path.relative(rootDir, projectDir);
|
|
488
1232
|
return relative === '' ? '.' : relative;
|
|
@@ -520,6 +1264,14 @@ function pluralizeSkill(count) {
|
|
|
520
1264
|
return 'скилов';
|
|
521
1265
|
}
|
|
522
1266
|
|
|
1267
|
+
function pluralizeAgent(count) {
|
|
1268
|
+
const mod10 = count % 10;
|
|
1269
|
+
const mod100 = count % 100;
|
|
1270
|
+
if (mod10 === 1 && mod100 !== 11) return 'агент';
|
|
1271
|
+
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'агента';
|
|
1272
|
+
return 'агентов';
|
|
1273
|
+
}
|
|
1274
|
+
|
|
523
1275
|
async function removeObsoleteCodexFile(dst, skillName) {
|
|
524
1276
|
try {
|
|
525
1277
|
await fs.rm(path.join(dst, `${skillName}.md`));
|
|
@@ -528,23 +1280,53 @@ async function removeObsoleteCodexFile(dst, skillName) {
|
|
|
528
1280
|
}
|
|
529
1281
|
}
|
|
530
1282
|
|
|
531
|
-
async function
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1283
|
+
async function removeRetiredComponents(targetRoot, target, previousManifest, skills, agents) {
|
|
1284
|
+
const currentSkills = new Set(skills.map(skill => skill.name));
|
|
1285
|
+
const currentAgents = new Set(agents.map(agent => agent.name));
|
|
1286
|
+
const staleSkills = new Set([
|
|
1287
|
+
...RETIRED_SKILLS,
|
|
1288
|
+
...(previousManifest?.skills ?? []).filter(name => !currentSkills.has(name))
|
|
1289
|
+
].filter(isManagedComponentName));
|
|
1290
|
+
const staleAgents = new Set([
|
|
1291
|
+
...RETIRED_AGENTS,
|
|
1292
|
+
...(previousManifest?.agents ?? []).filter(name => !currentAgents.has(name))
|
|
1293
|
+
].filter(isManagedComponentName));
|
|
536
1294
|
|
|
537
|
-
for (const
|
|
538
|
-
|
|
539
|
-
|
|
1295
|
+
for (const skillName of staleSkills) {
|
|
1296
|
+
await fs.rm(path.join(targetRoot, 'skills', skillName), { recursive: true, force: true });
|
|
1297
|
+
await fs.rm(path.join(targetRoot, 'skills', `${skillName}.md`), { force: true });
|
|
1298
|
+
}
|
|
540
1299
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1300
|
+
const extension = target === 'claude' ? '.md' : '.toml';
|
|
1301
|
+
for (const agentName of staleAgents) {
|
|
1302
|
+
await fs.rm(path.join(targetRoot, 'agents', `${agentName}${extension}`), { force: true });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function readManifest(manifestPath) {
|
|
1307
|
+
try {
|
|
1308
|
+
const manifestStat = await fs.lstat(manifestPath);
|
|
1309
|
+
if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) return null;
|
|
1310
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
1311
|
+
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.skills) || !Array.isArray(manifest.agents)) {
|
|
1312
|
+
return null;
|
|
544
1313
|
}
|
|
1314
|
+
return manifest;
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
if (err?.code === 'ENOENT' || err instanceof SyntaxError) return null;
|
|
1317
|
+
throw err;
|
|
545
1318
|
}
|
|
546
1319
|
}
|
|
547
1320
|
|
|
1321
|
+
async function writeManifest(manifestPath, manifest) {
|
|
1322
|
+
const content = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1323
|
+
await writeManagedFile(manifestPath, content);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function isManagedComponentName(name) {
|
|
1327
|
+
return typeof name === 'string' && /^eda-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
548
1330
|
async function dirExists(p) {
|
|
549
1331
|
try {
|
|
550
1332
|
const st = await fs.stat(p);
|
|
@@ -571,3 +1353,23 @@ async function statIfExists(p) {
|
|
|
571
1353
|
return null;
|
|
572
1354
|
}
|
|
573
1355
|
}
|
|
1356
|
+
|
|
1357
|
+
async function lstatIfExists(p) {
|
|
1358
|
+
try {
|
|
1359
|
+
return await fs.lstat(p);
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function assertManagedDirectory(directoryPath) {
|
|
1367
|
+
const directoryStat = await lstatIfExists(directoryPath);
|
|
1368
|
+
if (!directoryStat) return;
|
|
1369
|
+
if (directoryStat.isSymbolicLink()) {
|
|
1370
|
+
throw new Error(`Управляемый каталог не должен быть symlink: ${directoryPath}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (!directoryStat.isDirectory()) {
|
|
1373
|
+
throw new Error(`Ожидался каталог: ${directoryPath}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|