@7n/rules 1.3.0 → 1.3.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/index.md +0 -2
  3. package/docs/vitest.config.md +1 -1
  4. package/package.json +1 -1
  5. package/rules/abie/clean_merged_ignore_branches/docs/fix-clean_merged_ignore_branches.md +31 -0
  6. package/rules/abie/clean_merged_ignore_branches/docs/index.md +9 -0
  7. package/rules/abie/clean_merged_ignore_branches/fix-clean_merged_ignore_branches.mjs +6 -1
  8. package/rules/docker/lint_docker_yml/docs/fix-lint_docker_yml.md +31 -0
  9. package/rules/docker/lint_docker_yml/docs/index.md +9 -0
  10. package/rules/docker/lint_docker_yml/fix-lint_docker_yml.mjs +3 -1
  11. package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +33 -0
  12. package/rules/ga/clean_ga_workflows/docs/index.md +9 -0
  13. package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +6 -1
  14. package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +27 -0
  15. package/rules/ga/clean_merged_branch/docs/index.md +9 -0
  16. package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +6 -1
  17. package/rules/ga/git_ai/docs/fix-git_ai.md +30 -0
  18. package/rules/ga/git_ai/docs/index.md +9 -0
  19. package/rules/ga/git_ai/fix-git_ai.mjs +3 -1
  20. package/rules/ga/lint_ga/docs/fix-lint_ga.md +32 -0
  21. package/rules/ga/lint_ga/docs/index.md +9 -0
  22. package/rules/ga/lint_ga/fix-lint_ga.mjs +3 -1
  23. package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +26 -0
  24. package/rules/ga/vscode_settings/docs/index.md +9 -0
  25. package/rules/ga/vscode_settings/fix-vscode_settings.mjs +3 -1
  26. package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +28 -0
  27. package/rules/ga/zizmor_yml/docs/index.md +9 -0
  28. package/rules/js/jscpd_config/docs/fix-jscpd_config.md +26 -0
  29. package/rules/js/jscpd_config/docs/index.md +9 -0
  30. package/rules/js/lint_js_yml/docs/fix-lint_js_yml.md +28 -0
  31. package/rules/js/lint_js_yml/docs/index.md +9 -0
  32. package/rules/js/package_json/docs/fix-package_json.md +26 -0
  33. package/rules/js/package_json/docs/index.md +9 -0
  34. package/rules/k8s/lint_k8s_yml/docs/fix-lint_k8s_yml.md +30 -0
  35. package/rules/k8s/lint_k8s_yml/docs/index.md +9 -0
  36. package/rules/k8s/lint_k8s_yml/fix-lint_k8s_yml.mjs +3 -1
  37. package/rules/npm-module/emit_types_config/docs/fix-emit_types_config.md +25 -0
  38. package/rules/npm-module/emit_types_config/docs/index.md +9 -0
  39. package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +3 -1
  40. package/rules/npm-module/npm_package_json/docs/fix-npm_package_json.md +27 -0
  41. package/rules/npm-module/npm_package_json/docs/index.md +9 -0
  42. package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +3 -1
  43. package/rules/npm-module/npm_publish_yml/docs/fix-npm_publish_yml.md +28 -0
  44. package/rules/npm-module/npm_publish_yml/docs/index.md +9 -0
  45. package/rules/npm-module/npm_publish_yml/fix-npm_publish_yml.mjs +4 -1
  46. package/rules/npm-module/root_package_json/docs/fix-root_package_json.md +26 -0
  47. package/rules/npm-module/root_package_json/docs/index.md +9 -0
  48. package/rules/npm-module/root_package_json/fix-root_package_json.mjs +3 -1
  49. package/rules/php/lint_php_yml/docs/fix-lint_php_yml.md +28 -0
  50. package/rules/php/lint_php_yml/docs/index.md +9 -0
  51. package/rules/php/lint_php_yml/fix-lint_php_yml.mjs +3 -1
  52. package/rules/python/lint_python_yml/docs/fix-lint_python_yml.md +27 -0
  53. package/rules/python/lint_python_yml/docs/index.md +9 -0
  54. package/rules/python/lint_python_yml/fix-lint_python_yml.mjs +3 -1
  55. package/rules/rego/vscode_settings/docs/fix-vscode_settings.md +27 -0
  56. package/rules/rego/vscode_settings/docs/index.md +9 -0
  57. package/rules/rego/vscode_settings/fix-vscode_settings.mjs +3 -1
  58. package/rules/rust/lint_rust_yml/docs/fix-lint_rust_yml.md +28 -0
  59. package/rules/rust/lint_rust_yml/docs/index.md +9 -0
  60. package/rules/security/lint_security_yml/docs/fix-lint_security_yml.md +31 -0
  61. package/rules/security/lint_security_yml/docs/index.md +9 -0
  62. package/rules/security/lint_security_yml/fix-lint_security_yml.mjs +6 -1
  63. package/rules/style/lint_style_yml/docs/fix-lint_style_yml.md +31 -0
  64. package/rules/style/lint_style_yml/docs/index.md +9 -0
  65. package/rules/style/lint_style_yml/fix-lint_style_yml.mjs +3 -1
  66. package/rules/style/package_json/docs/fix-package_json.md +27 -0
  67. package/rules/style/package_json/docs/index.md +9 -0
  68. package/rules/style/vscode_settings/docs/fix-vscode_settings.md +27 -0
  69. package/rules/style/vscode_settings/docs/index.md +9 -0
  70. package/rules/style/vscode_settings/fix-vscode_settings.mjs +3 -1
  71. package/rules/text/lint_text/docs/fix-lint_text.md +28 -0
  72. package/rules/text/lint_text/docs/index.md +9 -0
  73. package/rules/text/oxfmtrc/docs/fix-oxfmtrc.md +26 -0
  74. package/rules/text/oxfmtrc/docs/index.md +9 -0
  75. package/rules/text/vscode_settings/docs/fix-vscode_settings.md +27 -0
  76. package/rules/text/vscode_settings/docs/index.md +9 -0
  77. package/rules/text/vscode_settings/fix-vscode_settings.mjs +3 -1
  78. package/rules/worktree/vscode_settings/docs/fix-vscode_settings.md +28 -0
  79. package/rules/worktree/vscode_settings/docs/index.md +9 -0
  80. package/rules/worktree/zed_settings/docs/fix-zed_settings.md +28 -0
  81. package/rules/worktree/zed_settings/docs/index.md +9 -0
  82. package/rules/worktree/zed_settings/fix-zed_settings.mjs +3 -1
  83. package/scripts/docs/skills-cli.md +1 -1
  84. package/scripts/lib/fix/docs/index.md +4 -5
  85. package/scripts/lib/fix/docs/template-deep-merge.md +35 -0
  86. package/scripts/lib/fix/template-deep-merge.mjs +58 -47
  87. package/scripts/skills-cli.mjs +15 -4
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/text/oxfmtrc
4
+ resource: npm/rules/text/oxfmtrc/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | --------------------------------- | --------- |
9
+ | [fix-oxfmtrc.mjs](fix-oxfmtrc.md) | JS Module |
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-vscode_settings.mjs
4
+ resource: npm/rules/text/vscode_settings/fix-vscode_settings.mjs
5
+ docgen:
6
+ crc: aa3c10a6
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 100
10
+ issues: judge:inaccurate:0.93
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл визначає, за якими правилами `patterns` перевіряє відповідність `.vscode/settings.json` конфігурації з `settings.json`. Це потрібно, щоб підтримувати узгоджений стан редакторських налаштувань у проєкті.
17
+
18
+ ## Поведінка
19
+
20
+ 1. `patterns` формує набір правил для приведення `.vscode/settings.json` до узгодженого стану з шаблоном.
21
+ 2. `patterns` орієнтується на конфігурацію `settings.json` як на джерело очікуваного вигляду налаштувань.
22
+ 3. `patterns` потрібен, щоб автоматично підтримувати єдину структуру редакторських налаштувань у проєкті.
23
+ 4. `patterns` працює лише як опис правил і не виконує запис у файлову систему чи базу даних.
24
+
25
+ ## Гарантії поведінки
26
+
27
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/text/vscode_settings
4
+ resource: npm/rules/text/vscode_settings/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------------------------------------- | --------- |
9
+ | [fix-vscode_settings.mjs](fix-vscode_settings.md) | JS Module |
@@ -1,3 +1,5 @@
1
1
  import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
