@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 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.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.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` ищет проекты на глубине до двух уровней, один раз спрашивает полный профиль настроек и записывает одинаковый `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`. Установщик намеренно сохраняет существующий файл без изменений, поэтому файл с `version: 1` нужно отредактировать вручную.
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` можно взять из полного актуального примера в [README](README.md). Если раздел или значение отсутствует, соответствующий скил использует документированный default.
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. Команда обновляет только найденные среды и не создаёт `docs/settings.yaml`, чтобы массовый запуск не добавлял новые файлы в проекты.
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` и `eda update` создают `docs/settings.yaml`, только если файла ещё нет. Существующий файл, включая v1, не перезаписывается; его нужно перевести вручную. `eda update-all` настройки не создаёт и не меняет.
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 # обновить скилы и агенты, создать docs/settings.yaml, если его ещё нет
426
- eda update-all [dir] # обновить пакет во всех найденных проектах внутри dir на глубине 2
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]` делает то же самое для каждого найденного проекта, но не создаёт `docs/settings.yaml`. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
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] — обновить пакет во всех найденных проектах внутри 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 ensureSettings(cwd, { input, output });
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(`Нет интерактивного терминала — создаю ${SETTINGS_RELATIVE_PATH} с настройками по умолчанию.\n`);
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
- output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
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
- const settings = await askSettings({ input, output });
796
- await fs.mkdir(path.dirname(settingsPath), { recursive: true });
797
- await fs.writeFile(settingsPath, formatSettings(settings));
798
- output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
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) {
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.2",
4
4
  "description": "Пакет скилов и специализированных агентов eda-* для Claude Code и Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {