@nitra/cursor 3.3.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 +12 -0
- package/package.json +1 -1
- package/rules/flow/flow.mdc +17 -3
- package/scripts/dispatcher/index.mjs +5 -3
- package/scripts/dispatcher/lib/commands.mjs +4 -1
- package/scripts/dispatcher/lib/level.mjs +41 -0
- package/scripts/dispatcher/lib/review.mjs +150 -0
- package/scripts/lib/worktree.mjs +27 -1
- package/scripts/worktree-cli.mjs +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [3.4.0] - 2026-06-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- flow: команда review (adversarial diff-review за рівнем) + scale-adaptive level в init (L0–L3, керує глибиною review)
|
|
14
|
+
|
|
3
15
|
## [3.3.0] - 2026-06-01
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
package/rules/flow/flow.mdc
CHANGED
|
@@ -20,7 +20,10 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Створює ізольований worktree (`.worktrees/<branch>/`) і стан задачі. Якщо ти
|
|
23
|
-
вже в worktree — новий не вкладається.
|
|
23
|
+
вже в worktree — новий не вкладається. `init` визначає **рівень** задачі
|
|
24
|
+
(L0 тривіальне … L3 архітектурне) за описом: для L0 (fix/typo/bump) фази
|
|
25
|
+
Spec/План можна пропустити; для L≥1 вони рекомендовані. Рівень також керує
|
|
26
|
+
глибиною `review`.
|
|
24
27
|
|
|
25
28
|
2. **Spec (дизайн)** — рекомендовано, не блокує. Brainstorm нашими термінами
|
|
26
29
|
(НЕ викликаючи superpowers):
|
|
@@ -62,10 +65,21 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
|
|
|
62
65
|
Проганяє Quality Gates (lint + coverage). Повертає `0` (pass) або `1` із
|
|
63
66
|
виводом проваленого gate.
|
|
64
67
|
|
|
65
|
-
6.
|
|
68
|
+
6. **Review (adversarial)** — рекомендовано перед release:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
npx @nitra/cursor flow review
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Незалежний субагент читає ЛИШЕ `git diff` від `base_commit` і шукає логічні
|
|
75
|
+
баги/ризики, яких не ловлять механічні гейти. Кількість рецензентів — за
|
|
76
|
+
рівнем (L0→1 … L3→3). Findings пишуться у `.flow.json` (не блокує); high-
|
|
77
|
+
severity варто виправити перед фінішем.
|
|
78
|
+
|
|
79
|
+
7. **На провал** — виправ код за виводом і виклич `flow verify` знову. Максимум
|
|
66
80
|
**3 спроби**; якщо не вдається — зупинись і поклич людину.
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
8. **Фініш** — лише після зеленого `verify`:
|
|
69
83
|
|
|
70
84
|
```
|
|
71
85
|
npx @nitra/cursor flow release --bump <patch|minor|major> --section <Added|Changed|Fixed> --message "<що зроблено>"
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
import { cancel, repair, resume, run } from './lib/active.mjs'
|
|
11
11
|
import { init, release, verify } from './lib/commands.mjs'
|
|
12
12
|
import { plan } from './lib/plan.mjs'
|
|
13
|
+
import { review } from './lib/review.mjs'
|
|
13
14
|
import { spec } from './lib/spec.mjs'
|
|
14
15
|
|
|
15
16
|
const USAGE = [
|
|
16
17
|
'Usage:',
|
|
17
|
-
' npx @nitra/cursor flow init "<опис>" # Фасад A: worktree + .flow.json',
|
|
18
|
+
' npx @nitra/cursor flow init "<опис>" # Фасад A: worktree + .flow.json (+ level)',
|
|
18
19
|
' npx @nitra/cursor flow spec [--panel] # Фасад A: фаза дизайну → docs/specs/<…>',
|
|
19
20
|
' npx @nitra/cursor flow plan [--panel] # Фасад A: фаза плану → docs/plans/<…> + state',
|
|
20
21
|
' npx @nitra/cursor flow verify # Фасад A: Quality Gates (pass/fail)',
|
|
22
|
+
' npx @nitra/cursor flow review # Фасад A: adversarial diff-review (за level)',
|
|
21
23
|
' npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot',
|
|
22
24
|
' npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл',
|
|
23
25
|
' npx @nitra/cursor flow resume # продовжити з чекпойнта',
|
|
@@ -26,13 +28,13 @@ const USAGE = [
|
|
|
26
28
|
].join('\n')
|
|
27
29
|
|
|
28
30
|
/** Підкоманди flow. */
|
|
29
|
-
export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'release', 'run', 'resume', 'cancel', 'repair']
|
|
31
|
+
export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'review', 'release', 'run', 'resume', 'cancel', 'repair']
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Усі handler-и реальні (Ф1 Spec/Plan + Ф2 Турнікет + Ф4 Активний Раннер).
|
|
33
35
|
* @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
|
|
34
36
|
*/
|
|
35
|
-
export const DEFAULT_HANDLERS = { init, spec, plan, verify, release, run, resume, cancel, repair }
|
|
37
|
+
export const DEFAULT_HANDLERS = { init, spec, plan, verify, review, release, run, resume, cancel, repair }
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
|
|
@@ -12,6 +12,7 @@ import { cwd as processCwd } from 'node:process'
|
|
|
12
12
|
import { worktreePaths } from '../../lib/worktree.mjs'
|
|
13
13
|
import { worktreeFingerprint } from '../../utils/worktree-fingerprint.mjs'
|
|
14
14
|
import { flowEventsPath } from './events.mjs'
|
|
15
|
+
import { detectLevel } from './level.mjs'
|
|
15
16
|
import { runReview } from './reviewer.mjs'
|
|
16
17
|
import { buildCompletionSnapshot, writeSummaryToTaskRecord } from './snapshot.mjs'
|
|
17
18
|
import { flowStatePath, readState, recordTransition, writeState } from './state-store.mjs'
|
|
@@ -101,14 +102,16 @@ export async function init(rest, deps = {}) {
|
|
|
101
102
|
const now = deps.now ?? Date.now
|
|
102
103
|
const log = deps.log ?? console.error
|
|
103
104
|
const statePath = flowStatePath(ew.worktreeDir)
|
|
105
|
+
const level = detectLevel(ew.desc)
|
|
104
106
|
writeState(statePath, {
|
|
105
107
|
branch: ew.branch,
|
|
106
108
|
status: 'in_progress',
|
|
107
109
|
started_at: new Date(now()).toISOString(),
|
|
108
110
|
metadata: { base_commit: ew.baseCommit },
|
|
111
|
+
level,
|
|
109
112
|
plan: []
|
|
110
113
|
})
|
|
111
|
-
log(`init: ${ew.branch} → ${statePath}`)
|
|
114
|
+
log(`init: ${ew.branch} (level ${level}) → ${statePath}`)
|
|
112
115
|
return 0
|
|
113
116
|
}
|
|
114
117
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale-adaptive рівень задачі (ідея з BMAD project-levels, у наших термінах).
|
|
3
|
+
* `init` визначає рівень за описом; рівень right-size'ить, скільки adversarial-
|
|
4
|
+
* рецензентів спавнить `flow review`, і які фази рекомендовані (контракт).
|
|
5
|
+
*
|
|
6
|
+
* Детекція — підрядками (case-insensitive), без regex (уникаємо slow-regex і
|
|
7
|
+
* проблем зі словомежами для кирилиці).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** L3 — велике/архітектурне. */
|
|
11
|
+
const L3_KEYS = ['platform', 'migration', 'rewrite', 'architecture', 'enterprise', 'редизайн', 'міграц', 'переписат']
|
|
12
|
+
/** L0 — тривіальне. */
|
|
13
|
+
const L0_KEYS = ['fix', 'typo', 'bump', 'rename', 'hotfix', 'опечат', 'перейменув']
|
|
14
|
+
/** L2 — багатофайлова фіча/рефактор. */
|
|
15
|
+
const L2_KEYS = ['feature', 'epic', 'refactor', 'рефактор', 'фіча']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Рівень складності задачі за описом: 0 (тривіальне) … 3 (архітектурне).
|
|
19
|
+
* Пріоритет: L3 > L0 > L2 > дефолт L1.
|
|
20
|
+
* @param {string} desc опис задачі
|
|
21
|
+
* @returns {0 | 1 | 2 | 3} рівень
|
|
22
|
+
*/
|
|
23
|
+
export function detectLevel(desc) {
|
|
24
|
+
const d = String(desc ?? '').toLowerCase()
|
|
25
|
+
const has = keys => keys.some(k => d.includes(k))
|
|
26
|
+
if (has(L3_KEYS)) return 3
|
|
27
|
+
if (has(L0_KEYS)) return 0
|
|
28
|
+
if (has(L2_KEYS)) return 2
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Скільки adversarial-рецензентів спавнити для рівня (глибина review за ризиком).
|
|
34
|
+
* @param {number} level рівень 0..3
|
|
35
|
+
* @returns {number} кількість рецензентів (1..3)
|
|
36
|
+
*/
|
|
37
|
+
export function reviewersForLevel(level) {
|
|
38
|
+
if (level >= 3) return 3
|
|
39
|
+
if (level === 2) return 2
|
|
40
|
+
return 1
|
|
41
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flow review` — adversarial-перевірка коду ПІСЛЯ написання (ідея з BMAD
|
|
3
|
+
* quick-dev: self-check → adversarial-review). Незалежний субагент читає ЛИШЕ
|
|
4
|
+
* `git diff base_commit` і шукає логічні баги/ризики, яких не ловлять механічні
|
|
5
|
+
* гейти `verify` (lint+coverage). Findings пишуться у `.flow.json`; команда
|
|
6
|
+
* інформативна (м'які ворота — завжди код 0). Кількість рецензентів — за `level`.
|
|
7
|
+
*
|
|
8
|
+
* Уся IO (`run`/`runner`/`now`) ін'єктується — тестується без git/LLM.
|
|
9
|
+
*/
|
|
10
|
+
import { cwd as processCwd } from 'node:process'
|
|
11
|
+
|
|
12
|
+
import { realRun } from './commands.mjs'
|
|
13
|
+
import { flowEventsPath } from './events.mjs'
|
|
14
|
+
import { reviewersForLevel } from './level.mjs'
|
|
15
|
+
import { flowStatePath, readState, recordTransition } from './state-store.mjs'
|
|
16
|
+
import { createRunner } from './subagent-runner.mjs'
|
|
17
|
+
|
|
18
|
+
/** Ліміт diff у промпті (символів) — щоб не роздувати контекст рецензента. */
|
|
19
|
+
const DIFF_LIMIT = 12_000
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Текст diff від base: `base...HEAD` (закомічене) + `git diff` (робоче дерево).
|
|
23
|
+
* @param {string} base базовий комміт
|
|
24
|
+
* @param {(cmd: string, args: string[], opts: object) => { stdout: string }} run git-runner
|
|
25
|
+
* @param {string} cwd worktree
|
|
26
|
+
* @returns {string} склеєний diff (trim)
|
|
27
|
+
*/
|
|
28
|
+
export function diffFromBase(base, run, cwd) {
|
|
29
|
+
const committed = run('git', ['diff', `${base}...HEAD`], { cwd })
|
|
30
|
+
const working = run('git', ['diff'], { cwd })
|
|
31
|
+
return `${committed.stdout ?? ''}\n${working.stdout ?? ''}`.trim()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Промпт adversarial-рецензента (читає ЛИШЕ diff).
|
|
36
|
+
* @param {string} diff текст diff
|
|
37
|
+
* @returns {string} промпт
|
|
38
|
+
*/
|
|
39
|
+
export function reviewerPrompt(diff) {
|
|
40
|
+
return [
|
|
41
|
+
'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells ЛИШЕ в цьому diff.',
|
|
42
|
+
'Поверни ЛИШЕ JSON-масив: [{ "severity": "high|med|low", "file": "...", "issue": "...", "suggestion": "..." }].',
|
|
43
|
+
'Якщо проблем нема — поверни [].',
|
|
44
|
+
'',
|
|
45
|
+
diff.slice(0, DIFF_LIMIT)
|
|
46
|
+
].join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Парсить findings із відповіді рецензента. Fail-soft: сміття/невалідний JSON → [].
|
|
51
|
+
* @param {string} text відповідь субагента
|
|
52
|
+
* @returns {{ severity?: string, file?: string, issue?: string, suggestion?: string }[]} findings
|
|
53
|
+
*/
|
|
54
|
+
export function parseFindings(text) {
|
|
55
|
+
const s = String(text)
|
|
56
|
+
const start = s.indexOf('[')
|
|
57
|
+
const end = s.lastIndexOf(']')
|
|
58
|
+
if (start === -1 || end === -1 || end < start) return []
|
|
59
|
+
try {
|
|
60
|
+
const arr = JSON.parse(s.slice(start, end + 1))
|
|
61
|
+
return Array.isArray(arr) ? arr : []
|
|
62
|
+
} catch {
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Дедуплікує findings за (file, issue).
|
|
69
|
+
* @param {object[]} findings вхідні
|
|
70
|
+
* @returns {object[]} без дублікатів
|
|
71
|
+
*/
|
|
72
|
+
export function dedupeFindings(findings) {
|
|
73
|
+
const seen = new Set()
|
|
74
|
+
const out = []
|
|
75
|
+
for (const f of findings) {
|
|
76
|
+
const key = `${f?.file ?? ''}::${f?.issue ?? ''}`
|
|
77
|
+
if (seen.has(key)) continue
|
|
78
|
+
seen.add(key)
|
|
79
|
+
out.push(f)
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Іконка за severity finding-а.
|
|
86
|
+
* @param {string} severity рівень
|
|
87
|
+
* @returns {string} емодзі
|
|
88
|
+
*/
|
|
89
|
+
function severityIcon(severity) {
|
|
90
|
+
if (severity === 'high') return '🔴'
|
|
91
|
+
if (severity === 'med') return '🟡'
|
|
92
|
+
return '⚪'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `flow review` — спавнить adversarial-рецензента(ів) на diff від base.
|
|
97
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
98
|
+
* @param {{ cwd?: string, log?: (m: string) => void, run?: (cmd: string, args: string[], opts: object) => { stdout: string }, runner?: object, now?: () => number }} [deps] ін'єкції
|
|
99
|
+
* @returns {Promise<number>} exit code (0 завжди — інформативна; 1 лише якщо нема стану/runner)
|
|
100
|
+
*/
|
|
101
|
+
export async function review(_rest, deps = {}) {
|
|
102
|
+
const cwd = deps.cwd ?? processCwd()
|
|
103
|
+
const log = deps.log ?? console.error
|
|
104
|
+
const run = deps.run ?? realRun
|
|
105
|
+
const now = deps.now ?? Date.now
|
|
106
|
+
|
|
107
|
+
const statePath = flowStatePath(cwd)
|
|
108
|
+
const state = readState(statePath)
|
|
109
|
+
if (!state) {
|
|
110
|
+
log('review: стану нема — спершу `flow init`')
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const base = state.metadata?.base_commit ?? 'HEAD~1'
|
|
115
|
+
const diff = diffFromBase(base, run, cwd)
|
|
116
|
+
if (!diff) {
|
|
117
|
+
log('review: нема змін від base — нічого ревʼювити')
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let runner = deps.runner
|
|
122
|
+
if (!runner) {
|
|
123
|
+
try {
|
|
124
|
+
runner = await createRunner(deps)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
log(`review: ${error.message}`)
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const reviewers = reviewersForLevel(state.level ?? 1)
|
|
132
|
+
const prompt = reviewerPrompt(diff)
|
|
133
|
+
const results = await Promise.all(Array.from({ length: reviewers }, () => runner.runStep(prompt, { cwd })))
|
|
134
|
+
const findings = dedupeFindings(results.flatMap(r => (r.ok ? parseFindings(r.output) : [])))
|
|
135
|
+
|
|
136
|
+
recordTransition(
|
|
137
|
+
{ statePath, eventsPath: flowEventsPath(cwd) },
|
|
138
|
+
{ type: 'review', findings: findings.length },
|
|
139
|
+
s => ({ ...s, review: { at: new Date(now()).toISOString(), reviewers, findings } }),
|
|
140
|
+
now
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for (const f of findings) {
|
|
144
|
+
log(`${severityIcon(f.severity)} ${f.file ?? '?'}: ${f.issue ?? ''}`)
|
|
145
|
+
}
|
|
146
|
+
const high = findings.filter(f => f.severity === 'high').length
|
|
147
|
+
if (high > 0) log(`⚠️ review: ${high} high-severity — рекомендовано виправити перед release`)
|
|
148
|
+
log(`review: ${findings.length} findings (рецензентів: ${reviewers})`)
|
|
149
|
+
return 0
|
|
150
|
+
}
|
package/scripts/lib/worktree.mjs
CHANGED
|
@@ -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
|
|
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 абсолютний корінь репозиторію
|
package/scripts/worktree-cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}`)
|