@nitra/cursor 1.37.0 → 1.38.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.38.0] - 2026-05-31
4
+
5
+ ### Added
6
+
7
+ - worktree: кросплатформний CLI n-cursor worktree (add/remove/list/prune) — виконавець конвенції .worktrees/ з інвентарним файлом-описом; тонкий skill worktree; pure-doc правило worktree
8
+
3
9
  ## [1.37.0] - 2026-05-31
4
10
 
5
11
  ### Changed
package/README.md CHANGED
@@ -68,6 +68,12 @@
68
68
  npx @nitra/cursor
69
69
  npx @nitra/cursor fix
70
70
  npx @nitra/cursor fix bun ga
71
+
72
+ # Керування git-worktree (.worktrees/ + інвентарний файл-опис)
73
+ npx @nitra/cursor worktree add <branch> "<опис>"
74
+ npx @nitra/cursor worktree list
75
+ npx @nitra/cursor worktree remove <branch> [--force]
76
+ npx @nitra/cursor worktree prune
71
77
  ```
72
78
 
73
79
  Команда `check` запускає programmatic перевірки з каталогу `scripts/` пакету. Якщо в корені репозиторію вже є `.n-cursor.json`, перед перевірками виконується зчитування конфігу — зокрема додається або виправляється поле `$schema`, якщо воно відсутнє або не збігається з очікуваним URL.
package/bin/n-cursor.js CHANGED
@@ -103,6 +103,7 @@ import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
103
103
  import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
104
104
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
105
105
  import { runSkillsCli } from '../scripts/skills-cli.mjs'
106
+ import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
106
107
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
107
108
  import { runLintCli } from '../scripts/lib/run-lint-cli.mjs'
108
109
  import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
@@ -1525,6 +1526,11 @@ try {
1525
1526
 
1526
1527
  break
1527
1528
  }
1529
+ case 'worktree': {
1530
+ process.exitCode = await runWorktreeCli(args)
1531
+
1532
+ break
1533
+ }
1528
1534
  case undefined:
1529
1535
  case '': {
1530
1536
  await runSync()
@@ -1534,7 +1540,7 @@ try {
1534
1540
  default: {
1535
1541
  console.error(`❌ Невідома команда: ${command}`)
1536
1542
  console.error(
1537
- ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill`
1543
+ ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree`
1538
1544
  )
1539
1545
  process.exitCode = 1
1540
1546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: Конвенція git-worktree у цьому репо — створення, інвентаризація та прибирання через n-cursor worktree CLI.
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # Worktree-конвенція
8
+
9
+ Усі git-worktree створюй і прибирай через CLI `n-cursor worktree` — він кладе їх у
10
+ `.worktrees/` (gitignored) і веде інвентарний файл-опис поруч.
11
+
12
+ ## Розташування
13
+
14
+ ```
15
+ .worktrees/
16
+ feat-skill-meta/ ← git worktree checkout
17
+ feat-skill-meta.md ← інвентарний опис поруч (gitignored через .worktrees/)
18
+ ```
19
+
20
+ Слеш у гілці перетворюється на дефіс: `feat/skill-meta` → `.worktrees/feat-skill-meta/`.
21
+ Git-гілка лишається з оригінальним імʼям (`feat/skill-meta`).
22
+
23
+ ## Команди
24
+
25
+ - **Створити** (опис обовʼязковий): `npx @nitra/cursor worktree add <branch> "<навіщо>"`
26
+ - **Інвентаризація**: `npx @nitra/cursor worktree list`
27
+ - **Прибрати**: `npx @nitra/cursor worktree remove <branch> [--force]`
28
+ - **Прибрати осиротілі**: `npx @nitra/cursor worktree prune`
29
+
30
+ ## Заборони
31
+
32
+ - Не клади worktree в `.claude/worktrees/` — це приватна директорія харнесу Claude Code.
33
+ - Не клади worktree в батьківський каталог `../cursor-<name>` — ускладнює інвентаризацію.
34
+ - Не створюй worktree вручну (`git worktree add`) повз CLI — інакше не буде інвентарного опису.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Чиста логіка worktree-tool `n-cursor worktree` (без git/fs side-effects).
3
+ *
4
+ * Тут — детерміновані, тестовані без git функції:
5
+ * - `sanitizeBranch` — імʼя гілки → безпечне імʼя каталогу/файла (слеш та інші
6
+ * небезпечні для шляху символи → дефіс), щоб структура `.worktrees/` лишалась пласкою;
7
+ * - `worktreePaths` — шляхи checkout і файла-опису поруч;
8
+ * - `buildDescription` — текст інвентарного `.worktrees/<name>.md` за конвенцією worktree.mdc;
9
+ * - `findOrphanDescFiles` — `.md`-описи без зареєстрованого worktree (для `prune`).
10
+ *
11
+ * Оркестрація (виклики git, запис файлів, argv) — у `npm/scripts/worktree-cli.mjs`.
12
+ */
13
+ import { basename, join } from 'node:path'
14
+
15
+ /** Символи, безпечні для імені каталогу/файла; решта → дефіс. */
16
+ const UNSAFE_PATH_CHARS_RE = /[^a-zA-Z0-9._-]+/gu
17
+
18
+ /**
19
+ * Перетворює імʼя git-гілки на безпечне імʼя каталогу/файла для `.worktrees/`.
20
+ * @param {string} branch імʼя git-гілки (наприклад `feat/skill-meta`)
21
+ * @returns {string} пласке імʼя (наприклад `feat-skill-meta`)
22
+ */
23
+ export function sanitizeBranch(branch) {
24
+ if (typeof branch !== 'string' || branch.trim() === '') {
25
+ throw new Error('worktree: імʼя гілки обовʼязкове')
26
+ }
27
+ const sanitized = branch.trim().replace(UNSAFE_PATH_CHARS_RE, '-').replace(/^-+|-+$/gu, '')
28
+ if (sanitized === '') {
29
+ throw new Error(`worktree: імʼя гілки "${branch}" не містить допустимих символів`)
30
+ }
31
+ return sanitized
32
+ }
33
+
34
+ /**
35
+ * Детерміновані шляхи checkout і файла-опису для гілки.
36
+ * @param {string} repoRoot абсолютний корінь репозиторію
37
+ * @param {string} branch імʼя git-гілки
38
+ * @returns {{ checkout: string, descFile: string }} абсолютні шляхи
39
+ */
40
+ export function worktreePaths(repoRoot, branch) {
41
+ const name = sanitizeBranch(branch)
42
+ const dir = join(repoRoot, '.worktrees')
43
+ return { checkout: join(dir, name), descFile: join(dir, `${name}.md`) }
44
+ }
45
+
46
+ /**
47
+ * Текст інвентарного файла-опису worktree.
48
+ * @param {{ branch: string, task: string, baseCommit: string, date: string }} params поля опису
49
+ * @returns {string} markdown-вміст `.worktrees/<name>.md`
50
+ */
51
+ export function buildDescription({ branch, task, baseCommit, date }) {
52
+ return [
53
+ `# ${branch}`,
54
+ '',
55
+ `**Задача:** ${task}`,
56
+ `**Дата:** ${date}`,
57
+ `**База (коміт):** ${baseCommit}`,
58
+ '',
59
+ 'Прибрати: ' + '`' + `npx @nitra/cursor worktree remove ${branch}` + '`',
60
+ ''
61
+ ].join('\n')
62
+ }
63
+
64
+ /**
65
+ * `.md`-описи без відповідного зареєстрованого worktree-checkout.
66
+ * @param {string[]} descFiles абсолютні шляхи `.worktrees/*.md`
67
+ * @param {string[]} registeredCheckouts абсолютні шляхи зареєстрованих worktree-checkout
68
+ * @returns {string[]} осиротілі `.md` (підмножина `descFiles`)
69
+ */
70
+ export function findOrphanDescFiles(descFiles, registeredCheckouts) {
71
+ const checkoutBasenames = new Set(registeredCheckouts.map(c => basename(c)))
72
+ return descFiles.filter(md => !checkoutBasenames.has(basename(md).replace(/\.md$/u, '')))
73
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * CLI-оркестратор worktree-tool `n-cursor worktree` (виконавець конвенції `.worktrees/`).
3
+ *
4
+ * Підкоманди:
5
+ * add <branch> "<опис>" — git worktree add .worktrees/<sanit> -b <branch> (від HEAD) + .md-опис
6
+ * remove <branch> [--force] — прибрати checkout + .md (гілку лишає)
7
+ * list — git worktree list + вміст .md-описів
8
+ * prune — git worktree prune + видалити осиротілі .md
9
+ *
10
+ * Чисті функції (санітизація, шляхи, текст опису, осиротілі) — у `lib/worktree.mjs`.
11
+ * Тут лише git-виклики, запис файлів, парсинг argv і звіт.
12
+ */
13
+ import { spawnSync } from 'node:child_process'
14
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { cwd as processCwd } from 'node:process'
17
+
18
+ import { buildDescription, findOrphanDescFiles, worktreePaths } from './lib/worktree.mjs'
19
+
20
+ const USAGE = [
21
+ 'Usage:',
22
+ ' npx @nitra/cursor worktree add <branch> "<опис>"',
23
+ ' npx @nitra/cursor worktree remove <branch> [--force]',
24
+ ' npx @nitra/cursor worktree list',
25
+ ' npx @nitra/cursor worktree prune'
26
+ ].join('\n')
27
+
28
+ /**
29
+ * Запускає git, повертає { status, stdout, stderr }.
30
+ * @param {string[]} args аргументи git
31
+ * @param {string} cwd робочий каталог
32
+ * @returns {{ status: number, stdout: string, stderr: string }} результат
33
+ */
34
+ function git(args, cwd) {
35
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8' })
36
+ return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }
37
+ }
38
+
39
+ /**
40
+ * Поточна дата YYYY-MM-DD (ін'єкція через ctx.now для тестів).
41
+ * @param {() => Date} now фабрика дати
42
+ * @returns {string} дата у форматі YYYY-MM-DD
43
+ */
44
+ function today(now) {
45
+ return now().toISOString().slice(0, 10)
46
+ }
47
+
48
+ /**
49
+ * Реєстровані worktree-checkout (абсолютні шляхи) з `git worktree list --porcelain`.
50
+ * @param {string} cwd корінь репо
51
+ * @returns {string[]} абсолютні шляхи checkout
52
+ */
53
+ function listRegisteredCheckouts(cwd) {
54
+ return git(['worktree', 'list', '--porcelain'], cwd)
55
+ .stdout.split('\n')
56
+ .filter(line => line.startsWith('worktree '))
57
+ .map(line => line.slice('worktree '.length).trim())
58
+ }
59
+
60
+ /**
61
+ * Абсолютні шляхи `.worktrees/*.md`.
62
+ * @param {string} cwd корінь репо
63
+ * @returns {string[]} шляхи файлів-описів
64
+ */
65
+ function listDescFiles(cwd) {
66
+ const dir = join(cwd, '.worktrees')
67
+ if (!existsSync(dir)) return []
68
+ return readdirSync(dir)
69
+ .filter(n => n.endsWith('.md'))
70
+ .map(n => join(dir, n))
71
+ }
72
+
73
+ /**
74
+ * add: створити worktree від HEAD + .md-опис.
75
+ * @param {string[]} rest [branch, ...descParts]
76
+ * @param {{ cwd: string, log: Function, logError: Function, now: () => Date }} ctx контекст
77
+ * @returns {number} exit code
78
+ */
79
+ function cmdAdd(rest, ctx) {
80
+ const [branch, ...descParts] = rest
81
+ const task = descParts.join(' ').trim()
82
+ if (!branch) {
83
+ ctx.logError('worktree add: потрібне імʼя гілки')
84
+ ctx.logError(USAGE)
85
+ return 1
86
+ }
87
+ if (!task) {
88
+ ctx.logError('worktree add: опис обовʼязковий — `worktree add <branch> "<опис>"`')
89
+ return 1
90
+ }
91
+ let paths
92
+ try {
93
+ paths = worktreePaths(ctx.cwd, branch)
94
+ } catch (error) {
95
+ ctx.logError(error.message)
96
+ return 1
97
+ }
98
+ const added = git(['worktree', 'add', paths.checkout, '-b', branch], ctx.cwd)
99
+ if (added.status !== 0) {
100
+ ctx.logError(`worktree add не вдався: ${added.stderr.trim()}`)
101
+ return 1
102
+ }
103
+ const baseCommit = git(['rev-parse', '--short', 'HEAD'], ctx.cwd).stdout.trim()
104
+ const md = buildDescription({ branch, task, baseCommit, date: today(ctx.now) })
105
+ writeFileSync(paths.descFile, md, 'utf8')
106
+ ctx.log(`✅ worktree: ${paths.checkout}`)
107
+ ctx.log(` опис: ${paths.descFile}`)
108
+ return 0
109
+ }
110
+
111
+ /**
112
+ * remove: прибрати checkout + .md (гілку лишає).
113
+ * @param {string[]} rest [branch, ...flags]
114
+ * @param {{ cwd: string, log: Function, logError: Function }} ctx контекст
115
+ * @returns {number} exit code
116
+ */
117
+ function cmdRemove(rest, ctx) {
118
+ const branch = rest.find(a => !a.startsWith('--'))
119
+ const force = rest.includes('--force')
120
+ if (!branch) {
121
+ ctx.logError('worktree remove: потрібне імʼя гілки')
122
+ return 1
123
+ }
124
+ let paths
125
+ try {
126
+ paths = worktreePaths(ctx.cwd, branch)
127
+ } catch (error) {
128
+ ctx.logError(error.message)
129
+ return 1
130
+ }
131
+ const args = ['worktree', 'remove', paths.checkout]
132
+ if (force) args.push('--force')
133
+ const removed = git(args, ctx.cwd)
134
+ if (removed.status !== 0) {
135
+ ctx.logError(`worktree remove не вдався: ${removed.stderr.trim()} (спробуй --force, якщо дерево брудне)`)
136
+ return 1
137
+ }
138
+ if (existsSync(paths.descFile)) rmSync(paths.descFile, { force: true })
139
+ ctx.log(`✅ прибрано: ${paths.checkout} (гілку ${branch} лишено)`)
140
+ return 0
141
+ }
142
+
143
+ /**
144
+ * list: git worktree list + вміст .md-описів.
145
+ * @param {{ cwd: string, log: Function }} ctx контекст
146
+ * @returns {number} exit code
147
+ */
148
+ function cmdList(ctx) {
149
+ ctx.log(git(['worktree', 'list'], ctx.cwd).stdout.trimEnd())
150
+ for (const md of listDescFiles(ctx.cwd)) {
151
+ ctx.log(`\n--- ${md} ---`)
152
+ ctx.log(readFileSync(md, 'utf8').trimEnd())
153
+ }
154
+ return 0
155
+ }
156
+
157
+ /**
158
+ * prune: git worktree prune + видалити осиротілі .md.
159
+ * @param {{ cwd: string, log: Function }} ctx контекст
160
+ * @returns {number} exit code
161
+ */
162
+ function cmdPrune(ctx) {
163
+ git(['worktree', 'prune'], ctx.cwd)
164
+ const orphans = findOrphanDescFiles(listDescFiles(ctx.cwd), listRegisteredCheckouts(ctx.cwd))
165
+ for (const md of orphans) {
166
+ rmSync(md, { force: true })
167
+ ctx.log(`🧹 видалено осиротілий опис: ${md}`)
168
+ }
169
+ ctx.log(`prune завершено (осиротілих описів: ${orphans.length})`)
170
+ return 0
171
+ }
172
+
173
+ /**
174
+ * Точка входу підкоманди worktree.
175
+ * @param {string[]} argv аргументи після `worktree`
176
+ * @param {{ cwd?: string, log?: Function, logError?: Function, now?: () => Date }} [options] ін'єкція для тестів
177
+ * @returns {Promise<number>} exit code
178
+ */
179
+ export function runWorktreeCli(argv, options = {}) {
180
+ const ctx = {
181
+ cwd: options.cwd ?? processCwd(),
182
+ log: options.log ?? (line => console.log(line)),
183
+ logError: options.logError ?? (line => console.error(line)),
184
+ now: options.now ?? (() => new Date())
185
+ }
186
+ const [sub, ...rest] = argv
187
+ switch (sub) {
188
+ case 'add':
189
+ return Promise.resolve(cmdAdd(rest, ctx))
190
+ case 'remove':
191
+ return Promise.resolve(cmdRemove(rest, ctx))
192
+ case 'list':
193
+ return Promise.resolve(cmdList(ctx))
194
+ case 'prune':
195
+ return Promise.resolve(cmdPrune(ctx))
196
+ default:
197
+ ctx.logError(USAGE)
198
+ return Promise.resolve(1)
199
+ }
200
+ }
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: worktree
3
+ description: >-
4
+ Створення та керування git-worktree через n-cursor worktree CLI: ізольований
5
+ workspace у .worktrees/<branch>/ з інвентарним файлом-описом
6
+ ---
7
+
8
+ # worktree — ізольований workspace через CLI
9
+
10
+ Для роботи в окремому git-worktree використовуй CLI `n-cursor worktree` — він
11
+ однаковий у Claude і Cursor, кладе worktree у `.worktrees/` (gitignored) і сам
12
+ створює інвентарний файл-опис поруч.
13
+
14
+ ## Команди
15
+
16
+ - Створити (опис **обовʼязковий**):
17
+ `npx @nitra/cursor worktree add <branch> "<навіщо цей worktree>"`
18
+ - Список активних з описами:
19
+ `npx @nitra/cursor worktree list`
20
+ - Прибрати (гілку лишає; `--force` для брудного дерева):
21
+ `npx @nitra/cursor worktree remove <branch> [--force]`
22
+ - Прибрати осиротілі описи / метадані:
23
+ `npx @nitra/cursor worktree prune`
24
+
25
+ ## Приклад
26
+
27
+ ```bash
28
+ npx @nitra/cursor worktree add feat/skill-meta "реалізація Spec A: meta.json"
29
+ cd .worktrees/feat-skill-meta
30
+ # … робота в ізоляції …
31
+ cd -
32
+ npx @nitra/cursor worktree remove feat/skill-meta
33
+ ```
34
+
35
+ Слеш у гілці перетворюється на дефіс для пласкої структури: `feat/skill-meta`
36
+ → `.worktrees/feat-skill-meta/`. Git-гілка лишається `feat/skill-meta`.
37
+
38
+ Конвенція й заборони (де НЕ створювати worktree) — `.cursor/rules/n-worktree.mdc`.
@@ -0,0 +1 @@
1
+ { "worktree": false }