@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Миграция на eda 1.0
2
2
 
3
- Версия 1.0 переводит `eda` с набора файлов-скилов на пакет с каноническими скилами и специализированными агентами для Claude Code и Codex CLI. Установщик обновляет управляемые компоненты автоматически, но существующий `docs/settings.yaml` версии 1 нужно перенести вручную.
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.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 и не создаёт и не меняет `docs/settings.yaml`.
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`. Установщик намеренно сохраняет существующий файл без изменений, поэтому файл с `version: 1` нужно отредактировать вручную.
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` можно взять из полного актуального примера в [README](README.md). Если раздел или значение отсутствует, соответствующий скил использует документированный default.
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. Команда обновляет только найденные среды и не создаёт `docs/settings.yaml`, чтобы массовый запуск не добавлял новые файлы в проекты.
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`, только если файла ещё нет. Существующий файл, включая v1, не перезаписывается; его нужно перевести вручную. `eda update-all` настройки не создаёт и не меняет.
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]` делает то же самое для каждого найденного проекта, но не создаёт `docs/settings.yaml`. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
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
- if (await fileExists(settingsPath)) {
789
- output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gian-tiaga/eda",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Пакет скилов и специализированных агентов eda-* для Claude Code и Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {