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