@gian-tiaga/eda 1.0.4 → 1.0.5

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. Начиная с `1.0.2`, `eda update` заново спрашивает полный профиль настроек и записывает актуальный `docs/settings.yaml` версии 2 перед обновлением компонентов.
3
+ Версия 1.0 переводит `eda` с набора файлов-скилов на пакет с каноническими скилами и специализированными агентами для Claude Code и Codex CLI. `eda update` сохраняет полный `docs/settings.yaml` версии 2 без вопросов, а в неполном v2 спрашивает и атомарно дописывает только отсутствующие разделы перед обновлением компонентов.
4
4
 
5
5
  ## Короткий путь
6
6
 
@@ -26,7 +26,7 @@ eda update
26
26
  eda update-all /path/to/projects
27
27
  ```
28
28
 
29
- `update-all` ищет проекты на глубине до двух уровней, один раз спрашивает полный профиль настроек и записывает одинаковый `docs/settings.yaml` версии 2 во все найденные проекты. В non-TTY запуске вопросы не задаются: во все проекты записывается один профиль defaults.
29
+ `update-all` ищет проекты на глубине до двух уровней и предлагает два режима. `configure` один раз спрашивает полный профиль и записывает одинаковый `docs/settings.yaml` версии 2 во все найденные проекты. `skip` не задаёт вопросов, сохраняет существующие конфиги и создаёт профиль defaults только в проектах без файла. В non-TTY запуске используется `skip`.
30
30
 
31
31
  После обновления перезапусти Claude Code или Codex CLI, чтобы среда перечитала установленные скилы и агентов.
32
32
 
package/README.md CHANGED
@@ -23,13 +23,15 @@ eda init
23
23
 
24
24
  Установщик покажет чекбоксы: стрелками выбираешь среду, пробелом отмечаешь, Enter продолжает. Можно поставить Claude Code, Codex CLI или обе среды сразу. Устанавливаются и скилы, и агенты. Если `docs/settings.yaml` ещё нет, установщик предложит настройки скилов, а для каждой review-проверки отдельно спросит режим и модели Claude/Codex. В неинтерактивном запуске записывается сбалансированный профиль. В конце `eda init` отдельно пишет, какие скилы и агенты реально установлены или изменились.
25
25
 
26
+ `eda update` не перенастраивает полный v2-конфиг: если все обязательные разделы уже есть, команда обновляет пакет без вопросов и сохраняет `docs/settings.yaml` байт-в-байт. Если разделов не хватает, спрашиваются только они и атомарно дописываются в существующий файл; без TTY для них используются defaults. Для отсутствующего файла или версии, отличной от 2, собирается полный v2-профиль.
27
+
26
28
  Если пакет уже стоит во многих проектах, можно обновить его одной командой:
27
29
 
28
30
  ```bash
29
31
  eda update-all ~/Code
30
32
  ```
31
33
 
32
- `eda update-all` ищет проекты в указанной директории, её прямых подпапках и подпапках второго уровня. Проект считается найденным, если в нём уже есть установленный скил, агент или manifest `eda` для Claude/Codex. Перед обновлением команда один раз спрашивает полный профиль настроек и записывает одинаковый `docs/settings.yaml` во все найденные проекты. В неинтерактивном запуске во все проекты записывается один профиль по умолчанию.
34
+ `eda update-all` ищет проекты в указанной директории, её прямых подпапках и подпапках второго уровня. Проект считается найденным, если в нём уже есть установленный скил, агент или manifest `eda` для Claude/Codex. Перед обновлением команда предлагает настроить один общий профиль для всех проектов или выбрать `skip`. В режиме `skip` вопросы не задаются, существующие `docs/settings.yaml` сохраняются байт-в-байт, а полный профиль defaults создаётся только в проектах без файла. Без TTY автоматически используется `skip`.
33
35
 
34
36
  ---
35
37
 
@@ -346,7 +348,7 @@ discover-automations:
346
348
 
347
349
  Поддерживается только `version: 2`. Для старого v2 без `review.agents` новый review использует сбалансированные defaults и предупреждает, что устаревшие `review.strict`, `review.include_code_quality` и `review-check` игнорируются. Установщик существующий файл не переписывает.
348
350
 
349
- `eda init` создаёт `docs/settings.yaml`, только если файла ещё нет; существующий v1 автоматически мигрируется в v2, а v2 и неизвестные версии сохраняются. `eda update` всегда заново задаёт все вопросы конфигурации и атомарно перезаписывает файл полным v2-профилем. `eda update-all` задаёт тот же набор вопросов один раз и записывает получившийся общий конфиг во все найденные проекты. Без TTY обе update-команды используют defaults.
351
+ `eda init` создаёт `docs/settings.yaml`, только если файла ещё нет; существующий v1 автоматически мигрируется в v2, а v2 и неизвестные версии сохраняются. `eda update` сохраняет полный v2-файл без вопросов, а в неполный v2 атомарно дописывает только отсутствующие разделы после вопросов только по ним; без TTY используются defaults недостающих разделов. Для отсутствующего файла или другой версии команда заново собирает полный v2-профиль. `eda update-all` предлагает общий профиль с перезаписью или `skip`, который сохраняет существующие файлы и создаёт defaults только при их отсутствии. Без TTY используется `skip`.
350
352
 
351
353
  ---
352
354
 
@@ -421,8 +423,8 @@ discover-automations может запускаться от review, fix-by-revie
421
423
 
422
424
  ```bash
