@nitra/cursor 3.22.0 → 3.23.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.
Files changed (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,156 @@
1
+ # list-rule-ids.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `list-rule-ids.mjs` — невелика бібліотечна утиліта, призначена для перебору директорій-правил у каталозі `npm/rules/` та повернення відсортованого алфавітно списку ідентифікаторів правил, які реально містять виконуваний модуль `fix.mjs`.
6
+
7
+ Логіка модуля побудована навколо архітектурної інваріанти проєкту: після так званої «атомарної міграції» кожне валідне правило **зобов'язане** мати файл `fix.mjs` у власній директорії `rules/<id>/`. Будь-яка піддиректорія без `fix.mjs` вважається або «не-правилом» (службова тека), або заглушкою-чернеткою — і ігнорується. Прихованих директорій (імена, що починаються з `.`) також не існує в результаті.
8
+
9
+ Додатково модуль підтримує опційне точкове фільтрування одним конкретним `id`, що використовується CLI-флагом `--rule abie` для прогону команд у режимі «тільки одне правило».
10
+
11
+ Модуль повністю асинхронний (використовує `node:fs/promises`), не має зовнішніх npm-залежностей і не виконує жодних побічних ефектів окрім читання директорії та перевірки наявності файлу через `existsSync`.
12
+
13
+ ## Експорти / API
14
+
15
+ Файл експортує одну іменовану функцію:
16
+
17
+ | Експорт | Тип | Призначення |
18
+ | ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `listRuleIds` | `async function` | Повертає `Promise<string[]>` зі списком id правил, відфільтрованим за наявністю `fix.mjs` і опційним рівнянням до конкретного id. |
20
+
21
+ Експорту за замовчуванням немає.
22
+
23
+ ## Функції
24
+
25
+ ### `listRuleIds(bundledRulesDir, filter)`
26
+
27
+ #### Сигнатура
28
+
29
+ ```js
30
+ export async function listRuleIds(bundledRulesDir, filter)
31
+ ```
32
+
33
+ JSDoc-типи:
34
+
35
+ ```
36
+ @param {string} bundledRulesDir — абсолютний шлях до `npm/rules/`
37
+ @param {string} [filter] — id одного правила (через `--rule abie`)
38
+ @returns {Promise<string[]>} — відсортовані алфавітно id
39
+ ```
40
+
41
+ #### Параметри
42
+
43
+ - **`bundledRulesDir`** (`string`, обовʼязковий) — абсолютний шлях до кореневої теки правил, найчастіше `npm/rules/` (наприклад, `/path/to/repo/npm/rules`). Передбачається, що шлях існує і є директорією; модуль не валідує його окремо — будь-яка помилка з `fs.readdir` пробрасується далі промісом.
44
+ - **`filter`** (`string | undefined`, опційний) — точна назва правила, до якої потрібно звузити список. Якщо параметр не передано (`undefined`), повертаються всі знайдені id. Якщо передано, у результат потрапляють лише ті id, що _точно_ рівні `filter`. Часткові збіги, регулярні вирази або кейс-незалежність не підтримуються.
45
+
46
+ #### Повертає
47
+
48
+ `Promise<string[]>` — масив рядків-ідентифікаторів правил, відсортованих алфавітно за допомогою `Array.prototype.toSorted` із компаратором `a.localeCompare(b)`. Сортування _стабільне_ для оригінального методу і коректне для Unicode-локалей.
49
+
50
+ Можливі сценарії результату:
51
+
52
+ - Порожній масив `[]` — якщо в `bundledRulesDir` немає жодної директорії з `fix.mjs`, або якщо `filter` не збігається з жодним наявним id.
53
+ - Масив з одного елемента — типово при заданому `filter`.
54
+ - Повний відсортований список усіх правил — у випадку відсутності `filter`.
55
+
56
+ #### Алгоритм / послідовність кроків
57
+
58
+ 1. Викликається `readdir(bundledRulesDir, { withFileTypes: true })` — отримуємо масив `Dirent`-обʼєктів (а не просто рядків імен), що дає змогу одразу перевіряти тип запису без додаткових `stat`-викликів.
59
+ 2. Залишаються лише записи, для яких одночасно:
60
+ - `e.isDirectory()` — це саме директорія;
61
+ - `!e.name.startsWith('.')` — імʼя не починається з крапки (відсікаються приховані теки на кшталт `.git`, `.cache`, тощо).
62
+ 3. Записи перетворюються на голі імена (`.map(e => e.name)`).
63
+ 4. Фільтрація на наявність `fix.mjs` — для кожного `id` синхронно перевіряється `existsSync(join(bundledRulesDir, id, 'fix.mjs'))`. Директорії без цього файлу відсіюються як «not-a-rule або заглушка».
64
+ 5. Фільтрація за `filter` — якщо параметр визначений, залишаються лише id, рівні `filter`; інакше пропускаються всі.
65
+ 6. Сортування `toSorted((a, b) => a.localeCompare(b))` — повертає новий масив без мутації проміжного.
66
+
67
+ #### Side effects
68
+
69
+ - **Дискові читання**: `readdir` (асинхронний обхід директорії) та `existsSync` (синхронні перевірки наявності файлу для кожного кандидата). Винятки I/O пробрасуються відхиленим промісом.
70
+ - **Жодних записів, мережевих викликів, мутацій глобального стану, логування** — функція є read-only і чистою щодо побічних ефектів окрім згаданих читань ФС.
71
+ - Використання `existsSync` у циклі — це _синхронні_ виклики всередині асинхронної функції; вони виконуються послідовно, але без `await`. Для типового розміру каталогу `npm/rules/` (десятки правил) це непомітна вартість.
72
+
73
+ #### Гранітні випадки
74
+
75
+ - Якщо `bundledRulesDir` не існує — `readdir` відхиляє проміс з `ENOENT`.
76
+ - Якщо в директорії є символічні посилання — поведінка залежить від того, що повертає `Dirent.isDirectory()`; ця функція покладається на штатну семантику Node.js.
77
+ - Якщо `filter` переданий як порожній рядок `''` — він _не_ є `undefined`, тому фільтр буде застосовано (`id === ''`), і результат майже напевно буде порожнім.
78
+ - Файл `fix.mjs` як директорія: малоймовірний випадок, але `existsSync` поверне `true` для будь-якого існуючого запису з цим імʼям; модуль не розрізняє файл і теку.
79
+
80
+ ## Залежності
81
+
82
+ ### Вбудовані модулі Node.js
83
+
84
+ | Модуль | Імпортований символ | Використання |
85
+ | ------------------ | ------------------- | ------------------------------------------------------------ |
86
+ | `node:fs` | `existsSync` | Синхронна перевірка наявності `fix.mjs` у кожному кандидаті. |
87
+ | `node:fs/promises` | `readdir` | Асинхронний обхід `bundledRulesDir` з `withFileTypes: true`. |
88
+ | `node:path` | `join` | Безпечне зʼєднання сегментів шляху до `fix.mjs`. |
89
+
90
+ ### Зовнішні npm-залежності
91
+
92
+ Немає.
93
+
94
+ ### Внутрішні залежності
95
+
96
+ Файл не імпортує жодних інших модулів проєкту. Сам по собі він використовується як бібліотечна функція з інших скриптів у `npm/scripts/` (CLI-обгортки, які приймають флаг `--rule`).
97
+
98
+ ## Потік виконання / Використання
99
+
100
+ ### Контекст у проєкті
101
+
102
+ `listRuleIds` — це фундамент будь-якого скрипту, який «робить щось для кожного правила»: лінт, авто-фікс, генерація звіту, перевірка покриття, нормалізація ADR тощо. Замість того щоб кожному скрипту самостійно перелічувати теки в `npm/rules/`, всі вони викликають цю функцію та отримують однаковий, відсортований, відфільтрований список — це гарантує детермінованість порядку обробки правил.
103
+
104
+ Архітектурно файл підкріплює інваріант «`fix.mjs` обовʼязковий»: будь-яка тека без нього автоматично невидима для всіх споживачів. Це дає змогу тримати у `rules/` допоміжні підтеки (наприклад, шаблони, шаред-файли) без ризику, що вони будуть оброблені як правило.
105
+
106
+ ### Приклад використання
107
+
108
+ ```js
109
+ import { listRuleIds } from './lib/list-rule-ids.mjs'
110
+ import { resolve } from 'node:path'
111
+
112
+ const bundledRulesDir = resolve(import.meta.dirname, '../../rules')
113
+
114
+ // 1. Всі правила.
115
+ const all = await listRuleIds(bundledRulesDir)
116
+ // → ['abie', 'changelog', 'feedback', ...]
117
+
118
+ // 2. Тільки одне правило (з CLI-флагу --rule abie).
119
+ const ruleFlag = process.argv.includes('--rule') ? process.argv[process.argv.indexOf('--rule') + 1] : undefined
120
+
121
+ const subset = await listRuleIds(bundledRulesDir, ruleFlag)
122
+ // → ['abie'] якщо ruleFlag === 'abie' і таке правило існує
123
+ // → [] якщо ruleFlag не відповідає жодному правилу
124
+ ```
125
+
126
+ ### Інтеграція з CLI
127
+
128
+ Типовий патерн виклику в скриптах проєкту:
129
+
130
+ 1. Скрипт парсить `process.argv` (часто через `mri`/`minimist` або вручну), отримує опційний `--rule <id>`.
131
+ 2. Викликає `listRuleIds(rulesDir, ruleFromArgv)` і чекає на результат.
132
+ 3. Ітерується по отриманому масиву і виконує цільову дію (fix/check/коверідж) для кожного id.
133
+
134
+ ### Послідовність виконання при виклику
135
+
136
+ ```
137
+ listRuleIds(dir, filter)
138
+
139
+ ├─ readdir(dir, withFileTypes:true) ── async ──▶ [Dirent...]
140
+
141
+ ├─ .filter(isDirectory && !startsWith('.'))
142
+ ├─ .map(e => e.name)
143
+ ├─ .filter(existsSync(<id>/fix.mjs)) ◀── sync I/O loop
144
+ ├─ .filter(filter === undefined || id === filter)
145
+ ├─ .toSorted(localeCompare)
146
+
147
+
148
+ Promise<string[]> (відсортовані id)
149
+ ```
150
+
151
+ ### Очікувані помилки
152
+
153
+ - `ENOENT` / `ENOTDIR` від `readdir` — якщо переданий `bundledRulesDir` неіснуючий або не є директорією. Викликач має або переконатися в коректності шляху перед викликом, або обгорнути виклик у `try/catch`.
154
+ - `EACCES` — якщо немає прав на читання теки.
155
+
156
+ Усі інші помилки (відсутність окремого `fix.mjs`, відсутність відповідності `filter`) семантично не є помилками — вони лише звужують повернений масив.
@@ -0,0 +1,147 @@
1
+ # load-cursor-config.mjs
2
+
3
+ ## Огляд
4
+
5
+ Утилітарний модуль для читання конфігураційного файлу `.n-cursor.json` з кореня репозиторію. Призначений для використання check-скриптами, що обходять файлову систему й мають виключати певні каталоги зі сканування.
6
+
7
+ Наразі експортує лише одну публічну функцію — `loadCursorIgnorePaths(root)`, яка повертає нормалізовані абсолютні posix-шляхи з масиву `ignore` у конфізі. Якщо конфіг відсутній, пошкоджений, або поле `ignore` має невалідний формат — функція повертає порожній масив без кидання винятків (fail-soft підхід).
8
+
9
+ Модуль свідомо **не валідує** структуру конфігу повністю — це робота окремої перевірки (наприклад, через `v8r`). Тут перевіряється лише наявність і тип поля `ignore` та його елементів.
10
+
11
+ Шлях до конфігу зашитий константою `CONFIG_FILE = '.n-cursor.json'` й читається з кореня репозиторію, переданого через параметр `root`.
12
+
13
+ ## Експорти / API
14
+
15
+ | Експорт | Тип | Призначення |
16
+ | ----------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
17
+ | `loadCursorIgnorePaths` | `async function (root: string): Promise<string[]>` | Зчитати й нормалізувати список ігнорованих шляхів з `.n-cursor.json` |
18
+
19
+ Внутрішні (не експортовані) сутності:
20
+
21
+ | Сутність | Тип | Призначення |
22
+ | ------------- | -------------------------------------------- | ------------------------------------------------------------------ |
23
+ | `CONFIG_FILE` | `string` (константа `'.n-cursor.json'`) | Ім'я конфіг-файлу в корені репозиторію |
24
+ | `toAbsPosix` | `function (root: string, p: string): string` | Нормалізатор шляху до абсолютного posix-формату без trailing-slash |
25
+
26
+ ## Функції
27
+
28
+ ### `toAbsPosix(root, p)`
29
+
30
+ Внутрішня (приватна для модуля) функція-нормалізатор шляху.
31
+
32
+ - **Сигнатура:** `function toAbsPosix(root: string, p: string): string`
33
+ - **Параметри:**
34
+ - `root` — абсолютний корінь репозиторію (використовується для розв'язання відносних шляхів).
35
+ - `p` — шлях з конфігу; може бути відносним або абсолютним.
36
+ - **Повертає:** абсолютний posix-шлях (роздільник `/`) без жодного завершального `/`.
37
+ - **Алгоритм:**
38
+ 1. Конвертує `p` у рядок через `String(p)` і прибирає пробіли по краях через `trim()`.
39
+ 2. Якщо результат вже абсолютний (`isAbsolute`), використовує його як є; інакше — резолвить відносно `root` через `resolve(root, trimmed)`.
40
+ 3. Замінює нативний роздільник платформи (`sep`) на `/` (через `split(sep).join('/')`) — на Windows це конвертує `\` у `/`.
41
+ 4. У циклі видаляє всі завершальні `/` (нормалізація `foo/bar//` → `foo/bar`).
42
+ - **Side effects:** немає (чиста функція над аргументами).
43
+
44
+ ### `loadCursorIgnorePaths(root)`
45
+
46
+ Публічна async-функція — точка входу модуля.
47
+
48
+ - **Сигнатура:** `async function loadCursorIgnorePaths(root: string): Promise<string[]>`
49
+ - **Параметри:**
50
+ - `root` — абсолютний шлях до кореня репозиторію, де очікується `.n-cursor.json`.
51
+ - **Повертає:** `Promise<string[]>` — масив абсолютних posix-шляхів без trailing-slash; може бути порожнім.
52
+ - **Алгоритм (fail-soft):**
53
+ 1. Обчислює абсолютний шлях до конфіг-файлу через `join(root, CONFIG_FILE)`.
54
+ 2. Якщо файлу немає на диску (`existsSync` повернув `false`) — повертає `[]`.
55
+ 3. Намагається прочитати файл через `readFile(file, 'utf8')` й розпарсити його як JSON. Якщо парсинг кидає виняток (битий JSON, помилка читання тощо) — `catch` повертає `[]`.
56
+ 4. Дістає поле `ignore` з розпарсеного об'єкта через optional chaining (`raw?.ignore`). Якщо поле не є масивом (`Array.isArray` повернув `false`) — повертає `[]`.
57
+ 5. Ітерує по `list`, для кожного елемента:
58
+ - якщо це не рядок — пропускає;
59
+ - тримить пробіли; якщо рядок став порожнім — пропускає;
60
+ - інакше додає в результат `toAbsPosix(root, v)`.
61
+ 6. Повертає накопичений масив `out`.
62
+ - **Side effects:**
63
+ - Синхронний `existsSync` на дисковий файл `<root>/.n-cursor.json`.
64
+ - Асинхронне читання того ж файлу через `readFile` (тільки якщо `existsSync` повернув `true`).
65
+ - Жодних записів, мережевих викликів чи мутацій глобального стану.
66
+ - **Обробка помилок:**
67
+ - Файл не існує → `[]`.
68
+ - Файл існує, але JSON битий або `readFile` падає → `[]` (через `try/catch`).
69
+ - `ignore` не масив, або взагалі відсутнє → `[]`.
70
+ - Нестрингові або порожні після `trim()` елементи масиву → мовчки пропускаються.
71
+
72
+ ## Залежності
73
+
74
+ ### Node.js built-ins
75
+
76
+ | Модуль | Імпортовані сутності | Використання |
77
+ | ------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
78
+ | `node:fs` | `existsSync` | Синхронна перевірка наявності `.n-cursor.json` перед читанням |
79
+ | `node:fs/promises` | `readFile` | Асинхронне читання вмісту конфіг-файлу як UTF-8 |
80
+ | `node:path` | `isAbsolute`, `join`, `resolve`, `sep` | Перевірка абсолютності, склейка шляху до конфігу, розв'язання відносних шляхів, заміна платформного роздільника на posix |
81
+
82
+ ### Зовнішні залежності
83
+
84
+ Жодних — лише стандартна бібліотека Node.js.
85
+
86
+ ### Конфігураційні артефакти
87
+
88
+ - `.n-cursor.json` у корені репозиторію — джерело правди для `ignore` (опціональне поле, масив рядків).
89
+
90
+ ## Потік виконання / Використання
91
+
92
+ ### Типовий сценарій виклику
93
+
94
+ Викликач (наприклад, check-скрипт) передає абсолютний корінь репозиторію й отримує перелік каталогів, які слід виключити з обходу.
95
+
96
+ ```js
97
+ import { loadCursorIgnorePaths } from './load-cursor-config.mjs'
98
+
99
+ const root = process.cwd() // або інший абсолютний корінь
100
+ const ignored = await loadCursorIgnorePaths(root)
101
+ // ignored: ['/abs/path/.worktrees', '/abs/path/node_modules', ...]
102
+
103
+ // Далі при обході файлів пропускаємо ті, що під будь-яким із ignored:
104
+ function isIgnored(absPosixPath) {
105
+ return ignored.some(ig => absPosixPath === ig || absPosixPath.startsWith(ig + '/'))
106
+ }
107
+ ```
108
+
109
+ ### Очікуваний формат `.n-cursor.json`
110
+
111
+ ```json
112
+ {
113
+ "ignore": [".worktrees", "node_modules", "/abs/path/to/skip"]
114
+ }
115
+ ```
116
+
117
+ Інші поля у файлі допустимі — їх ігнорує цей модуль (валідація схеми — окрема відповідальність).
118
+
119
+ ### Послідовність кроків усередині `loadCursorIgnorePaths`
120
+
121
+ 1. `join(root, '.n-cursor.json')` → шлях до конфігу.
122
+ 2. `existsSync(file)` → якщо `false`, ранній вихід з `[]`.
123
+ 3. `readFile + JSON.parse` у `try/catch` → при будь-якій помилці `[]`.
124
+ 4. `Array.isArray(raw?.ignore)` → якщо `false`, `[]`.
125
+ 5. Лінійний прохід по `ignore`-елементах: фільтр по типу й непорожності, нормалізація через `toAbsPosix(root, v)`.
126
+ 6. Повернення масиву `out`.
127
+
128
+ ### Інваріанти результату
129
+
130
+ - Усі шляхи — **абсолютні**.
131
+ - Усі шляхи — у **posix-форматі** (роздільник `/`), навіть на Windows.
132
+ - Жоден шлях не має **завершального `/`**.
133
+ - Порядок шляхів — відповідає порядку в `ignore` (вхідні дублікати **не дедуплікуються**).
134
+ - За жодних умов функція не кидає винятки — повертає `[]` як єдиний "поганий" результат.
135
+
136
+ ### Тестування (Rebuild Test)
137
+
138
+ Перевірка контрактів модуля:
139
+
140
+ - Файлу немає → `loadCursorIgnorePaths(root)` резолвиться у `[]`.
141
+ - Файл є, але JSON битий → `[]`.
142
+ - Файл є, але `ignore` відсутнє/не масив → `[]`.
143
+ - `ignore` містить нестрингові елементи й порожні рядки — вони відфільтровані; рядкові — нормалізовані до абсолютного posix-шляху без trailing-slash.
144
+ - Відносний шлях у `ignore` (наприклад `"node_modules"`) перетворюється на `<root>/node_modules` у posix-формі.
145
+ - Абсолютний шлях у `ignore` зберігається як є (з можливою конверсією `\` → `/`).
146
+ - На Windows шляхи з `\` конвертуються у `/`.
147
+ - Trailing `/` (один або кілька) видаляються.
@@ -0,0 +1,167 @@
1
+ ## Огляд
2
+
3
+ Модуль `mirror-parity.mjs` забезпечує перевірку парності (parity) між «дзеркальними» файлами правил Cursor у `.cursor/rules/n-<id>.mdc` та їх канонічними джерелами у `npm/rules/<id>/<id>.mdc`. Ідея така: репозиторій містить два представлення одного й того ж правила:
4
+
5
+ - канонічне джерело правила лежить у `npm/rules/<id>/<id>.mdc` (з можливими посиланнями на шаблони у тій самій теці);
6
+ - дзеркало лежить у `.cursor/rules/n-<id>.mdc` і повинно бути результатом застосування трансформу `inlineTemplateLinks` до канону (тобто посилання на шаблони мають бути вставлені «інлайном»).
7
+
8
+ Коли канонічний `.mdc` змінюють і забувають регенерувати дзеркало, виникає «дрейф» (drift). Цей модуль:
9
+
10
+ - перелічує керовані дзеркала (ті, для яких знайдено канон);
11
+ - обчислює очікуваний вміст дзеркала через ту саму трансформацію, що і синхронізатор;
12
+ - виявляє ідентифікатори дрейфних правил (де `actual ≠ expected`).
13
+
14
+ Модуль використовується одночасно і як гард у тестах (очікувано `drift === []`), і для разової регенерації дзеркал. Контекст беклогу — адаптація flow #10.
15
+
16
+ ## Експорти / API
17
+
18
+ Модуль експортує три іменовані функції:
19
+
20
+ - `listManagedMirrors(repoRoot)` — синхронна; повертає опис керованих дзеркал.
21
+ - `expectedMirrorContent(canonicalPath)` — асинхронна (повертає `Promise<string>`); обчислює очікуваний вміст дзеркала.
22
+ - `findMirrorDrift(repoRoot)` — асинхронна; повертає масив id правил із дрейфом, відсортований за зростанням.
23
+
24
+ Жодних дефолтних експортів, побічних ефектів на рівні модуля немає (лише імпорти стандартних модулів і одного локального).
25
+
26
+ ## Функції
27
+
28
+ ### `listManagedMirrors(repoRoot)`
29
+
30
+ Сигнатура: `function listManagedMirrors(repoRoot: string): { id: string, mirrorPath: string, canonicalPath: string }[]`
31
+
32
+ Параметри:
33
+
34
+ - `repoRoot` — абсолютний шлях до кореня репозиторію (директорія, у якій лежать `.cursor/` та `npm/`).
35
+
36
+ Повертає масив об'єктів-описів дзеркал, де:
37
+
38
+ - `id` — ідентифікатор правила без префіксу `n-` і без розширення `.mdc` (наприклад, для файлу `n-bun.mdc` → `bun`);
39
+ - `mirrorPath` — абсолютний шлях до файлу дзеркала всередині `.cursor/rules/`;
40
+ - `canonicalPath` — абсолютний шлях до канонічного `.mdc` всередині `npm/rules/<id>/`.
41
+
42
+ Алгоритм:
43
+
44
+ 1. Будує шлях до `.cursor/rules`.
45
+ 2. Якщо такої директорії не існує (`existsSync` false) — повертає порожній масив (раннє повернення, безпечне для свіжих репо).
46
+ 3. Зчитує перелік файлів директорії синхронно (`readdirSync`).
47
+ 4. Фільтрує файли: повинні починатися з префіксу `'n-'` (константа `MIRROR_PREFIX`) і завершуватися розширенням `'.mdc'` (константа `MDC_EXT`).
48
+ 5. Мапить кожен файл у об'єкт із обчисленими шляхами: `id` отримується вирізанням префіксу і розширення (`f.slice(MIRROR_PREFIX.length, -MDC_EXT.length)`).
49
+ 6. Залишає лише ті записи, для яких реально існує канонічне джерело `canonicalPath` (друга фільтрація через `existsSync`). Це дозволяє пропускати «зовнішні» дзеркала, які не мають канону в монорепо.
50
+
51
+ Side effects: лише читання директорії з диску (синхронні `existsSync`, `readdirSync`). Жодних записів, мережевих викликів, мутацій глобального стану.
52
+
53
+ Помилки: якщо файлова система кине помилку під час `readdirSync` (наприклад, права доступу) — вона прокинеться вгору; для типового сценарію відсутності директорії `.cursor/rules` функція не падає (гард через `existsSync`).
54
+
55
+ ### `expectedMirrorContent(canonicalPath)`
56
+
57
+ Сигнатура: `function expectedMirrorContent(canonicalPath: string): Promise<string>`
58
+
59
+ Параметри:
60
+
61
+ - `canonicalPath` — абсолютний шлях до канонічного `.mdc`-файлу (`npm/rules/<id>/<id>.mdc`).
62
+
63
+ Повертає `Promise<string>` — очікуваний (нормалізований) текст дзеркала після інлайнінгу шаблонів. Хоча сама функція не використовує `async`/`await`, її результат типізовано як `Promise`, оскільки делегує виклик до `inlineTemplateLinks`, яка є асинхронною (її результат може бути thenable). Це означає, що споживач має `await`-нути повернене значення.
64
+
65
+ Алгоритм:
66
+
67
+ 1. Синхронно зчитує вміст канонічного файлу як UTF-8 (`readFileSync(canonicalPath, 'utf8')`).
68
+ 2. Викликає `inlineTemplateLinks(content, dirname(canonicalPath))`, передаючи прочитаний текст та теку канону як base для розв'язання шляхів до шаблонів.
69
+ 3. Повертає результат трансформації.
70
+
71
+ Side effects: одне читання з диску. Подальша поведінка щодо шаблонів інкапсульована в `inlineTemplateLinks` (див. її документацію). Контракт: цей же самий трансформ застосовує синк, тому очікуваний вміст детермінований по канону.
72
+
73
+ Помилки: якщо файл за `canonicalPath` не існує або недоступний — `readFileSync` кине помилку. Викличні сторони мають гарантувати існування (`listManagedMirrors` уже фільтрує неіснуючі канони).
74
+
75
+ ### `findMirrorDrift(repoRoot)`
76
+
77
+ Сигнатура: `async function findMirrorDrift(repoRoot: string): Promise<string[]>`
78
+
79
+ Параметри:
80
+
81
+ - `repoRoot` — абсолютний шлях до кореня репозиторію.
82
+
83
+ Повертає `Promise<string[]>` — відсортований за зростанням масив `id` правил, де дзеркало розійшлося з очікуваним вмістом.
84
+
85
+ Алгоритм:
86
+
87
+ 1. Заводить порожній масив `drift`.
88
+ 2. Послідовно (через `for...of`) обходить усі керовані дзеркала, отримані з `listManagedMirrors(repoRoot)`.
89
+ 3. Для кожного дзеркала `m`:
90
+ - `await`-ить очікуваний вміст через `expectedMirrorContent(m.canonicalPath)`;
91
+ - синхронно зчитує фактичний вміст файлу дзеркала `readFileSync(m.mirrorPath, 'utf8')`;
92
+ - якщо стрічки не дорівнюють — додає `m.id` до `drift`.
93
+ 4. Повертає `drift.sort()` (лексикографічне сортування).
94
+
95
+ Side effects: тільки читання з диску. Не записує жодних файлів, не пише в лог.
96
+
97
+ Зауваження щодо порівняння: рівність стрічок є точною (строге `!==` по UTF-8 тексту). Будь-яка різниця в пробілах, символах кінця рядка чи порядку інлайнених шаблонів буде розцінена як дрейф.
98
+
99
+ ## Залежності
100
+
101
+ Стандартна бібліотека Node.js:
102
+
103
+ - `node:fs` — `existsSync`, `readdirSync`, `readFileSync`. Усі виклики синхронні. Імпорт зроблено через `node:`-префікс для явної прив'язки до built-in модуля.
104
+ - `node:path` — `dirname`, `join`. Використовуються для безпечного складання абсолютних шляхів і отримання батьківської теки канону.
105
+
106
+ Локальні модулі:
107
+
108
+ - `./inline-template-links.mjs` — імпорт іменованої функції `inlineTemplateLinks(content, baseDir)`. Це той самий трансформ, що його використовує синк дзеркал; саме завдяки спільному використанню досягається консистентність між «гардом дрейфу» і «генератором дзеркал».
109
+
110
+ Зовнішніх npm-залежностей немає.
111
+
112
+ Константи на рівні модуля:
113
+
114
+ - `MIRROR_PREFIX = 'n-'` — префікс імені файлів дзеркал.
115
+ - `MDC_EXT = '.mdc'` — розширення файлів правил Cursor.
116
+
117
+ ## Потік виконання / Використання
118
+
119
+ Типові сценарії використання модуля:
120
+
121
+ 1. Тест-гард парності (CI). Споживач передає корінь репо й перевіряє, що дрейф порожній:
122
+
123
+ ```js
124
+ import { findMirrorDrift } from './mirror-parity.mjs'
125
+
126
+ const drift = await findMirrorDrift(repoRoot)
127
+ if (drift.length) {
128
+ throw new Error(`Mirror drift detected for: ${drift.join(', ')}`)
129
+ }
130
+ ```
131
+
132
+ 2. Разова регенерація дзеркал. Споживач отримує список керованих дзеркал і для кожного перезаписує файл дзеркала очікуваним вмістом:
133
+
134
+ ```js
135
+ import { writeFileSync } from 'node:fs'
136
+ import { listManagedMirrors, expectedMirrorContent } from './mirror-parity.mjs'
137
+
138
+ for (const m of listManagedMirrors(repoRoot)) {
139
+ const expected = await expectedMirrorContent(m.canonicalPath)
140
+ writeFileSync(m.mirrorPath, expected)
141
+ }
142
+ ```
143
+
144
+ (Сам модуль `mirror-parity.mjs` записів не виконує — це задача викличного скрипта.)
145
+
146
+ Внутрішній потік виконання `findMirrorDrift`:
147
+
148
+ 1. Виклик `listManagedMirrors(repoRoot)`:
149
+ - перевірка існування `.cursor/rules`;
150
+ - читання списку файлів;
151
+ - фільтрація за префіксом `n-` і розширенням `.mdc`;
152
+ - обчислення `id`, `mirrorPath`, `canonicalPath`;
153
+ - відкидання дзеркал без канону.
154
+ 2. Послідовний обхід отриманих описів:
155
+ - читання канону → `inlineTemplateLinks` → очікуваний текст;
156
+ - читання дзеркала → порівняння;
157
+ - накопичення id з дрейфом.
158
+ 3. Сортування результату та повернення.
159
+
160
+ Контрактна гарантія: якщо синк використовує ту ж саму функцію `inlineTemplateLinks` для генерації дзеркал, то регенерація закриває будь-який дрейф, виявлений `findMirrorDrift`. Якщо дрейф виявлено — він завжди вказує на людську помилку (зміна канону без регенерації) або на розбіжність версій трансформу.
161
+
162
+ Обмеження:
163
+
164
+ - Обхід послідовний (без `Promise.all`) — простіший контракт по помилках, але повільніший на великих наборах правил. Зважаючи на типову кількість правил у `.cursor/rules`, це не критично.
165
+ - Усі читання файлів синхронні, попри асинхронну сигнатуру `findMirrorDrift` — асинхронність зумовлена тільки інтерфейсом `inlineTemplateLinks`.
166
+ - Префікс `n-` зашитий константою — додавання інших префіксів дзеркал вимагатиме зміни модуля.
167
+ - Зовнішні дзеркала (без канону в `npm/rules/<id>/`) автоматично пропускаються; модуль не повідомляє про них окремо.
@@ -87,6 +87,32 @@ export function buildDescription({ branch, task, baseCommit, date }) {
87
87
  ].join('\n')
88
88
  }
89
89
 
90
+ /** Поріг переліку файлів у нагадуванні: понад нього показуємо лише кількість. */
91
+ const DIRTY_LIST_LIMIT = 10
92
+
93
+ /**
94
+ * Нагадування про незакомічені зміни основного дерева, які **не** потрапляють
95
+ * у новий worktree (він створюється від HEAD, без брудного стану). До
96
+ * `limit` файлів — перелік шляхів; більше — лише підсумкова кількість, щоб не
97
+ * залити екран. Призначене для виводу одразу після `worktree add`.
98
+ * @param {string} porcelain вивід `git status --porcelain` основного дерева
99
+ * @param {number} [limit] поріг переліку (понад нього — лише кількість)
100
+ * @returns {string | null} текст нагадування або `null`, якщо дерево чисте
101
+ */
102
+ export function buildDirtyNotice(porcelain, limit = DIRTY_LIST_LIMIT) {
103
+ // Порядок: XY + пробіл (3 символи) + шлях; для перейменування — `orig -> dest`.
104
+ const files = String(porcelain ?? '')
105
+ .split('\n')
106
+ .map(line => line.slice(3).trim())
107
+ .filter(Boolean)
108
+ if (files.length === 0) return null
109
+ const head = `⚠️ Основне дерево має ${files.length} незакомічених змін — вони НЕ потрапили в цей worktree (створено від HEAD).`
110
+ const tail = ' Закоміть потрібні файли, якщо worktree-скіл має їх бачити.'
111
+ if (files.length > limit) return `${head}\n${tail}`
112
+ const list = files.map(f => ` - ${f}`).join('\n')
113
+ return `${head}\n${list}\n${tail}`
114
+ }
115
+
90
116
  /**
91
117
  * `.md`-описи без відповідного зареєстрованого worktree-checkout.
92
118
  * @param {string[]} descFiles абсолютні шляхи `.worktrees/*.md`
@@ -2,7 +2,7 @@
2
2
  * CLI-оркестратор worktree-tool `n-cursor worktree` (виконавець конвенції `.worktrees/`).
3
3
  *
4
4
  * Підкоманди:
5
- * add <branch> "<опис>" — git worktree add .worktrees/<sanit> -b <branch> (від HEAD) + .md-опис
5
+ * add <branch> "<опис>" — git worktree add .worktrees/<sanit> -b <branch> (від HEAD) + .md-опис + нагадування про незакомічені зміни
6
6
  * remove <branch> [--force] — прибрати checkout + .md (гілку лишає)
7
7
  * list — git worktree list + вміст .md-описів
8
8
  * prune — git worktree prune + видалити осиротілі .md
@@ -16,7 +16,13 @@ import { join } from 'node:path'
16
16
  import { cwd as processCwd } from 'node:process'
17
17
 
18
18
  import { cleanupFlowSiblings } from './dispatcher/lib/state-store.mjs'
19
- import { buildDescription, findOrphanDescFiles, firstFreeBranch, worktreePaths } from './lib/worktree.mjs'
19
+ import {
20
+ buildDescription,
21
+ buildDirtyNotice,
22
+ findOrphanDescFiles,
23
+ firstFreeBranch,
24
+ worktreePaths
25
+ } from './lib/worktree.mjs'
20
26
 
21
27
  const USAGE = [
22
28
  'Usage:',
@@ -110,6 +116,9 @@ function cmdAdd(rest, ctx) {
110
116
  if (chosen !== branch) {
111
117
  ctx.log(`ℹ️ гілка/worktree "${branch}" уже існує — обрано вільну назву "${chosen}"`)
112
118
  }
119
+ // Знімаємо статус ДО створення worktree: інакше щойно створений checkout/опис у `.worktrees/`
120
+ // потрапили б у `git status` (коли `.worktrees/` не в .gitignore) і дали б хибне нагадування.
121
+ const dirty = buildDirtyNotice(git(['status', '--porcelain'], ctx.cwd).stdout)
113
122
  const added = git(['worktree', 'add', paths.checkout, '-b', chosen], ctx.cwd)
114
123
  if (added.status !== 0) {
115
124
  ctx.logError(`worktree add не вдався: ${added.stderr.trim()}`)
@@ -120,6 +129,7 @@ function cmdAdd(rest, ctx) {
120
129
  writeFileSync(paths.descFile, md, 'utf8')
121
130
  ctx.log(`✅ worktree: ${paths.checkout}`)
122
131
  ctx.log(` опис: ${paths.descFile}`)
132
+ if (dirty) ctx.log(dirty) // нагадування про незакомічені зміни основного дерева (зняте ДО add)
123
133
  return 0
124
134
  }
125
135