@nitra/cursor 1.36.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 +12 -0
- package/README.md +7 -1
- package/bin/n-cursor.js +16 -3
- package/package.json +1 -1
- package/rules/graphql/js/tooling.mjs +3 -1
- package/rules/npm-module/js/package_structure.mjs +4 -1
- package/rules/npm-module/js/skill_meta.mjs +61 -0
- package/rules/release/change.mjs +3 -1
- package/rules/release/lib/change-file.mjs +3 -1
- package/rules/release/release.mjs +3 -1
- package/rules/test/js/data/stryker_config/stryker-vue-macros-ignorer.mjs +2 -1
- package/rules/test/js/stryker_config.mjs +6 -8
- package/rules/vue/js/packages.mjs +1 -9
- package/rules/worktree/worktree.mdc +34 -0
- package/schemas/skill-meta.json +22 -0
- package/schemas/v8r-catalog.json +5 -0
- package/scripts/auto-skills.mjs +14 -37
- package/scripts/lib/skill-meta.mjs +54 -0
- package/scripts/lib/worktree-notice.mjs +55 -0
- package/scripts/lib/worktree.mjs +73 -0
- package/scripts/worktree-cli.mjs +200 -0
- package/skills/adr-normalize/meta.json +1 -0
- package/skills/coverage-fix/meta.json +1 -0
- package/skills/fix/meta.json +1 -0
- package/skills/fix-tests/meta.json +1 -0
- package/skills/lint/meta.json +1 -0
- package/skills/llm-patch/meta.json +1 -0
- package/skills/publish-telegram/meta.json +1 -0
- package/skills/start-check/meta.json +1 -0
- package/skills/taze/meta.json +1 -0
- package/skills/worktree/SKILL.md +38 -0
- package/skills/worktree/meta.json +1 -0
- package/skills/adr-normalize/auto.md +0 -1
- package/skills/coverage-fix/auto.md +0 -1
- package/skills/fix/auto.md +0 -1
- package/skills/fix-tests/auto.md +0 -1
- package/skills/lint/auto.md +0 -1
- package/skills/llm-patch/auto.md +0 -1
- package/skills/publish-telegram/auto.md +0 -1
- package/skills/start-check/auto.md +0 -1
- package/skills/taze/auto.md +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [1.37.0] - 2026-05-31
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- skills: meta.json замість auto.md (+ worktree-прапорець з вшиванням у SKILL.md і забороною паралелі)
|
|
14
|
+
|
|
3
15
|
## [1.36.0] - 2026-05-31
|
|
4
16
|
|
|
5
17
|
### Added
|
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.
|
|
@@ -114,7 +120,7 @@ npm/
|
|
|
114
120
|
```
|
|
115
121
|
npm/rules/<id>/
|
|
116
122
|
├── <id>.mdc # текст правила (після синку — .cursor/rules/n-<id>.mdc)
|
|
117
|
-
├──
|
|
123
|
+
├── meta.json # метадані скілу: auto (автоактивація) + worktree
|
|
118
124
|
├── js/ # JS для `npx @nitra/cursor fix`
|
|
119
125
|
│ └── <concern>/
|
|
120
126
|
│ ├── 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'
|
|
@@ -101,6 +103,7 @@ import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
|
|
|
101
103
|
import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
|
|
102
104
|
import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
103
105
|
import { runSkillsCli } from '../scripts/skills-cli.mjs'
|
|
106
|
+
import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
|
|
104
107
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
105
108
|
import { runLintCli } from '../scripts/lib/run-lint-cli.mjs'
|
|
106
109
|
import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
|
|
@@ -762,10 +765,15 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
|
762
765
|
process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${destDirName} ... `)
|
|
763
766
|
try {
|
|
764
767
|
await mkdir(destDir, { recursive: true })
|
|
768
|
+
const meta = readSkillMetaRaw(srcDir)
|
|
769
|
+
const worktree = meta?.worktree === true
|
|
765
770
|
const files = await readdir(srcDir)
|
|
766
771
|
for (const file of files) {
|
|
767
|
-
if (file === '
|
|
768
|
-
|
|
772
|
+
if (file === 'meta.json') continue
|
|
773
|
+
let content = await readFile(join(srcDir, file), 'utf8')
|
|
774
|
+
if (file === 'SKILL.md') {
|
|
775
|
+
content = injectWorktreeNotice(content, worktree)
|
|
776
|
+
}
|
|
769
777
|
await writeFile(join(destDir, file), content, 'utf8')
|
|
770
778
|
}
|
|
771
779
|
console.log(`✅`)
|
|
@@ -1518,6 +1526,11 @@ try {
|
|
|
1518
1526
|
|
|
1519
1527
|
break
|
|
1520
1528
|
}
|
|
1529
|
+
case 'worktree': {
|
|
1530
|
+
process.exitCode = await runWorktreeCli(args)
|
|
1531
|
+
|
|
1532
|
+
break
|
|
1533
|
+
}
|
|
1521
1534
|
case undefined:
|
|
1522
1535
|
case '': {
|
|
1523
1536
|
await runSync()
|
|
@@ -1527,7 +1540,7 @@ try {
|
|
|
1527
1540
|
default: {
|
|
1528
1541
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1529
1542
|
console.error(
|
|
1530
|
-
` Очікується: (без аргументів) синхронізація правил, 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`
|
|
1531
1544
|
)
|
|
1532
1545
|
process.exitCode = 1
|
|
1533
1546
|
}
|
package/package.json
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
+
}
|
package/rules/release/change.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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,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,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
|
+
}
|
package/schemas/v8r-catalog.json
CHANGED
|
@@ -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
|
}
|
package/scripts/auto-skills.mjs
CHANGED
|
@@ -1,56 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Автовизначення skills для `.n-cursor.json` за умовами
|
|
2
|
+
* Автовизначення skills для `.n-cursor.json` за умовами з `npm/skills/<skill>/meta.json`.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
4
|
+
* `meta.json` — джерело правди (а не hardcoded мапа). Підтримуються три варіанти:
|
|
5
5
|
*
|
|
6
|
-
* -
|
|
6
|
+
* - `auto: "завжди"` — скіл активується незалежно від інших правил
|
|
7
7
|
* (приклади: `fix`, `lint`, `llm-patch`, `publish-telegram`).
|
|
8
|
-
* - `[rule,
|
|
9
|
-
* auto-rules (приклади: `adr-normalize - [adr]`, `taze - [bun]`).
|
|
10
|
-
* -
|
|
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
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
64
|
-
if (!
|
|
65
|
-
const spec = parseSkillAutoSpec(
|
|
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,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 @@
|
|
|
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 }
|
|
@@ -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 }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[adr]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[js-lint]
|
package/skills/fix/auto.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
завжди
|
package/skills/fix-tests/auto.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[js-lint]
|
package/skills/lint/auto.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
завжди
|
package/skills/llm-patch/auto.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
завжди
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
завжди
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[bun]
|
package/skills/taze/auto.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[bun]
|