2
 
3
- export const patterns = [createTemplateFixPattern({ id: 'text-vscode_settings-template', targetPath: '.vscode/settings.json' })]
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'text-vscode_settings-template', targetPath: '.vscode/settings.json' })
5
+ ]
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-vscode_settings.mjs
4
+ resource: npm/rules/worktree/vscode_settings/fix-vscode_settings.mjs
5
+ docgen:
6
+ crc: e68a8ed3
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 100
10
+ issues: judge:inaccurate:0.97
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Надає `patterns` для визначення конфігураційних файлів, на які має зважати перевірка робочого дерева. Спирається на конфіги: settings.json.
17
+
18
+ ## Поведінка
19
+
20
+ 1. `patterns` визначає правило виправлення для робочого дерева, яке підтримує наявність очікуваного VS Code-конфігу `settings.json`.
21
+
22
+ 2. `patterns` спрямовує перевірку на `.vscode/settings.json`, щоб проєкт мав узгоджені налаштування редактора для всіх учасників.
23
+
24
+ 3. `patterns` не змінює файлову систему самостійно; воно лише описує доступний шаблон виправлення для зовнішнього механізму застосування правил.
25
+
26
+ ## Гарантії поведінки
27
+
28
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/worktree/vscode_settings
4
+ resource: npm/rules/worktree/vscode_settings/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------------------------------------- | --------- |
9
+ | [fix-vscode_settings.mjs](fix-vscode_settings.md) | JS Module |
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-zed_settings.mjs
4
+ resource: npm/rules/worktree/zed_settings/fix-zed_settings.mjs
5
+ docgen:
6
+ crc: 452faa10
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 100
10
+ issues: judge:inaccurate:0.98
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Надає правило `patterns` для перевірки очікуваних налаштувань проєкту. Код спирається на конфіг `settings.json` і працює read-only, щоб описати вимоги до конфігурації без самостійного внесення змін.
17
+
18
+ ## Поведінка
19
+
20
+ 1. `patterns` визначає правило автоматичного приведення робочого дерева до очікуваного шаблону налаштувань Zed.
21
+
22
+ 2. `patterns` орієнтується на конфіг `settings.json`, щоб забезпечити наявність і узгодженість `.zed/settings.json` у проєкті.
23
+
24
+ 3. `patterns` не змінює файлову систему самостійно; воно лише описує поведінку виправлення для зовнішнього механізму застосування правил.
25
+
26
+ ## Гарантії поведінки
27
+
28
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/worktree/zed_settings
4
+ resource: npm/rules/worktree/zed_settings/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------------------------------- | --------- |
9
+ | [fix-zed_settings.mjs](fix-zed_settings.md) | JS Module |
@@ -1,3 +1,5 @@
1
1
  import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
