@nitra/cursor 1.36.0 → 1.37.0

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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/bin/n-cursor.js +9 -2
  4. package/package.json +1 -1
  5. package/rules/graphql/js/tooling.mjs +3 -1
  6. package/rules/npm-module/js/package_structure.mjs +4 -1
  7. package/rules/npm-module/js/skill_meta.mjs +61 -0
  8. package/rules/release/change.mjs +3 -1
  9. package/rules/release/lib/change-file.mjs +3 -1
  10. package/rules/release/release.mjs +3 -1
  11. package/rules/test/js/data/stryker_config/stryker-vue-macros-ignorer.mjs +2 -1
  12. package/rules/test/js/stryker_config.mjs +6 -8
  13. package/rules/vue/js/packages.mjs +1 -9
  14. package/schemas/skill-meta.json +22 -0
  15. package/schemas/v8r-catalog.json +5 -0
  16. package/scripts/auto-skills.mjs +14 -37
  17. package/scripts/lib/skill-meta.mjs +54 -0
  18. package/scripts/lib/worktree-notice.mjs +55 -0
  19. package/skills/adr-normalize/meta.json +1 -0
  20. package/skills/coverage-fix/meta.json +1 -0
  21. package/skills/fix/meta.json +1 -0
  22. package/skills/fix-tests/meta.json +1 -0
  23. package/skills/lint/meta.json +1 -0
  24. package/skills/llm-patch/meta.json +1 -0
  25. package/skills/publish-telegram/meta.json +1 -0
  26. package/skills/start-check/meta.json +1 -0
  27. package/skills/taze/meta.json +1 -0
  28. package/skills/adr-normalize/auto.md +0 -1
  29. package/skills/coverage-fix/auto.md +0 -1
  30. package/skills/fix/auto.md +0 -1
  31. package/skills/fix-tests/auto.md +0 -1
  32. package/skills/lint/auto.md +0 -1
  33. package/skills/llm-patch/auto.md +0 -1
  34. package/skills/publish-telegram/auto.md +0 -1
  35. package/skills/start-check/auto.md +0 -1
  36. package/skills/taze/auto.md +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.37.0] - 2026-05-31
4
+
5
+ ### Changed
6
+
7
+ - skills: meta.json замість auto.md (+ worktree-прапорець з вшиванням у SKILL.md і забороною паралелі)
8
+
3
9
  ## [1.36.0] - 2026-05-31
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -114,7 +114,7 @@ npm/
114
114
  ```
115
115
  npm/rules/<id>/
116
116
  ├── <id>.mdc # текст правила (після синку — .cursor/rules/n-<id>.mdc)
117
- ├── auto.md # умова автоактивації скілу (опційно)
117
+ ├── meta.json # метадані скілу: auto (автоактивація) + worktree
118
118
  ├── js/ # JS для `npx @nitra/cursor fix`
119
119
  │ └── <concern>/
120
120
  │ ├── check.mjs # діагностика — повертає список violations
package/bin/n-cursor.js CHANGED
@@ -88,6 +88,8 @@ import {
88
88
  RULE_MIGRATIONS
89
89
  } from '../scripts/auto-rules.mjs'
90
90
  import { detectAutoSkills } from '../scripts/auto-skills.mjs'
91
+ import { readSkillMetaRaw } from '../scripts/lib/skill-meta.mjs'
92
+ import { injectWorktreeNotice } from '../scripts/lib/worktree-notice.mjs'
91
93
  import { runPostToolUseFixCli } from '../scripts/post-tool-use-fix.mjs'
92
94
  import { discoverCheckRulesFromCursorRules } from '../scripts/lib/discover-check-rules-from-cursor.mjs'
93
95
  import { listRuleIds } from '../scripts/lib/list-rule-ids.mjs'
@@ -762,10 +764,15 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
762
764
  process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${destDirName} ... `)