423
425
  eda init # выбрать Claude Code / Codex / обе среды и установить пакет
424
- eda update # заново настроить конфиг и обновить скилы и агенты
425
- eda update-all [dir] # один раз настроить конфиг и обновить все найденные проекты на глубине 2
426
+ eda update # дополнить недостающие настройки и обновить скилы и агенты
427
+ eda update-all [dir] # сохранить конфиги или настроить общий и обновить проекты на глубине 2
426
428
  eda --version # показать версию установленного пакета
427
429
  eda --help # справка
428
430
  ```
@@ -455,7 +457,7 @@ eda update-all ~/Code
455
457
  | Агент | `.claude/agents/<agent>.md` | `.codex/agents/<agent>.toml` |
456
458
  | Manifest владения | `.claude/eda-manifest.json` | `.codex/eda-manifest.json` |
457
459
 
458
- `eda update` заново собирает полный v2-конфиг, затем идемпотентно синхронизирует обе группы компонентов и отдельно показывает реально изменившиеся скилы и агенты. Manifest хранит только идентификаторы компонентов, которыми владеет `eda`: благодаря этому retired-компоненты удаляются, а чужие скилы и агенты остаются нетронутыми. `eda update-all [dir]` один раз собирает общий конфиг, записывает его во все найденные проекты и синхронизирует компоненты каждого проекта. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
460
+ `eda update` сохраняет полный v2-конфиг как есть или дополняет только отсутствующие разделы, затем идемпотентно синхронизирует обе группы компонентов и отдельно показывает реально изменившиеся скилы и агенты. Manifest хранит только идентификаторы компонентов, которыми владеет `eda`: благодаря этому retired-компоненты удаляются, а чужие скилы и агенты остаются нетронутыми. `eda update-all [dir]` либо один раз собирает и записывает общий конфиг, либо в режиме `skip` сохраняет существующие настройки и создаёт defaults только при отсутствии файла; после этого синхронизирует компоненты каждого проекта. Старые Codex-файлы `.codex/skills/<skill>.md` также удаляются при миграции.
459
461
 
460
462
  Установленные копии не являются источником правды. Локальные изменения внутри `.claude/skills/eda-*`, `.codex/skills/eda-*`, `.claude/agents/eda-*` или `.codex/agents/eda-*` будут заменены следующим обновлением.
461
463
 
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
@@ -229,28 +229,43 @@ const DEFAULT_SETTINGS = {
229
229
  includePlans: false
230
230
  }
231
231
  };
232
+ const SETTINGS_SECTION_NAMES = [
233
+ 'orhestra',
234
+ 'aim',
235
+ 'explore',
236
+ 'plan',
237
+ 'plan-polish',
238
+ 'review',
239
+ 'send-review',
240
+ 'discover-automations'
241
+ ];
232
242
  const SETTINGS_CHOICES = [
233
243
  {
244
+ section: 'explore',
234
245
  value: 'exploreStrict',
235
246
  name: 'Strict по умолчанию для eda-explore',
236
247
  checked: DEFAULT_SETTINGS.explore.strict
237
248
  },
238
249
  {
250
+ section: 'plan',
239
251
  value: 'planStrict',
240
252
  name: 'Strict по умолчанию для eda-plan',
241
253
  checked: DEFAULT_SETTINGS.plan.strict
242
254
  },
243
255
  {
256
+ section: 'plan-polish',
244
257
  value: 'planPolishStrict',
245
258
  name: 'Strict по умолчанию для eda-plan-polish',
246
259
  checked: DEFAULT_SETTINGS.planPolish.strict
247
260
  },
248
261
  {
262
+ section: 'send-review',
249
263
  value: 'sendReviewClosePreviousReviews',
250
264
  name: 'Закрывать предыдущие ревью eda-send-review',
251
265
  checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
252
266
  },
253
267
  {
268
+ section: 'discover-automations',
254
269
  value: 'discoverAutomationsIncludePlans',
255
270
  name: 'Анализировать планы в eda-discover-automations по умолчанию',
256
271
  checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
@@ -355,6 +370,16 @@ const CODEX_REVIEW_MODEL_CHOICES = [
355
370
  { value: 'gpt-5.6-terra', name: 'gpt-5.6-terra — сильная проверка кода' },
356
371
  { value: 'gpt-5.6-sol', name: 'gpt-5.6-sol — максимальная глубина' }
357
372
  ];
373
+ const UPDATE_ALL_SETTINGS_MODE_CHOICES = [
374
+ {
375
+ value: 'configure',
376
+ name: 'Настроить один общий профиль и записать его во все проекты'
377
+ },
378
+ {
379
+ value: 'skip',
380
+ name: 'Skip — сохранить существующие конфиги, defaults создать только там, где файла нет'
381
+ }
382
+ ];
358
383
 
359
384
  export async function init({ cwd, input = process.stdin, output = process.stdout }) {
360
385
  const targets = await askTargets({ input, output });
@@ -381,7 +406,8 @@ export async function updateAll({
381
406
  root = process.cwd(),
382
407
  input = process.stdin,
383
408
  output = process.stdout,
384
- maxDepth = UPDATE_ALL_MAX_DEPTH
409
+ maxDepth = UPDATE_ALL_MAX_DEPTH,
410
+ settingsMode
385
411
  } = {}) {
386
412
  if (!Number.isInteger(maxDepth) || maxDepth < 0) {
387
413
  throw new Error('Глубина поиска должна быть неотрицательным целым числом.');
@@ -409,9 +435,20 @@ export async function updateAll({
409
435
  }
410
436
 
411
437
  output.write(`Найдено ${formatProjectCount(projects.length)}: ${projects.map(project => formatProjectPath(rootDir, project)).join(', ')}\n`);
412
- output.write('Настройки будут запрошены один раз и записаны одинаково во все найденные проекты.\n');
413
- const sharedSettings = await askSettings({ input, output });
414
- const sharedSettingsContent = formatSettings(sharedSettings);
438
+ const resolvedSettingsMode = settingsMode ?? await askUpdateAllSettingsMode({ input, output });
439
+ if (!['configure', 'skip'].includes(resolvedSettingsMode)) {
440
+ throw new Error(`Неизвестный режим настроек update-all: ${resolvedSettingsMode}.`);
441
+ }
442
+
443
+ let sharedSettingsContent;
444
+ if (resolvedSettingsMode === 'configure') {
445
+ output.write('Настройки будут запрошены один раз и записаны одинаково во все найденные проекты.\n');
446
+ const sharedSettings = await askSettings({ input, output });
447
+ sharedSettingsContent = formatSettings(sharedSettings);
448
+ } else {
449
+ output.write(`Режим skip: существующие ${SETTINGS_RELATIVE_PATH} сохраняются, defaults создаются только для проектов без файла.\n`);
450
+ sharedSettingsContent = formatSettings(structuredClone(DEFAULT_SETTINGS));
451
+ }
415
452
 
416
453
  const updatedProjects = [];
417
454
  const skippedProjects = [];
@@ -430,8 +467,15 @@ export async function updateAll({
430
467
  }
431
468
 
432
469
  output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
433
- await writeSettingsContent(projectDir, sharedSettingsContent);
434
- output.write(`Записан общий файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
470
+ const projectSettingsPath = path.join(projectDir, SETTINGS_RELATIVE_PATH);
471
+ if (resolvedSettingsMode === 'skip' && await fileExists(projectSettingsPath)) {
472
+ output.write(`Существующий файл настроек сохранён: ${SETTINGS_RELATIVE_PATH}\n`);
473
+ } else {
474
+ await writeSettingsContent(projectDir, sharedSettingsContent);
475
+ output.write(resolvedSettingsMode === 'skip'
476
+ ? `Создан файл настроек с defaults: ${SETTINGS_RELATIVE_PATH}\n`
477
+ : `Записан общий файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
478
+ }
435
479
  const result = await syncPackage(projectDir, targets, output, {
436
480
  action: 'update',
437
481
  writeDone: false
@@ -459,6 +503,7 @@ export async function updateAll({
459
503
  return {
460
504
  root: rootDir,
461
505
  maxDepth,
506
+ settingsMode: resolvedSettingsMode,
462
507
  projects,
463
508
  updatedProjects,
464
509
  skippedProjects,
@@ -486,45 +531,79 @@ export async function askTargets({ input = process.stdin, output = process.stdou
486
531
  });
487
532
  }
488
533
 
489
- export async function askSettings({ input = process.stdin, output = process.stdout } = {}) {
534
+ export async function askUpdateAllSettingsMode({
535
+ input = process.stdin,
536
+ output = process.stdout,
537
+ selectPrompt = select
538
+ } = {}) {
490
539
  if (!input.isTTY || !output.isTTY) {
491
- output.write(`Нет интерактивного терминала — использую настройки ${SETTINGS_RELATIVE_PATH} по умолчанию.\n`);
492
- return structuredClone(DEFAULT_SETTINGS);
540
+ output.write(`Нет интерактивного терминала — использую режим skip: сохраняю существующие ${SETTINGS_RELATIVE_PATH}, defaults создаю только при отсутствии файла.\n`);
541
+ return 'skip';
493
542
  }
494
543
 
495
- const selected = await checkbox({
496
- message: 'Какие настройки включить?',
497
- instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
498
- choices: SETTINGS_CHOICES
544
+ return selectPrompt({
545
+ message: `Как обновить ${SETTINGS_RELATIVE_PATH} в найденных проектах?`,
546
+ choices: UPDATE_ALL_SETTINGS_MODE_CHOICES,
547
+ default: 'configure'
499
548
  }, {
500
549
  input,
501
550
  output
502
551
  });
552
+ }
503
553
 
504
- const orhestraMode = await select({
505
- message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
506
- choices: ORHESTRA_MODE_CHOICES,
507
- default: DEFAULT_SETTINGS.orhestra.mode
508
- }, {
509
- input,
510
- output
511
- });
554
+ export async function askSettings({
555
+ input = process.stdin,
556
+ output = process.stdout,
557
+ sections = SETTINGS_SECTION_NAMES,
558
+ checkboxPrompt = checkbox,
559
+ selectPrompt = select
560
+ } = {}) {
561
+ if (!input.isTTY || !output.isTTY) {
562
+ output.write(`Нет интерактивного терминала — использую настройки ${SETTINGS_RELATIVE_PATH} по умолчанию.\n`);
563
+ return structuredClone(DEFAULT_SETTINGS);
564
+ }
512
565
 
513
- const enabledOrhestraSteps = await checkbox({
514
- message: 'Какие этапы eda-orhestra включить по умолчанию?',
515
- instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
516
- choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
517
- name: step.name,
518
- value: step.skill,
519
- checked: step.enabled
520
- }))
521
- }, {
522
- input,
523
- output
524
- });
566
+ const requestedSections = new Set(sections);
567
+ const booleanChoices = SETTINGS_CHOICES.filter(choice => requestedSections.has(choice.section));
568
+ const selected = booleanChoices.length > 0
569
+ ? await checkboxPrompt({
570
+ message: 'Какие настройки включить?',
571
+ instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
572
+ choices: booleanChoices.map(({ section, ...choice }) => choice)
573
+ }, {
574
+ input,
575
+ output
576
+ })
577
+ : [];
578
+
579
+ const orhestraMode = requestedSections.has('orhestra')
580
+ ? await selectPrompt({
581
+ message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
582
+ choices: ORHESTRA_MODE_CHOICES,
583
+ default: DEFAULT_SETTINGS.orhestra.mode
584
+ }, {
585
+ input,
586
+ output
587
+ })
588
+ : DEFAULT_SETTINGS.orhestra.mode;
589
+
590
+ const enabledOrhestraSteps = requestedSections.has('orhestra')
591
+ ? await checkboxPrompt({
592
+ message: 'Какие этапы eda-orhestra включить по умолчанию?',
593
+ instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
594
+ choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
595
+ name: step.name,
596
+ value: step.skill,
597
+ checked: step.enabled
598
+ }))
599
+ }, {
600
+ input,
601
+ output
602
+ })
603
+ : DEFAULT_ORHESTRA_STEPS.filter(step => step.enabled).map(step => step.skill);
525
604
 
526
- const polishLimit = enabledOrhestraSteps.includes('eda-polish')
527
- ? await select({
605
+ const polishLimit = requestedSections.has('orhestra') && enabledOrhestraSteps.includes('eda-polish')
606
+ ? await selectPrompt({
528
607
  message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
529
608
  choices: ORHESTRA_POLISH_LIMIT_CHOICES,
530
609
  default: 5
@@ -540,61 +619,75 @@ export async function askSettings({ input = process.stdin, output = process.stdo
540
619
  ...(step.skill === 'eda-polish' ? { args: `limit ${polishLimit}` } : {})
541
620
  }));
542
621
 
543
- const aimMode = await select({
544
- message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
545
- choices: AIM_MODE_CHOICES,
546
- default: DEFAULT_SETTINGS.aim.mode
547
- }, {
548
- input,
549
- output
550
- });
622
+ const aimMode = requestedSections.has('aim')
623
+ ? await selectPrompt({
624
+ message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
625
+ choices: AIM_MODE_CHOICES,
626
+ default: DEFAULT_SETTINGS.aim.mode
627
+ }, {
628
+ input,
629
+ output
630
+ })
631
+ : DEFAULT_SETTINGS.aim.mode;
551
632
 
552
- const exploreDecisionMode = await select({
553
- message: 'Как принимать важные решения в eda-explore?',
554
- choices: DECISION_MODE_CHOICES,
555
- default: DEFAULT_SETTINGS.explore.decisionMode
556
- }, {
557
- input,
558
- output
559
- });
633
+ const exploreDecisionMode = requestedSections.has('explore')
634
+ ? await selectPrompt({
635
+ message: 'Как принимать важные решения в eda-explore?',
636
+ choices: DECISION_MODE_CHOICES,
637
+ default: DEFAULT_SETTINGS.explore.decisionMode
638
+ }, {
639
+ input,
640
+ output
641
+ })
642
+ : DEFAULT_SETTINGS.explore.decisionMode;
560
643
 
561
- const planSize = await select({
562
- message: 'Какой размер плана eda-plan использовать по умолчанию?',
563
- choices: PLAN_SIZE_CHOICES,
564
- default: DEFAULT_SETTINGS.plan.size
565
- }, {
566
- input,
567
- output
568
- });
644
+ const planSize = requestedSections.has('plan')
645
+ ? await selectPrompt({
646
+ message: 'Какой размер плана eda-plan использовать по умолчанию?',
647
+ choices: PLAN_SIZE_CHOICES,
648
+ default: DEFAULT_SETTINGS.plan.size
649
+ }, {
650
+ input,
651
+ output
652
+ })
653
+ : DEFAULT_SETTINGS.plan.size;
569
654
 
570
- const planDecisionMode = await select({
571
- message: 'Как принимать важные решения в eda-plan?',
572
- choices: DECISION_MODE_CHOICES,
573
- default: DEFAULT_SETTINGS.plan.decisionMode
574
- }, {
575
- input,
576
- output
577
- });
655
+ const planDecisionMode = requestedSections.has('plan')
656
+ ? await selectPrompt({
657
+ message: 'Как принимать важные решения в eda-plan?',
658
+ choices: DECISION_MODE_CHOICES,
659
+ default: DEFAULT_SETTINGS.plan.decisionMode
660
+ }, {
661
+ input,
662
+ output
663
+ })
664
+ : DEFAULT_SETTINGS.plan.decisionMode;
578
665
 
579
- const testStrategy = await select({
580
- message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
581
- choices: TEST_STRATEGY_CHOICES,
582
- default: DEFAULT_SETTINGS.plan.testStrategy
583
- }, {
584
- input,
585
- output
586
- });
666
+ const testStrategy = requestedSections.has('plan')
667
+ ? await selectPrompt({
668
+ message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
669
+ choices: TEST_STRATEGY_CHOICES,
670
+ default: DEFAULT_SETTINGS.plan.testStrategy
671
+ }, {
672
+ input,
673
+ output
674
+ })
675
+ : DEFAULT_SETTINGS.plan.testStrategy;
587
676
 
588
- const loggingStrategy = await select({
589
- message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
590
- choices: LOGGING_STRATEGY_CHOICES,
591
- default: DEFAULT_SETTINGS.plan.loggingStrategy
592
- }, {
593
- input,
594
- output
595
- });
677
+ const loggingStrategy = requestedSections.has('plan')
678
+ ? await selectPrompt({
679
+ message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
680
+ choices: LOGGING_STRATEGY_CHOICES,
681
+ default: DEFAULT_SETTINGS.plan.loggingStrategy
682
+ }, {
683
+ input,
684
+ output
685
+ })
686
+ : DEFAULT_SETTINGS.plan.loggingStrategy;
596
687
 
597
- const reviewAgents = await askReviewAgentSettings({ input, output });
688
+ const reviewAgents = requestedSections.has('review')
689
+ ? await askReviewAgentSettings({ input, output, selectPrompt })
690
+ : structuredClone(DEFAULT_REVIEW_AGENTS);
598
691
 
599
692
  return {
600
693
  orhestra: {
@@ -802,6 +895,26 @@ async function ensureSettings(cwd, { input = process.stdin, output = process.std
802
895
  }
803
896
 
804
897
  async function configureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
898
+ const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
899
+ const existingContent = await readFileIfExists(settingsPath);
900
+ if (existingContent !== null && readTopLevelYamlScalar(existingContent, 'version') === '2') {
901
+ const presentSections = readTopLevelYamlKeys(existingContent);
902
+ const missingSections = SETTINGS_SECTION_NAMES.filter(section => !presentSections.has(section));
903
+ if (missingSections.length === 0) {
904
+ output.write(`Настройки ${SETTINGS_RELATIVE_PATH} версии 2 полные — вопросы не требуются.\n`);
905
+ return;
906
+ }
907
+
908
+ output.write(`Дополняю отсутствующие разделы ${SETTINGS_RELATIVE_PATH}: ${missingSections.join(', ')}.\n`);
909
+ const settings = await askSettings({ input, output, sections: missingSections });
910
+ const missingContent = extractSettingsSections(formatSettings(settings), missingSections);
911
+ const separator = existingContent.endsWith('\n\n') ? '' : existingContent.endsWith('\n') ? '\n' : '\n\n';
912
+ const updatedContent = `${existingContent}${separator}${missingContent}\n`;
913
+ await writeSettingsContent(cwd, updatedContent);
914
+ output.write(`Добавлены разделы ${SETTINGS_RELATIVE_PATH}: ${missingSections.join(', ')}.\n`);
915
+ return;
916
+ }
917
+
805
918
  output.write(`Настраиваю ${SETTINGS_RELATIVE_PATH} заново.\n`);
806
919
  const settings = await askSettings({ input, output });
807
920
  await writeSettingsContent(cwd, formatSettings(settings));
@@ -923,6 +1036,40 @@ function readTopLevelYamlScalar(content, key) {
923
1036
  return null;
924
1037
  }
925
1038
 
1039
+ function readTopLevelYamlKeys(content) {
1040
+ const keys = new Set();
1041
+ for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
1042
+ const match = line.match(/^([A-Za-z0-9_-]+):(?:\s.*)?$/);
1043
+ if (match) keys.add(match[1]);
1044
+ }
1045
+ return keys;
1046
+ }
1047
+
1048
+ function extractSettingsSections(content, sectionNames) {
1049
+ const requested = new Set(sectionNames);
1050
+ const lines = content.split(/\r?\n/);
1051
+ const sectionStarts = [];
1052
+ for (let index = 0; index < lines.length; index += 1) {
1053
+ const match = lines[index].match(/^([A-Za-z0-9_-]+):(?:\s.*)?$/);
1054
+ if (match) sectionStarts.push({ name: match[1], index });
1055
+ }
1056
+
1057
+ const blocks = [];
1058
+ for (let index = 0; index < sectionStarts.length; index += 1) {
1059
+ const section = sectionStarts[index];
1060
+ if (!requested.has(section.name)) continue;
1061
+ const end = sectionStarts[index + 1]?.index ?? lines.length;
1062
+ blocks.push(lines.slice(section.index, end).join('\n').trimEnd());
1063
+ }
1064
+
1065
+ const found = new Set(blocks.map(block => block.match(/^([A-Za-z0-9_-]+):/)?.[1]));
1066
+ const missing = sectionNames.filter(section => !found.has(section));
1067
+ if (missing.length > 0) {
1068
+ throw new Error(`Не удалось сформировать разделы ${SETTINGS_RELATIVE_PATH}: ${missing.join(', ')}.`);
1069
+ }
1070
+ return blocks.join('\n\n');
1071
+ }
1072
+
926
1073
  function readNestedYamlScalar(content, section, key) {
927
1074
  let currentSection = null;
928
1075
  for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gian-tiaga/eda",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Пакет скилов и специализированных агентов eda-* для Claude Code и Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {