@nitra/cursor 3.4.0 → 3.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.4.1] - 2026-06-01
4
+
5
+ ### Fixed
6
+
7
+ - worktree add: перевіряє зайнятість назви й автоматично обирає вільну (base, base2, base3, …) замість падіння на 'a branch named … already exists'
8
+
3
9
  ## [3.4.0] - 2026-06-01
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -24,13 +24,39 @@ export function sanitizeBranch(branch) {
24
24
  if (typeof branch !== 'string' || branch.trim() === '') {
25
25
  throw new Error('worktree: імʼя гілки обовʼязкове')
26
26
  }
27
- const sanitized = branch.trim().replace(UNSAFE_PATH_CHARS_RE, '-').replace(/^-+|-+$/gu, '')
27
+ const sanitized = branch
28
+ .trim()
29
+ .replace(UNSAFE_PATH_CHARS_RE, '-')
30
+ .replace(/^-+|-+$/gu, '')
28
31
  if (sanitized === '') {
29
32
  throw new Error(`worktree: імʼя гілки "${branch}" не містить допустимих символів`)
30
33
  }
31
34
  return sanitized
32
35
  }
33
36
 
37
+ /**
38
+ * Перша вільна назва гілки за конвенцією `base`, `base2`, `base3`, … —
39
+ * суфікс просто число без розділювача (як `main-fix` → `main-fix2`).
40
+ * Дає змогу `worktree add` спершу перевірити зайнятість і обрати назву,
41
+ * що спрацює, замість падіння на `fatal: a branch named '…' already exists`.
42
+ * @param {string} branch бажане імʼя гілки
43
+ * @param {(candidate: string) => boolean} isTaken чи зайнята назва (гілка/worktree вже існують)
44
+ * @param {number} [limit] стеля кількості спроб (захист від нескінченного циклу)
45
+ * @returns {string} перша вільна назва (= `branch`, якщо вона вільна)
46
+ */
47
+ export function firstFreeBranch(branch, isTaken, limit = 1000) {
48
+ if (typeof branch !== 'string' || branch.trim() === '') {
49
+ throw new Error('worktree: імʼя гілки обовʼязкове')
50
+ }
51
+ const base = branch.trim()
52
+ if (!isTaken(base)) return base
53
+ for (let n = 2; n <= limit; n++) {
54
+ const candidate = `${base}${n}`
55
+ if (!isTaken(candidate)) return candidate
56
+ }
57
+ throw new Error(`worktree: не знайдено вільної назви для "${base}" за ${limit} спроб`)
58
+ }
59
+
34
60
  /**
35
61
  * Детерміновані шляхи checkout і файла-опису для гілки.
36
62
  * @param {string} repoRoot абсолютний корінь репозиторію
@@ -16,7 +16,7 @@ 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, worktreePaths } from './lib/worktree.mjs'
19
+ import { buildDescription, findOrphanDescFiles, firstFreeBranch, worktreePaths } from './lib/worktree.mjs'
20
20
 
21
21
  const USAGE = [
22
22
  'Usage:',
@@ -89,20 +89,34 @@ function cmdAdd(rest, ctx) {
89
89
  ctx.logError('worktree add: опис обовʼязковий — `worktree add <branch> "<опис>"`')
90
90
  return 1
91
91
  }
92
+ // Зайнята, якщо вже є git-гілка з такою назвою або checkout-каталог `.worktrees/<sanit>`.
93
+ const isTaken = name => {
94
+ if (git(['show-ref', '--verify', '--quiet', `refs/heads/${name}`], ctx.cwd).status === 0) return true
95
+ try {
96
+ return existsSync(worktreePaths(ctx.cwd, name).checkout)
97
+ } catch {
98
+ return false // невалідна для шляху назва — впаде нижче на worktreePaths(chosen) з людинозрозумілим текстом
99
+ }
100
+ }
101
+ let chosen
92
102
  let paths
93
103
  try {
94
- paths = worktreePaths(ctx.cwd, branch)
104
+ chosen = firstFreeBranch(branch, isTaken)
105
+ paths = worktreePaths(ctx.cwd, chosen)
95
106
  } catch (error) {
96
107
  ctx.logError(error.message)
97
108
  return 1
98
109
  }
99
- const added = git(['worktree', 'add', paths.checkout, '-b', branch], ctx.cwd)
110
+ if (chosen !== branch) {
111
+ ctx.log(`ℹ️ гілка/worktree "${branch}" уже існує — обрано вільну назву "${chosen}"`)
112
+ }
113
+ const added = git(['worktree', 'add', paths.checkout, '-b', chosen], ctx.cwd)
100
114
  if (added.status !== 0) {
101
115
  ctx.logError(`worktree add не вдався: ${added.stderr.trim()}`)
102
116
  return 1
103
117
  }
104
118
  const baseCommit = git(['rev-parse', '--short', 'HEAD'], ctx.cwd).stdout.trim()
105
- const md = buildDescription({ branch, task, baseCommit, date: today(ctx.now) })
119
+ const md = buildDescription({ branch: chosen, task, baseCommit, date: today(ctx.now) })
106
120
  writeFileSync(paths.descFile, md, 'utf8')
107
121
  ctx.log(`✅ worktree: ${paths.checkout}`)
108
122
  ctx.log(` опис: ${paths.descFile}`)