2
 
3
- export const patterns = [createTemplateFixPattern({ id: 'worktree-zed_settings-template', targetPath: '.zed/settings.json' })]
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'worktree-zed_settings-template', targetPath: '.zed/settings.json' })
5
+ ]
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: skills-cli.mjs
4
4
  resource: npm/scripts/skills-cli.mjs
5
5
  docgen:
6
- crc: 30b6209c
6
+ crc: 33d8696c
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -4,8 +4,7 @@ title: npm/scripts/lib/fix
4
4
  resource: npm/scripts/lib/fix/
5
5
  ---
6
6
 
7
- # npm/scripts/lib/fix
8
-
9
- | Файл | Тип |
10
- | --------------------------------------- | --------- |
11
- | [vscode-ext-add.mjs](vscode-ext-add.md) | JS Module |
7
+ | Файл | Тип |
8
+ | ------------------------------------------------- | --------- |
9
+ | [template-deep-merge.mjs](template-deep-merge.md) | JS Module |
10
+ | [vscode-ext-add.mjs](vscode-ext-add.md) | JS Module |
@@ -0,0 +1,35 @@
1
+ ---
2
+ type: JS Module
3
+ title: template-deep-merge.mjs
4
+ resource: npm/scripts/lib/fix/template-deep-merge.mjs
5
+ docgen:
6
+ crc: 90221a92
7
+ model: openai-codex/gpt-5.4-mini
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Файл створює T0-патерн для одного target-файлу за канонічним `template/*.snippet.{json,jsonc,yml,yaml}` свого concern-а, який резолвиться через `ctx.concernDir`, щоб уніфіковано приводити `engine:"template"` і `engine:"rego"` до потрібного стану без ручного редагування. Для merge об’єкти поєднуються по ключах, масиви зводяться до union за структурним підмножинним збігом, а leaf-значення замінюються канонічним значенням із snippet; якщо target-файл відсутній, snippet копіюється без merge. Для JSON/JSONC застосовується plain-object merge з `JSON.stringify`, а для YAML — `Document API` (`setIn`/`addIn`/`hasIn`), щоб зберігати наявні коментарі й форматування та створювати лише те, чого бракує. `createTemplateFixPattern` працює fail-safe: не кидає винятків назовні й за відсутності потрібного snippet або некоректного входу повертає порожнє значення.
16
+
17
+ ## Поведінка
18
+
19
+ 1. `createTemplateFixPattern` формує T0-патерн для одного цільового файлу, який приводить його до стану, описаного канонічним snippet-файлом у `template/` свого concern-а.
20
+ 2. Під час запуску патерн спершу визначає, чи стосується порушення саме цього цільового файлу; якщо ні — нічого не змінює.
21
+ 3. Далі він шукає відповідний snippet-файл для цільового файлу; якщо snippet не знайдено або немає контексту concern-а, зміни не виконуються.
22
+ 4. Якщо цільового файлу ще немає, він створює його з вмісту snippet без merge — як початковий канонічний стан.
23
+ 5. Якщо цільовий файл уже існує, патерн зливає snippet у наявний вміст: об’єкти доповнюються по ключах, масиви поповнюються лише відсутніми структурно елементами, а прості значення замінюються канонічними.
24
+ 6. Для JSON і JSONC результат записується як нормалізований JSON-текст; якщо вхід невалідний або файл уже відповідає snippet-у, він лишається без змін.
25
+ 7. Для YAML збереження робиться так, щоб не втрачати коментарі й форматування; якщо YAML невалідний, або вже відповідає snippet-у, файл не чіпається.
26
+ 8. У разі успішного оновлення патерн фіксує запис і повідомляє, що цільовий файл приведено у відповідність snippet.
27
+
28
+ ## Публічний API
29
+
30
+ - createTemplateFixPattern — Формує T0-патерн, який вирівнює `targetPath` під `template/*.snippet.*` для свого concern-а; працює як deep-merge, не ламає повторні запуски і лишає одного writer-а для single-target snippet-концернів з `engine:"template"` або `engine:"rego"` на тому самому snippet-шаблоні.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
35
+ - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
@@ -25,7 +25,7 @@ const SNIPPET_EXTS = ['yml', 'yaml', 'json', 'jsonc']
25
25
  * Чи `needle` структурно вже присутній у якомусь елементі `actualArray`
