@gian-tiaga/eda 1.0.0 → 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 +8 -6
- package/README.md +3 -3
- package/lib/install.js +158 -6
- 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.1`, установщик обновляет управляемые компоненты и автоматически переводит существующий `docs/settings.yaml` версии 1 в актуальный формат версии 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` ищет проекты на глубине до двух уровней, обновляет только уже найденные среды Claude/Codex и не создаёт
|
|
29
|
+
`update-all` ищет проекты на глубине до двух уровней, обновляет только уже найденные среды Claude/Codex и не создаёт отсутствующий `docs/settings.yaml`. Если конфиг существует с `version: 1`, команда автоматически мигрирует его на v2.
|
|
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 init`, `eda update` и `eda update-all` распознают `version: 1`, переносят поддерживаемые значения, добавляют новые настройки с defaults и атомарно заменяют файл. Повторных интерактивных вопросов при миграции нет.
|
|
61
|
+
|
|
62
|
+
Существующий v2 сохраняется без изменений. Файл без версии или с неизвестной версией установщик не переписывает, чтобы не повредить неподдерживаемый формат. Поля вне документированной схемы v1 в новый файл не переносятся.
|
|
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` установщик добавляет автоматически из сбалансированных 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`, но автоматически переводит найденный конфиг с `version: 1` на `version: 2`.
|
|
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` и `eda update` создают `docs/settings.yaml`, только если файла ещё нет. Существующий
|
|
350
|
+
`eda init` и `eda update` создают `docs/settings.yaml`, только если файла ещё нет. Существующий v2 сохраняется без изменений, а v1 автоматически и атомарно переписывается в v2 с переносом поддерживаемых значений. Файл неизвестной версии установщик не меняет. `eda update-all` не создаёт отсутствующие настройки, но также мигрирует найденный v1.
|
|
351
351
|
|
|
352
352
|
---
|
|
353
353
|
|
|
@@ -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` идемпотентно синхронизирует обе группы компонентов и отдельно показывает реально изменившиеся скилы и агенты. Manifest хранит только идентификаторы компонентов, которыми владеет `eda`: благодаря этому retired-компоненты удаляются, а чужие скилы и агенты остаются нетронутыми. `eda update-all [dir]` делает то же самое для каждого найденного проекта, не создаёт отсутствующий `docs/settings.yaml`, но переводит найденный v1 в v2. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
|
|
460
460
|
|
|
461
461
|
Установленные копии не являются источником правды. Локальные изменения внутри `.claude/skills/eda-*`, `.codex/skills/eda-*`, `.claude/agents/eda-*` или `.codex/agents/eda-*` будут заменены следующим обновлением.
|
|
462
462
|
|
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';
|
|
@@ -423,6 +424,7 @@ export async function updateAll({
|
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
|
|
427
|
+
await migrateSettingsIfNeeded(projectDir, { output, reportExisting: false });
|
|
426
428
|
const result = await syncPackage(projectDir, targets, output, {
|
|
427
429
|
action: 'update',
|
|
428
430
|
writeDone: false
|
|
@@ -785,12 +787,8 @@ async function findInstalledProjects(rootDir, maxDepth) {
|
|
|
785
787
|
|
|
786
788
|
async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
787
789
|
const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
output.write('Существующий файл не перезаписываю. Актуальный формат:\n\n');
|
|
791
|
-
output.write(formatSettings(DEFAULT_SETTINGS));
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
790
|
+
const existing = await migrateSettingsIfNeeded(cwd, { output });
|
|
791
|
+
if (existing.exists) return;
|
|
794
792
|
|
|
795
793
|
const settings = await askSettings({ input, output });
|
|
796
794
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
@@ -798,6 +796,160 @@ async function ensureSettings(cwd, { input = process.stdin, output = process.std
|
|
|
798
796
|
output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
|
|
799
797
|
}
|
|
800
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
|
+
|
|
801
953
|
function formatSettings(settings) {
|
|
802
954
|
const orhestra = settings.orhestra ?? DEFAULT_SETTINGS.orhestra;
|
|
803
955
|
const aim = settings.aim ?? DEFAULT_SETTINGS.aim;
|