763
765
  try {
764
766
  await mkdir(destDir, { recursive: true })
767
+ const meta = readSkillMetaRaw(srcDir)
768
+ const worktree = meta?.worktree === true
765
769
  const files = await readdir(srcDir)
766
770
  for (const file of files) {
767
- if (file === 'auto.md') continue
768
- const content = await readFile(join(srcDir, file), 'utf8')
771
+ if (file === 'meta.json') continue
772
+ let content = await readFile(join(srcDir, file), 'utf8')
773
+ if (file === 'SKILL.md') {
774
+ content = injectWorktreeNotice(content, worktree)
775
+ }
769
776
  await writeFile(join(destDir, file), content, 'utf8')
770
777
  }
771
778
  console.log(`✅`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -82,7 +82,9 @@ function checkExtensionsRecommendation(pass, fail, cwd) {
82
82
  const pathRel = '.vscode/extensions.json'
83
83
  const pathAbs = join(cwd, pathRel)
84
84
  if (!existsSync(pathAbs)) {
85
- fail(`${pathRel} не існує — створи файл і додай у recommendations ${REQUIRED_GRAPHQL_VSCODE_EXTENSION} (graphql.mdc)`)
85
+ fail(
86
+ `${pathRel} не існує — створи файл і додай у recommendations ${REQUIRED_GRAPHQL_VSCODE_EXTENSION} (graphql.mdc)`
87
+ )
86
88
  return
87
89
  }
88
90
  const violations = runConftestBatch({
@@ -422,7 +422,10 @@ async function collectPublishedFiles(filesField, cwd) {
422
422
  }
423
423
  if (!s.isDirectory()) continue
424
424
  await walkDir(fullPath, p => {
425
- const rel = p.slice(npmRoot.length + 1).split(sep).join('/')
425
+ const rel = p
426
+ .slice(npmRoot.length + 1)
427
+ .split(sep)
428
+ .join('/')
426
429
  collected.add(rel)
427
430
  })
428
431
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Перевірка метаданих скілів пакета `@nitra/cursor` (концерн правила npm-module).
3
+ *
4
+ * Кожен `npm/skills/<id>/` має містити валідний `meta.json`:
5
+ * - `worktree` присутнє і boolean;
6
+ * - `auto` (якщо присутнє) — розпізнане (`"завжди"` або непорожній масив рядків);
7
+ * - залишковий `auto.md` заборонено (міграція на meta.json завершена).
8
+ *
9
+ * Концерн застосовний лише в репо самого пакета (де є `npm/skills/`); у споживача
10
+ * каталогу `npm/skills/` нема, тож перевірка мовчки проходить.
11
+ */
12
+ import { existsSync, readdirSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+
15
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
16
+ import { parseSkillAutoSpec, readSkillMetaRaw } from '../../../scripts/lib/skill-meta.mjs'
17
+
18
+ /**
19
+ * Валідує всі `npm/skills/<id>/meta.json`.
20
+ * @param {string} [cwd] корінь репозиторію
21
+ * @returns {Promise<number>} 0 — OK, 1 — порушення
22
+ */
23
+ export function check(cwd = process.cwd()) {
24
+ const reporter = createCheckReporter()
25
+ const skillsDir = join(cwd, 'npm', 'skills')
26
+ if (!existsSync(skillsDir)) {
27
+ reporter.pass('npm/skills/ відсутній — немає скілів для валідації')
28
+ return Promise.resolve(reporter.getExitCode())
29
+ }
30
+
31
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
32
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue
33
+ const id = entry.name
34
+ const skillDir = join(skillsDir, id)
35
+ let skillOk = true
36
+
37
+ if (existsSync(join(skillDir, 'auto.md'))) {
38
+ reporter.fail(`skills/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
39
+ skillOk = false
40
+ }
41
+
42
+ const raw = readSkillMetaRaw(skillDir)
43
+ if (!raw) {
44
+ reporter.fail(`skills/${id}: відсутній або невалідний meta.json (очікується {"auto"?, "worktree": bool})`)
45
+ continue
46
+ }
47
+ if (typeof raw.worktree !== 'boolean') {
48
+ reporter.fail(`skills/${id}: meta.json.worktree має бути boolean`)
49
+ skillOk = false
50
+ }
51
+ if (raw.auto !== undefined && parseSkillAutoSpec(raw.auto) === null) {
52
+ reporter.fail(`skills/${id}: meta.json.auto нерозпізнане — очікується "завжди" або непорожній масив правил`)
53
+ skillOk = false
54
+ }
55
+ if (skillOk) {
56
+ reporter.pass(`skills/${id}: meta.json валідний`)
57
+ }
58
+ }
59
+
60
+ return Promise.resolve(reporter.getExitCode())
61
+ }
@@ -43,7 +43,9 @@ export async function runChangeCli(args) {
43
43
  const message = get('--message')
44
44
  const ws = get('--ws') ?? '.'
45
45
  if (!bump || !section || !message) {
46
- console.error('❌ Використання: n-cursor change --bump <major|minor|patch> --section <Added|Changed|Fixed|Removed> --message "<опис>" [--ws <шлях>]')
46
+ console.error(
47
+ '❌ Використання: n-cursor change --bump <major|minor|patch> --section <Added|Changed|Fixed|Removed> --message "<опис>" [--ws <шлях>]'
48
+ )
47
49
  return 1
48
50
  }
49
51
  try {
@@ -45,7 +45,9 @@ export function parseChangeFile(text) {
45
45
  throw new Error(`change-файл: bump має бути одним із ${VALID_BUMPS.join('|')} (отримано «${fm.bump ?? ''}»)`)
46
46
  }
47
47
  if (!VALID_SECTIONS.includes(fm.section)) {
48
- throw new Error(`change-файл: section має бути одним із ${VALID_SECTIONS.join('|')} (отримано «${fm.section ?? ''}»)`)
48
+ throw new Error(
49
+ `change-файл: section має бути одним із ${VALID_SECTIONS.join('|')} (отримано «${fm.section ?? ''}»)`
50
+ )
49
51
  }
50
52
  if (!description) throw new Error('change-файл: порожній опис')
51
53
  return { bump: fm.bump, section: fm.section, description }
@@ -28,7 +28,9 @@ async function writeManifestVersion(cwd, manifest, newVersion) {
28
28
  const re = manifest.kind === 'npm' ? SEMVER_LINE_RE : PY_VERSION_LINE_RE
29
29
  const replaced = text.replace(re, `$1${newVersion}$2`)
30
30
  if (replaced === text) {
31
- throw new Error(`release: не вдалося оновити version у ${manifest.ws}/${manifest.manifestRel} — патерн version не знайдено`)
31
+ throw new Error(
32
+ `release: не вдалося оновити version у ${manifest.ws}/${manifest.manifestRel} — патерн version не знайдено`
33
+ )
32
34
  }
33
35
  await writeFile(path, replaced)
34
36
  }
@@ -24,7 +24,8 @@ const VUE_SETUP_MACROS = new Set([
24
24
  'defineOptions'
25
25
  ])
26
26
 
27
- const IGNORE_MESSAGE = 'Vue <script setup> macro call cannot be mutated (defineProps/defineEmits/etc. must be statically analyzable for @vue/compiler-sfc).'
27
+ const IGNORE_MESSAGE =
28
+ 'Vue <script setup> macro call cannot be mutated (defineProps/defineEmits/etc. must be statically analyzable for @vue/compiler-sfc).'
28
29
 
29
30
  /**
30
31
  * @param {{isCallExpression: () => boolean, node: {callee: {type: string, name?: string}}}} path babel NodePath, переданий Stryker-instrumenter
@@ -111,13 +111,7 @@ export async function check(cwd = process.cwd()) {
111
111
  for (const jsRoot of jsRoots) {
112
112
  const isVueRoot = await hasVueFiles(jsRoot)
113
113
  const strykerBaseline = isVueRoot ? STRYKER_VUE_BASELINE_PATH : STRYKER_BASELINE_PATH
114
- await ensureBaselineFile(
115
- reporter,
116
- cwd,
117
- strykerBaseline,
118
- join(jsRoot, 'stryker.config.mjs'),
119
- 'stryker.config.mjs'
120
- )
114
+ await ensureBaselineFile(reporter, cwd, strykerBaseline, join(jsRoot, 'stryker.config.mjs'), 'stryker.config.mjs')
121
115
  if (isVueRoot) {
122
116
  await ensureBaselineFile(
123
117
  reporter,
@@ -133,7 +127,11 @@ export async function check(cwd = process.cwd()) {
133
127
  // Гарантуємо що тест-артефакти (Stryker output, lcov HTML-звіт) ніколи не
134
128
  // потрапляють у commit. Patterns покривають усі workspaces через `**/`-префікс
135
129
  // (єдиний root .gitignore).
136
- const { added } = await ensureGitignoreEntries(cwd, TEST_GITIGNORE_ENTRIES, 'Test artifacts: Stryker + coverage (test.mdc)')
130
+ const { added } = await ensureGitignoreEntries(
131
+ cwd,
132
+ TEST_GITIGNORE_ENTRIES,
133
+ 'Test artifacts: Stryker + coverage (test.mdc)'
134
+ )
137
135
  if (added.length > 0) {
138
136
  reporter.pass(`.gitignore: додано тест-патерни (${added.join(', ')}) (test.mdc)`)
139
137
  }
@@ -422,15 +422,7 @@ async function checkVuePackage(rootDir, ignorePaths, fail, passFn, cwd) {
422
422
  await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail, cwd)
423
423
 
424
424
  const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail, cwd)
425
- await checkVueImportViolations(
426
- rootDir,
427
- join(cwd, rootDir),
428
- ignorePaths,
429
- hasVueAutoImport,
430
- prefix,
431
- passFn,
432
- fail
433
- )
425
+ await checkVueImportViolations(rootDir, join(cwd, rootDir), ignorePaths, hasVueAutoImport, prefix, passFn, fail)
434
426
  await checkVueNodeImportViolations(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
435
427
  await checkEsbuildMentions(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
436
428
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@nitra/cursor/schemas/skill-meta.json",
4
+ "title": "n-cursor skill meta",
5
+ "description": "Метадані скіла @nitra/cursor: умова автоактивації (auto) і чи виконувати в окремому git-worktree (worktree). Файл npm/skills/<id>/meta.json.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["worktree"],
9
+ "properties": {
10
+ "auto": {
11
+ "description": "Умова автоактивації: \"завжди\" або непорожній масив id правил, від яких залежить скіл.",
12
+ "oneOf": [
13
+ { "const": "завжди" },
14
+ { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 }
15
+ ]
16
+ },
17
+ "worktree": {
18
+ "type": "boolean",
19
+ "description": "true — виконувати скіл в окремому git-worktree, один інстанс за раз (без паралельного запуску); false — у worktree не виконується."
20
+ }
21
+ }
22
+ }
@@ -25,6 +25,11 @@
25
25
  "description": "TypeScript config для pi extension templates, які Schema Store не матчить через вкладений/прихований шлях",
26
26
  "url": "https://json.schemastore.org/tsconfig.json",
27
27
  "fileMatch": [".pi/extensions/*/tsconfig.json", "npm/.pi-template/extensions/*/tsconfig.json"]
28
+ },
29
+ {
30
+ "name": "n-cursor skill meta",
31
+ "fileMatch": ["npm/skills/*/meta.json"],
32
+ "url": "https://unpkg.com/@nitra/cursor/schemas/skill-meta.json"
28
33
  }
29
34
  ]
30
35
  }
@@ -1,56 +1,33 @@
1
1
  /**
2
- * Автовизначення skills для `.n-cursor.json` за умовами зі `npm/skills/<skill>/auto.md`.
2
+ * Автовизначення skills для `.n-cursor.json` за умовами з `npm/skills/<skill>/meta.json`.
3
3
  *
4
- * `auto.md` — джерело правди (а не hardcoded мапа). Підтримуються три варіанти:
4
+ * `meta.json` — джерело правди (а не hardcoded мапа). Підтримуються три варіанти:
5
5
  *
6
- * - `завжди` — скіл активується незалежно від інших правил
6
+ * - `auto: "завжди"` — скіл активується незалежно від інших правил
7
7
  * (приклади: `fix`, `lint`, `llm-patch`, `publish-telegram`).
8
- * - `[rule, rule, …]` — скіл активується, якщо ВСІ перелічені правила вже виявлені
9
- * auto-rules (приклади: `adr-normalize - [adr]`, `taze - [bun]`).
10
- * - файл відсутній або формат не розпізнано — скіл opt-in лише через `.n-cursor.json:skills`.
8
+ * - `auto: ["rule", …]` — скіл активується, якщо ВСІ перелічені правила вже виявлені
9
+ * auto-rules (приклади: `adr-normalize - ["adr"]`, `taze - ["bun"]`).
10
+ * - поле `auto` відсутнє або формат не розпізнано — скіл opt-in лише через `.n-cursor.json:skills`.
11
11
  *
12
12
  * Сканування `npm/skills/` — sync під час завантаження модуля (детермінізм + sync API
13
13
  * `auto-rules.mjs`-сусіда). Кеш на час процесу.
14
14
  */
15
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
15
+ import { existsSync, readdirSync } from 'node:fs'
16
16
  import { dirname, join } from 'node:path'
17
17
  import { fileURLToPath } from 'node:url'
18
18
 
19
+ import { parseSkillAutoSpec, readSkillMetaRaw } from './lib/skill-meta.mjs'
20
+
19
21
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
20
22
  const SKILLS_DIR = join(PACKAGE_ROOT, 'skills')
21
23
 
22
- const ALWAYS_LITERAL = 'завжди'
23
- const BRACKET_LIST_RE = /^\[([^\]]+)\]$/u
24
-
25
24
  /**
26
25
  * @typedef {{ always: true } | { rules: readonly string[] }} SkillAutoSpec
27
26
  */
28
27
 
29
28
  /**
30
- * Парсить тіло `auto.md` одного скіла.
31
- * @param {string} text вміст файла (без `trim`)
32
- * @returns {SkillAutoSpec | null} `null` — формат не розпізнано (= opt-in)
33
- */
34
- function parseSkillAutoSpec(text) {
35
- const trimmed = text.trim()
36
- if (trimmed === ALWAYS_LITERAL) {
37
- return { always: true }
38
- }
39
- const m = trimmed.match(BRACKET_LIST_RE)
40
- if (m) {
41
- const rules = m[1]
42
- .split(',')
43
- .map(s => s.trim())
44
- .filter(s => s.length > 0)
45
- if (rules.length === 0) return null
46
- return { rules: Object.freeze(rules) }
47
- }
48
- return null
49
- }
50
-
51
- /**
52
- * Сканує `npm/skills/<id>/auto.md`. Скіли без `auto.md` або з нерозпізнаним
53
- * вмістом не потрапляють у результат — їх можна вмикати лише вручну в конфізі.
29
+ * Сканує `npm/skills/<id>/meta.json`. Скіли без `meta.json` або без розпізнаного
30
+ * `auto` не потрапляють у результат їх вмикають лише вручну в конфізі.
54
31
  * @param {string} [skillsDir] override для тестів
55
32
  * @returns {Record<string, SkillAutoSpec>} мапа `skillId → spec`
56
33
  */
@@ -60,9 +37,9 @@ export function discoverSkillAutoActivation(skillsDir = SKILLS_DIR) {
60
37
  const out = {}
61
38
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
62
39
  if (!entry.isDirectory() || entry.name.startsWith('.')) continue
63
- const autoMdPath = join(skillsDir, entry.name, 'auto.md')
64
- if (!existsSync(autoMdPath)) continue
65
- const spec = parseSkillAutoSpec(readFileSync(autoMdPath, 'utf8'))
40
+ const raw = readSkillMetaRaw(join(skillsDir, entry.name))
41
+ if (!raw) continue
42
+ const spec = parseSkillAutoSpec(raw.auto)
66
43
  if (spec) out[entry.name] = spec
67
44
  }
68
45
  return out
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Спільний парсер метаданих скіла з `npm/skills/<id>/meta.json`.
3
+ *
4
+ * `meta.json` — єдине джерело правди для скіла замість колишнього `auto.md`:
5
+ * - `auto` — умова автоактивації (`"завжди"` | масив id правил), опційне;
6
+ * - `worktree` — boolean: чи виконувати скіл в окремому git-worktree (один інстанс).
7
+ *
8
+ * Цим хелпером користуються `auto-skills.mjs` (автоактивація), `n-cursor.js`
9
+ * (sync + вшивання worktree-блоку) і check-концерн `npm-module/js/skill_meta.mjs`,
10
+ * щоб не дублювати парсинг і форму валідації.
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+
15
+ /** Літерал безумовної автоактивації (українською, як у `auto-skills.mjs`). */
16
+ export const SKILL_ALWAYS = 'завжди'
17
+
18
+ /**
19
+ * @typedef {{ always: true } | { rules: string[] }} SkillAutoSpec
20
+ */
21
+
22
+ /**
23
+ * Перетворює значення поля `auto` з `meta.json` у `SkillAutoSpec`.
24
+ * @param {unknown} value значення `meta.json.auto`
25
+ * @returns {SkillAutoSpec | null} `null` — формат не розпізнано (= opt-in)
26
+ */
27
+ export function parseSkillAutoSpec(value) {
28
+ if (value === SKILL_ALWAYS) {
29
+ return { always: true }
30
+ }
31
+ if (Array.isArray(value)) {
32
+ const rules = value.map(s => String(s).trim()).filter(s => s.length > 0)
33
+ if (rules.length === 0) return null
34
+ return { rules }
35
+ }
36
+ return null
37
+ }
38
+
39
+ /**
40
+ * Читає й парсить `meta.json` одного скіла.
41
+ * @param {string} skillDir абсолютний шлях до каталогу скіла
42
+ * @returns {Record<string, unknown> | null} розпарсений обʼєкт або `null` (немає файлу / невалідний JSON / не-обʼєкт)
43
+ */
44
+ export function readSkillMetaRaw(skillDir) {
45
+ const metaPath = join(skillDir, 'meta.json')
46
+ if (!existsSync(metaPath)) return null
47
+ try {
48
+ const parsed = JSON.parse(readFileSync(metaPath, 'utf8'))
49
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
50
+ return /** @type {Record<string, unknown>} */ (parsed)
51
+ } catch {
52
+ return null
53
+ }
54
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Вшивання worktree-інструкції у синкнутий `SKILL.md` (рішення D2 зі spec).
3
+ *
4
+ * Коли `meta.json.worktree === true`, скіл має виконуватись в окремому git-worktree
5
+ * і не паралелитись. Підказка адресована агенту, який читає `SKILL.md`, тож
6
+ * вставляється в текст між стабільними маркерами — ре-синк ідемпотентний:
7
+ * наявний блок замінюється, при `worktree:false` — видаляється.
8
+ */
9
+
10
+ /** Маркер початку worktree-блоку (стабільний, не залежить від тексту всередині). */
11
+ export const WORKTREE_START = '<!-- n-cursor:worktree:start -->'
12
+ /** Маркер кінця worktree-блоку. */
13
+ export const WORKTREE_END = '<!-- n-cursor:worktree:end -->'
14
+
15
+ const NOTICE_BODY =
16
+ '> **Worktree:** виконуй цей скіл в окремому git-worktree (`git worktree add`); ' +
17
+ '**не** запускай паралельно — один інстанс за раз.'
18
+
19
+ /** Наявний блок разом із сусідніми порожніми рядками (для чистого видалення). */
20
+ const BLOCK_RE = /\n*<!-- n-cursor:worktree:start -->[\s\S]*?<!-- n-cursor:worktree:end -->\n*/u
21
+
22
+ /** Закриття YAML-frontmatter на початку файла. */
23
+ const FRONTMATTER_RE = /^(---\n[\s\S]*?\n---\n)/u
24
+
25
+ /**
26
+ * Канонічний блок worktree-інструкції.
27
+ * @returns {string} текст блоку від START до END
28
+ */
29
+ function buildBlock() {
30
+ return `${WORKTREE_START}\n${NOTICE_BODY}\n${WORKTREE_END}`
31
+ }
32
+
33
+ /**
34
+ * Вставляє / оновлює / видаляє worktree-блок у вмісті `SKILL.md`.
35
+ * @param {string} content вміст `SKILL.md`
36
+ * @param {boolean} enabled чи має бути блок (значення `meta.json.worktree`)
37
+ * @returns {string} оновлений вміст (ідемпотентно)
38
+ */
39
+ export function injectWorktreeNotice(content, enabled) {
40
+ const hadBlock = content.includes(WORKTREE_START)
41
+ const withoutBlock = content.replace(BLOCK_RE, '\n\n')
42
+
43
+ if (!enabled) {
44
+ return hadBlock ? withoutBlock : content
45
+ }
46
+
47
+ const block = buildBlock()
48
+ const fm = withoutBlock.match(FRONTMATTER_RE)
49
+ if (fm) {
50
+ const head = fm[1]
51
+ const rest = withoutBlock.slice(head.length).replace(/^\n+/u, '')
52
+ return `${head}\n${block}\n\n${rest}`
53
+ }
54
+ return `${block}\n\n${withoutBlock.replace(/^\n+/u, '')}`
55
+ }
@@ -0,0 +1 @@
1
+ { "auto": ["adr"], "worktree": true }
@@ -0,0 +1 @@
1
+ { "auto": ["js-lint"], "worktree": true }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": true }
@@ -0,0 +1 @@
1
+ { "auto": ["js-lint"], "worktree": true }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": false }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": false }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": false }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": false }
@@ -0,0 +1 @@
1
+ { "auto": ["bun"], "worktree": true }
@@ -1 +0,0 @@
1
- [adr]
@@ -1 +0,0 @@
1
- [js-lint]
@@ -1 +0,0 @@
1
- завжди
@@ -1 +0,0 @@
1
- [js-lint]
@@ -1 +0,0 @@
1
- завжди
@@ -1 +0,0 @@
1
- завжди
@@ -1 +0,0 @@
1
- завжди
@@ -1 +0,0 @@
1
- [bun]
@@ -1 +0,0 @@
1
- [bun]