26
26
  * (та сама subset-семантика, що й у детекторі — жодного окремого визначення "збігу").
27
27
  * @param {unknown[]} actualArray наявний масив
28
- * @param {unknown} needle елемент снippета, наявність якого перевіряємо
28
+ * @param {unknown} needle елемент snippet-а, наявність якого перевіряємо
29
29
  * @returns {boolean} true — вже присутній структурно
30
30
  */
31
31
  function containedIn(actualArray, needle) {
@@ -98,6 +98,57 @@ function mergeYamlDoc(doc, snippet, path) {
98
98
  doc.setIn(path, snippet) // leaf — перезаписуємо канонічним значенням
99
99
  }
100
100
 
101
+ /**
102
+ * Рахує наступний вміст JSON/JSONC-target. `null` — невалідний вхід (не чіпаємо);
103
+ * незмінений `prevText` — уже відповідає snippet-у (idempotent, без reformat).
104
+ * @param {string} prevText наявний вміст target-файлу
105
+ * @param {string} snippetPath абсолютний шлях до snippet-файлу
106
+ * @returns {string|null} новий вміст, незмінений `prevText`, або `null`
107
+ */
108
+ function computeJsonNextText(prevText, snippetPath) {
109
+ let snippet
110
+ let actual
111
+ try {
112
+ snippet = JSON.parse(readFileSync(snippetPath, 'utf8'))
113
+ actual = JSON.parse(prevText)
114
+ } catch {
115
+ return null // невалідний JSON — не чіпаємо детермінованим фіксом
116
+ }
117
+ // Уже відповідає snippet-у (та сама перевірка, що й детектор) → без reformat:
118
+ // JSON.stringify інакше переформатував би вже коректний файл (напр. компактний
119
+ // однорядковий snippet → pretty-print) без жодної реальної зміни.
120
+ if (checkSnippet(actual, snippet, { targetPath: '', source: '' }).length === 0) return prevText
121
+ return JSON.stringify(mergeJsonValue(actual, snippet), null, 2) + '\n'
122
+ }
123
+
124
+ /**
125
+ * Рахує наступний вміст YAML-target через `yaml` Document API. `null` — невалідний
126
+ * вхід; незмінений `prevText` — уже відповідає snippet-у (idempotent, без reformat).
127
+ * @param {string} prevText наявний вміст target-файлу
128
+ * @param {string} snippetPath абсолютний шлях до snippet-файлу
129
+ * @returns {Promise<string|null>} новий вміст, незмінений `prevText`, або `null`
130
+ */
131
+ async function computeYamlNextText(prevText, snippetPath) {
132
+ const { parse, parseDocument } = await import('yaml')
133
+ let snippet
134
+ let actualPlain
135
+ try {
136
+ snippet = parse(readFileSync(snippetPath, 'utf8'))
137
+ actualPlain = parse(prevText)
138
+ } catch {
139
+ return null // невалідний YAML — не чіпаємо детермінованим фіксом
140
+ }
141
+ // Уже відповідає snippet-у → без reformat: Document.toString() не завжди
142
+ // byte-identical на вже коректному вмісті (напр. folded block scalars).
143
+ if (checkSnippet(actualPlain, snippet, { targetPath: '', source: '' }).length === 0) return prevText
144
+ const doc = parseDocument(prevText)
145
+ if (doc.errors.length > 0) return null
146
+ if (snippet !== null && typeof snippet === 'object' && !Array.isArray(snippet)) {
147
+ for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [k])
148
+ }
149
+ return doc.toString()
150
+ }
151
+
101
152
  /**
102
153
  * Створює T0-патерн, що приводить `targetPath` у відповідність `template/*.snippet.*`
103
154
  * свого concern-а (deep-merge, idempotent). Один writer — для будь-якого single-target
@@ -110,16 +161,13 @@ export function createTemplateFixPattern({ id, targetPath }) {
110
161
  id,
111
162
  test: violations => violations.some(v => v.file === targetPath),
112
163
  apply: async (violations, ctx) => {
113
- if (!violations.some(v => v.file === targetPath)) return { touchedFiles: [] }
164
+ if (violations.every(v => v.file !== targetPath)) return { touchedFiles: [] }
114
165
  if (!ctx.concernDir) return { touchedFiles: [] }
115
166
 
116
167
  const snippetPath = findSnippetFile(join(ctx.concernDir, 'template'), basename(targetPath))
117
168
  if (!snippetPath) return { touchedFiles: [] }
118
169
 
119
170
  const absTarget = join(ctx.cwd, targetPath)
120
- const ext = extname(snippetPath).toLowerCase()
121
- const isJson = ext === '.json' || ext === '.jsonc'
122
-
123
171
  const prevText = existsSync(absTarget) ? readFileSync(absTarget, 'utf8') : null
124
172
 
125
173
  // Файл відсутній → копіюємо snippet як є (без merge — немає з чим мерджити).
@@ -131,49 +179,12 @@ export function createTemplateFixPattern({ id, targetPath }) {
131
179
  return { touchedFiles: [absTarget], message: `${targetPath}: створено зі snippet` }
132
180
  }
133
181
 
134
- let nextText
135
- if (isJson) {
136
- let snippet
137
- let actual
138
- try {
139
- snippet = JSON.parse(readFileSync(snippetPath, 'utf8'))
140
- actual = JSON.parse(prevText)
141
- } catch {
142
- return { touchedFiles: [] } // невалідний JSON — не чіпаємо детермінованим фіксом
143
- }
144
- // Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
145
- // взагалі: без цієї гейтки JSON.stringify переформатував би вже коректний файл
146
- // (напр. компактний однорядковий snippet → pretty-print) без жодної реальної зміни.
147
- if (checkSnippet(actual, snippet, { targetPath: '', source: '' }).length === 0) {
148
- return { touchedFiles: [] }
149
- }
150
- const merged = mergeJsonValue(actual, snippet)
151
- nextText = JSON.stringify(merged, null, 2) + '\n'
152
- } else {
153
- const { parse, parseDocument } = await import('yaml')
154
- let snippet
155
- let actualPlain
156
- try {
157
- snippet = parse(readFileSync(snippetPath, 'utf8'))
158
- actualPlain = parse(prevText)
159
- } catch {
160
- return { touchedFiles: [] } // невалідний YAML — не чіпаємо детермінованим фіксом
161
- }
162
- // Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
163
- // взагалі: Document.toString() не завжди byte-identical на вже коректному вмісті
164
- // (напр. folded block scalars переформатовуються при round-trip без потреби).
165
- if (checkSnippet(actualPlain, snippet, { targetPath: '', source: '' }).length === 0) {
166
- return { touchedFiles: [] }
167
- }
168
- const doc = parseDocument(prevText)
169
- if (doc.errors.length > 0) return { touchedFiles: [] }
170
- if (snippet !== null && typeof snippet === 'object' && !Array.isArray(snippet)) {
171
- for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [k])
172
- }
173
- nextText = doc.toString()
174
- }
182
+ const isJson = ['.json', '.jsonc'].includes(extname(snippetPath).toLowerCase())
183
+ const nextText = isJson
184
+ ? computeJsonNextText(prevText, snippetPath)
185
+ : await computeYamlNextText(prevText, snippetPath)
175
186
 
176
- if (nextText === prevText) return { touchedFiles: [] }
187
+ if (nextText === null || nextText === prevText) return { touchedFiles: [] }
177
188
  ctx.recordWrite?.(absTarget)
178
189
  writeFileSync(absTarget, nextText, 'utf8')
179
190
  return { touchedFiles: [absTarget], message: `${targetPath}: приведено у відповідність snippet` }
@@ -22,7 +22,7 @@ import { dirname, join } from 'node:path'
22
22
  import { cwd } from 'node:process'
23
23
  import { fileURLToPath } from 'node:url'
24
24
 
25
- import { readSkillMetaRaw, skillTier } from './lib/skill-meta.mjs'
25
+ import { readSkillMetaRaw, skillRequiresRoot, skillTier } from './lib/skill-meta.mjs'
26
26
 
27
27
  /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`codex`/`claude` — зовнішні CLI (`claude` — deprecated). */
