@gian-tiaga/eda 1.0.0 → 1.0.2
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 +8 -6
- package/README.md +5 -5
- package/bin/cli.js +2 -2
- package/lib/install.js +181 -9
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Миграция на eda 1.0
|
|
2
2
|
|
|
3
|
-
Версия 1.0 переводит `eda` с набора файлов-скилов на пакет с каноническими скилами и специализированными агентами для Claude Code и Codex CLI.
|
|
3
|
+
Версия 1.0 переводит `eda` с набора файлов-скилов на пакет с каноническими скилами и специализированными агентами для Claude Code и Codex CLI. Начиная с `1.0.2`, `eda update` заново спрашивает полный профиль настроек и записывает актуальный `docs/settings.yaml` версии 2 перед обновлением компонентов.
|
|
4
4
|
|
|
5
5
|
## Короткий путь
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ npm install -g @gian-tiaga/eda@1
|
|
|
11
11
|
eda --version
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Команда должна вывести `1.0.
|
|
14
|
+
Команда должна вывести актуальную версию ветки `1.0.x`.
|
|
15
15
|
|
|
16
16
|
В одном проекте выполни:
|
|
17
17
|
|
|
@@ -26,7 +26,7 @@ eda update
|
|
|
26
26
|
eda update-all /path/to/projects
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`update-all` ищет проекты на глубине до двух уровней,
|
|
29
|
+
`update-all` ищет проекты на глубине до двух уровней, один раз спрашивает полный профиль настроек и записывает одинаковый `docs/settings.yaml` версии 2 во все найденные проекты. В non-TTY запуске вопросы не задаются: во все проекты записывается один профиль defaults.
|
|
30
30
|
|
|
31
31
|
После обновления перезапусти Claude Code или Codex CLI, чтобы среда перечитала установленные скилы и агентов.
|
|
32
32
|
|
|
@@ -57,7 +57,9 @@ eda update-all /path/to/projects
|
|
|
57
57
|
|
|
58
58
|
## Миграция `docs/settings.yaml`
|
|
59
59
|
|
|
60
|
-
Версия 1.0 поддерживает только формат настроек `version: 2`.
|
|
60
|
+
Версия 1.0 поддерживает только формат настроек `version: 2`. При `eda update` ответь на полный набор вопросов: команда соберёт новый профиль и атомарно заменит прежний файл независимо от его версии. `eda update-all` задаст эти вопросы только один раз и применит ответы ко всем проектам.
|
|
61
|
+
|
|
62
|
+
`eda init` ведёт себя осторожнее: если конфиг уже существует, v2 и неизвестные версии сохраняются, а v1 автоматически мигрируется с переносом поддерживаемых значений. При update-командах прежний файл заменяется выбранным полным профилем, поэтому перед запуском сохрани нестандартные поля отдельно.
|
|
61
63
|
|
|
62
64
|
Основное соответствие полей:
|
|
63
65
|
|
|
@@ -74,7 +76,7 @@ eda update-all /path/to/projects
|
|
|
74
76
|
|
|
75
77
|
У старого `review.strict` нет прямого аналога. В 1.0 каждая проверка в `review.agents` отдельно получает режим `always`, `auto` или `off` и модели для Claude/Codex. Раздел `review-check` больше не используется.
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
Результат переноса старых общих настроек выглядит так:
|
|
78
80
|
|
|
79
81
|
```yaml
|
|
80
82
|
version: 2
|
|
@@ -97,7 +99,7 @@ discover-automations:
|
|
|
97
99
|
include_plans: false
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
Новые разделы `orhestra`, `aim`, `review.agents` и `send-review`
|
|
102
|
+
Новые разделы `orhestra`, `aim`, `review.agents` и `send-review` заполняются ответами на вопросы; в non-TTY запуске используются сбалансированные defaults. Полный актуальный пример приведён в [README](README.md).
|
|
101
103
|
|
|
102
104
|
## Проверка после миграции
|
|
103
105
|
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ eda init
|
|
|
29
29
|
eda update-all ~/Code
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`eda update-all` ищет проекты в указанной директории, её прямых подпапках и подпапках второго уровня. Проект считается найденным, если в нём уже есть установленный скил, агент или manifest `eda` для Claude/Codex.
|
|
32
|
+
`eda update-all` ищет проекты в указанной директории, её прямых подпапках и подпапках второго уровня. Проект считается найденным, если в нём уже есть установленный скил, агент или manifest `eda` для Claude/Codex. Перед обновлением команда один раз спрашивает полный профиль настроек и записывает одинаковый `docs/settings.yaml` во все найденные проекты. В неинтерактивном запуске во все проекты записывается один профиль по умолчанию.
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
@@ -347,7 +347,7 @@ discover-automations:
|
|
|
347
347
|
|
|
348
348
|
Поддерживается только `version: 2`. Для старого v2 без `review.agents` новый review использует сбалансированные defaults и предупреждает, что устаревшие `review.strict`, `review.include_code_quality` и `review-check` игнорируются. Установщик существующий файл не переписывает.
|
|
349
349
|
|
|
350
|
-
`eda init`
|
|
350
|
+
`eda init` создаёт `docs/settings.yaml`, только если файла ещё нет; существующий v1 автоматически мигрируется в v2, а v2 и неизвестные версии сохраняются. `eda update` всегда заново задаёт все вопросы конфигурации и атомарно перезаписывает файл полным v2-профилем. `eda update-all` задаёт тот же набор вопросов один раз и записывает получившийся общий конфиг во все найденные проекты. Без TTY обе update-команды используют defaults.
|
|
351
351
|
|
|
352
352
|
---
|
|
353
353
|
|
|
@@ -422,8 +422,8 @@ discover-automations может запускаться от review, fix-by-revie
|
|
|
422
422
|
|
|
423
423
|
```bash
|
|
424
424
|
eda init # выбрать Claude Code / Codex / обе среды и установить пакет
|
|
425
|
-
eda update #
|
|
426
|
-
eda update-all [dir] #
|
|
425
|
+
eda update # заново настроить конфиг и обновить скилы и агенты
|
|
426
|
+
eda update-all [dir] # один раз настроить конфиг и обновить все найденные проекты на глубине 2
|
|
427
427
|
eda --version # показать версию установленного пакета
|
|
428
428
|
eda --help # справка
|
|
429
429
|
```
|
|
@@ -456,7 +456,7 @@ eda update-all ~/Code
|
|
|
456
456
|
| Агент | `.claude/agents/<agent>.md` | `.codex/agents/<agent>.toml` |
|
|
457
457
|
| Manifest владения | `.claude/eda-manifest.json` | `.codex/eda-manifest.json` |
|
|
458
458
|
|
|
459
|
-
`eda update` идемпотентно синхронизирует обе группы компонентов и отдельно показывает реально изменившиеся скилы и агенты. Manifest хранит только идентификаторы компонентов, которыми владеет `eda`: благодаря этому retired-компоненты удаляются, а чужие скилы и агенты остаются нетронутыми. `eda update-all [dir]`
|
|
459
|
+
`eda update` заново собирает полный v2-конфиг, затем идемпотентно синхронизирует обе группы компонентов и отдельно показывает реально изменившиеся скилы и агенты. Manifest хранит только идентификаторы компонентов, которыми владеет `eda`: благодаря этому retired-компоненты удаляются, а чужие скилы и агенты остаются нетронутыми. `eda update-all [dir]` один раз собирает общий конфиг, записывает его во все найденные проекты и синхронизирует компоненты каждого проекта. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
|
|
460
460
|
|
|
461
461
|
Установленные копии не являются источником правды. Локальные изменения внутри `.claude/skills/eda-*`, `.codex/skills/eda-*`, `.claude/agents/eda-*` или `.codex/agents/eda-*` будут заменены следующим обновлением.
|
|
462
462
|
|
package/bin/cli.js
CHANGED
|
@@ -10,8 +10,8 @@ const HELP = `eda — установка и обновление скилов и
|
|
|
10
10
|
|
|
11
11
|
Использование:
|
|
12
12
|
eda init — выбрать целевые среды (Claude / Codex / обе) и установить пакет
|
|
13
|
-
eda update —
|
|
14
|
-
eda update-all [dir] —
|
|
13
|
+
eda update — заново настроить конфиг и обновить текущий проект
|
|
14
|
+
eda update-all [dir] — один раз настроить конфиг и обновить все проекты внутри dir
|
|
15
15
|
eda --version — показать версию
|
|
16
16
|
eda --help — показать эту справку
|
|
17
17
|
`;
|
package/lib/install.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
@@ -370,12 +371,13 @@ export async function update({ cwd, input = process.stdin, output = process.stdo
|
|
|
370
371
|
return;
|
|
371
372
|
}
|
|
372
373
|
output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
|
|
373
|
-
await
|
|
374
|
+
await configureSettings(cwd, { input, output });
|
|
374
375
|
await syncPackage(cwd, targets, output, { action: 'update' });
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
export async function updateAll({
|
|
378
379
|
root = process.cwd(),
|
|
380
|
+
input = process.stdin,
|
|
379
381
|
output = process.stdout,
|
|
380
382
|
maxDepth = UPDATE_ALL_MAX_DEPTH
|
|
381
383
|
} = {}) {
|
|
@@ -405,6 +407,9 @@ export async function updateAll({
|
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
output.write(`Найдено ${formatProjectCount(projects.length)}: ${projects.map(project => formatProjectPath(rootDir, project)).join(', ')}\n`);
|
|
410
|
+
output.write('Настройки будут запрошены один раз и записаны одинаково во все найденные проекты.\n');
|
|
411
|
+
const sharedSettings = await askSettings({ input, output });
|
|
412
|
+
const sharedSettingsContent = formatSettings(sharedSettings);
|
|
408
413
|
|
|
409
414
|
const updatedProjects = [];
|
|
410
415
|
const skippedProjects = [];
|
|
@@ -423,6 +428,8 @@ export async function updateAll({
|
|
|
423
428
|
}
|
|
424
429
|
|
|
425
430
|
output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
|
|
431
|
+
await writeSettingsContent(projectDir, sharedSettingsContent);
|
|
432
|
+
output.write(`Записан общий файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
426
433
|
const result = await syncPackage(projectDir, targets, output, {
|
|
427
434
|
action: 'update',
|
|
428
435
|
writeDone: false
|
|
@@ -479,7 +486,7 @@ export async function askTargets({ input = process.stdin, output = process.stdou
|
|
|
479
486
|
|
|
480
487
|
export async function askSettings({ input = process.stdin, output = process.stdout } = {}) {
|
|
481
488
|
if (!input.isTTY || !output.isTTY) {
|
|
482
|
-
output.write(`Нет интерактивного терминала —
|
|
489
|
+
output.write(`Нет интерактивного терминала — использую настройки ${SETTINGS_RELATIVE_PATH} по умолчанию.\n`);
|
|
483
490
|
return structuredClone(DEFAULT_SETTINGS);
|
|
484
491
|
}
|
|
485
492
|
|
|
@@ -784,18 +791,183 @@ async function findInstalledProjects(rootDir, maxDepth) {
|
|
|
784
791
|
}
|
|
785
792
|
|
|
786
793
|
async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
794
|
+
const existing = await migrateSettingsIfNeeded(cwd, { output });
|
|
795
|
+
if (existing.exists) return;
|
|
796
|
+
|
|
797
|
+
const settings = await askSettings({ input, output });
|
|
798
|
+
await writeSettingsContent(cwd, formatSettings(settings));
|
|
799
|
+
output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function configureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
803
|
+
output.write(`Настраиваю ${SETTINGS_RELATIVE_PATH} заново.\n`);
|
|
804
|
+
const settings = await askSettings({ input, output });
|
|
805
|
+
await writeSettingsContent(cwd, formatSettings(settings));
|
|
806
|
+
output.write(`Записан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function writeSettingsContent(cwd, content) {
|
|
787
810
|
const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
|
|
811
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
788
812
|
if (await fileExists(settingsPath)) {
|
|
789
|
-
|
|
790
|
-
output.write('Существующий файл не перезаписываю. Актуальный формат:\n\n');
|
|
791
|
-
output.write(formatSettings(DEFAULT_SETTINGS));
|
|
813
|
+
await writeFileAtomically(settingsPath, content);
|
|
792
814
|
return;
|
|
793
815
|
}
|
|
816
|
+
await fs.writeFile(settingsPath, content);
|
|
817
|
+
}
|
|
794
818
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
819
|
+
async function migrateSettingsIfNeeded(cwd, {
|
|
820
|
+
output = process.stdout,
|
|
821
|
+
reportExisting = true
|
|
822
|
+
} = {}) {
|
|
823
|
+
const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
|
|
824
|
+
const content = await readFileIfExists(settingsPath);
|
|
825
|
+
if (content === null) return { exists: false, migrated: false };
|
|
826
|
+
|
|
827
|
+
const version = readTopLevelYamlScalar(content, 'version');
|
|
828
|
+
if (version !== '1') {
|
|
829
|
+
if (reportExisting) {
|
|
830
|
+
output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
831
|
+
if (version !== '2') {
|
|
832
|
+
output.write('Автомиграция не выполнена: безопасно поддерживается только переход version: 1 → version: 2.\n');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return { exists: true, migrated: false, version };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const migratedSettings = migrateSettingsV1(content);
|
|
839
|
+
await writeFileAtomically(settingsPath, formatSettings(migratedSettings));
|
|
840
|
+
output.write(`Мигрированы настройки ${SETTINGS_RELATIVE_PATH}: version: 1 → version: 2.\n`);
|
|
841
|
+
return { exists: true, migrated: true, version: '2' };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function migrateSettingsV1(content) {
|
|
845
|
+
const defaultsStrict = readLegacyBoolean(content, 'defaults', 'strict', false);
|
|
846
|
+
const decisionMode = readLegacyEnum(
|
|
847
|
+
content,
|
|
848
|
+
'defaults',
|
|
849
|
+
'decision_mode',
|
|
850
|
+
['autonomous', 'recommend_and_ask', 'ask_each_time'],
|
|
851
|
+
DEFAULT_SETTINGS.explore.decisionMode
|
|
852
|
+
);
|
|
853
|
+
const reviewAgents = structuredClone(DEFAULT_REVIEW_AGENTS);
|
|
854
|
+
reviewAgents.code_quality.mode = readLegacyBoolean(
|
|
855
|
+
content,
|
|
856
|
+
'review',
|
|
857
|
+
'include_code_quality',
|
|
858
|
+
true
|
|
859
|
+
) ? 'always' : 'off';
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
...structuredClone(DEFAULT_SETTINGS),
|
|
863
|
+
explore: {
|
|
864
|
+
strict: defaultsStrict,
|
|
865
|
+
decisionMode
|
|
866
|
+
},
|
|
867
|
+
plan: {
|
|
868
|
+
strict: defaultsStrict,
|
|
869
|
+
size: readLegacyEnum(
|
|
870
|
+
content,
|
|
871
|
+
'defaults',
|
|
872
|
+
'plan_size',
|
|
873
|
+
['normal', 'short', 'ask_each_time'],
|
|
874
|
+
DEFAULT_SETTINGS.plan.size
|
|
875
|
+
),
|
|
876
|
+
decisionMode,
|
|
877
|
+
testStrategy: readLegacyEnum(
|
|
878
|
+
content,
|
|
879
|
+
'defaults',
|
|
880
|
+
'test_strategy',
|
|
881
|
+
['after_each_phase', 'tdd_each_phase', 'end_of_plan', 'ask_each_time'],
|
|
882
|
+
DEFAULT_SETTINGS.plan.testStrategy
|
|
883
|
+
),
|
|
884
|
+
loggingStrategy: readLegacyEnum(
|
|
885
|
+
content,
|
|
886
|
+
'defaults',
|
|
887
|
+
'logging_strategy',
|
|
888
|
+
['debug_precise', 'standard', 'ask_each_time'],
|
|
889
|
+
DEFAULT_SETTINGS.plan.loggingStrategy
|
|
890
|
+
)
|
|
891
|
+
},
|
|
892
|
+
planPolish: {
|
|
893
|
+
strict: defaultsStrict
|
|
894
|
+
},
|
|
895
|
+
review: {
|
|
896
|
+
agents: reviewAgents
|
|
897
|
+
},
|
|
898
|
+
discoverAutomations: {
|
|
899
|
+
includePlans: readLegacyBoolean(content, 'automate', 'include_plans', false)
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function readLegacyBoolean(content, section, key, fallback) {
|
|
905
|
+
const value = readNestedYamlScalar(content, section, key);
|
|
906
|
+
if (value === 'true') return true;
|
|
907
|
+
if (value === 'false') return false;
|
|
908
|
+
return fallback;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function readLegacyEnum(content, section, key, allowedValues, fallback) {
|
|
912
|
+
const value = readNestedYamlScalar(content, section, key);
|
|
913
|
+
return allowedValues.includes(value) ? value : fallback;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function readTopLevelYamlScalar(content, key) {
|
|
917
|
+
for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
918
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
919
|
+
if (match?.[1] === key) return normalizeYamlScalar(match[2]);
|
|
920
|
+
}
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function readNestedYamlScalar(content, section, key) {
|
|
925
|
+
let currentSection = null;
|
|
926
|
+
for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
927
|
+
if (/^\s*(?:#.*)?$/.test(line)) continue;
|
|
928
|
+
|
|
929
|
+
const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*(?:#.*)?$/);
|
|
930
|
+
if (sectionMatch) {
|
|
931
|
+
currentSection = sectionMatch[1];
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (/^\S/.test(line)) {
|
|
936
|
+
currentSection = null;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (currentSection !== section) continue;
|
|
941
|
+
const valueMatch = line.match(/^ {2}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
942
|
+
if (valueMatch?.[1] === key) return normalizeYamlScalar(valueMatch[2]);
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function normalizeYamlScalar(value) {
|
|
948
|
+
const withoutComment = value.replace(/\s+#.*$/, '').trim();
|
|
949
|
+
if (
|
|
950
|
+
withoutComment.length >= 2
|
|
951
|
+
&& ((withoutComment.startsWith('"') && withoutComment.endsWith('"'))
|
|
952
|
+
|| (withoutComment.startsWith("'") && withoutComment.endsWith("'")))
|
|
953
|
+
) {
|
|
954
|
+
return withoutComment.slice(1, -1);
|
|
955
|
+
}
|
|
956
|
+
return withoutComment;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function writeFileAtomically(filePath, content) {
|
|
960
|
+
const currentStat = await fs.stat(filePath);
|
|
961
|
+
const tempPath = path.join(
|
|
962
|
+
path.dirname(filePath),
|
|
963
|
+
`.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`
|
|
964
|
+
);
|
|
965
|
+
try {
|
|
966
|
+
await fs.writeFile(tempPath, content, { mode: currentStat.mode & 0o777 });
|
|
967
|
+
await fs.rename(tempPath, filePath);
|
|
968
|
+
} finally {
|
|
969
|
+
await fs.rm(tempPath, { force: true });
|
|
970
|
+
}
|
|
799
971
|
}
|
|
800
972
|
|
|
801
973
|
function formatSettings(settings) {
|