28
28
  const RUNNERS = new Set(['pi', 'cursor', 'codex', 'claude'])
@@ -177,13 +177,20 @@ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logErro
177
177
  * або deprecated `claude -p`) — промпт передається через stdin. `claude` лишається як
178
178
  * deprecated fallback для тих, у кого pi-модель ще не налаштована; буде прибрано
179
179
  * (мігруй на `skill pi`). `cursor`/`codex` — повноцінні альтернативні раннери.
180
+ *
181
+ * `cursor-agent` у non-interactive режимі блокується власним `approvalMode: "allowlist"`
182
+ * користувача (git/bun/npx/rm тощо потребують інтерактивного підтвердження, якого в headless
183
+ * виклику немає). Для `worktree:true`/`requireRoot:true` скілів (блок-радіус вже ізольований)
184
+ * додаємо `--force`, замість того щоб просити користувача розширювати глобальний allowlist
185
+ * у `~/.cursor/cli-config.json`.
180
186
  * @param {'claude' | 'cursor' | 'codex'} kind який LLM CLI запускати
181
187
  * @param {string} prompt промпт для передачі у stdin
182
188
  * @param {string} projectDir робочий каталог дочірнього процесу
183
189
  * @param {(line: string) => void} logError вивід попередження/помилок
190
+ * @param {boolean} isolated чи скіл ізольований (worktree/requireRoot) — дозволяє force-approve для `cursor`
184
191
  * @returns {number} exit code дочірнього процесу
185
192
  */
186
- function runLlmCli(kind, prompt, projectDir, logError) {
193
+ function runLlmCli(kind, prompt, projectDir, logError, isolated) {
187
194
  const runner = EXTERNAL_RUNNERS[kind]
188
195
 
189
196
  if (runner.deprecated) {
@@ -194,7 +201,9 @@ function runLlmCli(kind, prompt, projectDir, logError) {
194
201
  throw new Error(`\`${runner.bin}\` not found in PATH. Install ${kind} CLI or use \`skill pi\`.`)
195
202
  }
196
203
 
197
- const result = spawnSync(runner.bin, runner.args, {
204
+ const args = kind === 'cursor' && isolated ? [...runner.args, '--force'] : runner.args
205
+
206
+ const result = spawnSync(runner.bin, args, {
198
207
  input: prompt,
199
208
  cwd: projectDir,
200
209
  stdio: ['pipe', 'inherit', 'inherit'],
@@ -252,7 +261,9 @@ export async function runSkillsCli(argv, options = {}) {
252
261
  if (first === 'pi') {
253
262
  return await runPiRunner(prompt, second, skillsRoot, projectDir, logError, deps)
254
263
  }
255
- return runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError)
264
+ const skillMeta = readSkillMetaRaw(join(skillsRoot, normalizeSkillId(second)))
265
+ const isolated = skillRequiresRoot(skillMeta)
266
+ return runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError, isolated)
256
267
  }
257
268
 
258
269
  if (skillIds.includes(normalizeSkillId(first))) {