@nitra/cursor 1.22.0 → 1.24.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/.pi-template/extensions/n-cursor-adr/index.ts +104 -0
- package/.pi-template/extensions/n-cursor-adr/tsconfig.json +16 -0
- package/CHANGELOG.md +22 -0
- package/bin/n-cursor.js +1 -0
- package/package.json +8 -2
- package/scripts/lib/run-lint-cli.mjs +0 -2
- package/scripts/lib/timing-summary.mjs +4 -5
- package/scripts/sync-claude-config.mjs +75 -5
- package/skills/coverage-fix/SKILL.md +114 -0
- package/skills/coverage-fix/auto.md +1 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi.dev extension: ADR capture + normalize.
|
|
3
|
+
*
|
|
4
|
+
* На pi `agent_end` event серіалізує `ctx.sessionManager.getEntries()` у
|
|
5
|
+
* Claude-сумісний JSONL у tmpdir, формує stdin JSON і спавнить існуючі
|
|
6
|
+
* `.claude/hooks/{capture,normalize}-decisions.sh` через `pi.exec`.
|
|
7
|
+
*
|
|
8
|
+
* Логіка skip/throttle/LLM-CLI-selection лишається у bash — TS лише
|
|
9
|
+
* адаптер pi → bash. Recursion guard через env vars, що їх bash виставляє
|
|
10
|
+
* перед спавном LLM CLI.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
14
|
+
import { writeFileSync } from 'node:fs'
|
|
15
|
+
import { tmpdir } from 'node:os'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { env } from 'node:process'
|
|
18
|
+
|
|
19
|
+
interface PiContext {
|
|
20
|
+
cwd: string
|
|
21
|
+
sessionId?: string
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
sessionManager: { getEntries(): Array<{ message?: { role?: string; content?: unknown } }> }
|
|
24
|
+
ui?: { notify?: (msg: string, level?: 'info' | 'warning' | 'error') => void }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PiExec {
|
|
28
|
+
exec: (
|
|
29
|
+
cmd: string,
|
|
30
|
+
args: string[],
|
|
31
|
+
opts?: {
|
|
32
|
+
cwd?: string
|
|
33
|
+
env?: Record<string, string>
|
|
34
|
+
input?: string
|
|
35
|
+
signal?: AbortSignal
|
|
36
|
+
timeout?: number
|
|
37
|
+
}
|
|
38
|
+
) => Promise<{ code: number; stdout: string; stderr: string }>
|
|
39
|
+
on: (
|
|
40
|
+
event: string,
|
|
41
|
+
handler: (event: unknown, ctx: PiContext) => Promise<void> | void
|
|
42
|
+
) => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CAPTURE_HOOK = '.claude/hooks/capture-decisions.sh'
|
|
46
|
+
const NORMALIZE_HOOK = '.claude/hooks/normalize-decisions.sh'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pi extension entry point.
|
|
50
|
+
* @param {PiExec} pi pi.dev extension API
|
|
51
|
+
*/
|
|
52
|
+
export default function (pi: PiExec): void {
|
|
53
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
54
|
+
// Recursion guard: bash спавнить LLM CLI (claude/cursor-agent), той може
|
|
55
|
+
// стартувати pi-сесію. Bash виставляє ці env-vars перед спавном — child
|
|
56
|
+
// inheritance ловить рекурсивний trigger тут.
|
|
57
|
+
if (env.CAPTURE_DECISIONS_RUNNING || env.ADR_NORMALIZE_RUNNING) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let jsonlPath: string
|
|
62
|
+
try {
|
|
63
|
+
const entries = ctx.sessionManager.getEntries()
|
|
64
|
+
const lines = entries
|
|
65
|
+
.filter(e => e.message?.role === 'user' || e.message?.role === 'assistant')
|
|
66
|
+
.map(e => JSON.stringify({ type: e.message?.role, message: e.message }))
|
|
67
|
+
.join('\n')
|
|
68
|
+
jsonlPath = join(tmpdir(), `n-cursor-pi-transcript-${Date.now()}-${randomUUID()}.jsonl`)
|
|
69
|
+
writeFileSync(jsonlPath, lines + '\n', 'utf8')
|
|
70
|
+
} catch (error) {
|
|
71
|
+
ctx.ui?.notify?.(
|
|
72
|
+
`@nitra/cursor: transcript serialization failed — ${(error as Error).message}`,
|
|
73
|
+
'error'
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stdinPayload = JSON.stringify({
|
|
79
|
+
transcript_path: jsonlPath,
|
|
80
|
+
session_id: ctx.sessionId ?? randomUUID()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const envOverride = { ...env, CLAUDE_PROJECT_DIR: ctx.cwd } as Record<string, string>
|
|
84
|
+
|
|
85
|
+
// Async, не блокує agent_end. Якщо bash-скриптів немає (pi-only консьюмер
|
|
86
|
+
// із claude-config: false) — pi.exec поверне ENOENT, ловимо у allSettled.
|
|
87
|
+
await Promise.allSettled([
|
|
88
|
+
pi.exec('bash', [CAPTURE_HOOK], {
|
|
89
|
+
cwd: ctx.cwd,
|
|
90
|
+
env: envOverride,
|
|
91
|
+
input: stdinPayload,
|
|
92
|
+
signal: ctx.signal,
|
|
93
|
+
timeout: 180_000
|
|
94
|
+
}),
|
|
95
|
+
pi.exec('bash', [NORMALIZE_HOOK], {
|
|
96
|
+
cwd: ctx.cwd,
|
|
97
|
+
env: envOverride,
|
|
98
|
+
input: stdinPayload,
|
|
99
|
+
signal: ctx.signal,
|
|
100
|
+
timeout: 600_000
|
|
101
|
+
})
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "TS-конфіг для pi.dev extension. Не компілюється сам пакетом (синкається як є у .pi/extensions/<name>/), потрібен лише для IDE/TS-сервера у проєкті-споживачі, щоб резолвити node:* модулі. Споживачу треба мати @types/node у devDependencies (зазвичай уже є транзитивно).",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"lib": ["ES2022"],
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts"]
|
|
16
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.24.0] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`.pi-template/extensions/n-cursor-adr/tsconfig.json`** — мінімальний TS-конфіг шаблону pi.dev extension, який синкається у `.pi/extensions/n-cursor-adr/` разом із `index.ts`. Дозволяє IDE/TS-серверу резолвити `node:*` модулі без project-wide tsconfig у проєкті-споживачі (`types: ["node"]`, `module/target: ES2022 + NodeNext`, `noEmit`, `isolatedModules`). Споживачу потрібен `@types/node` у devDeps (зазвичай уже є транзитивно).
|
|
12
|
+
- **`pi` manifest у `npm/package.json`** — `{"skills":"skills","extensions":".pi-template/extensions"}`. Pi.dev під час `pi install npm:@nitra/cursor` тепер бачить explicit-шляхи до bundled-ресурсів, замість convention-auto-discovery. `extensions: ".pi-template/extensions"` критичний — pi за замовч. шукає `extensions/` у корені пакета, а у нас шлях нестандартний.
|
|
13
|
+
- **`"pi-package"` keyword** у `npm/package.json` — пакет з’являється у pi-gallery для discoverability.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`syncPiExtensions`** тепер копіює **всі файли** з теки `.pi-template/extensions/<name>/`, а не лише `index.ts`. Контракт повернення: `{ written, path, files }` — `path` тепер тека (а не файл), `files` — відсортований список базових імен. У `🤖 Claude-конфіг` логу та у `result.piExtension` caller-стороні виводиться тека.
|
|
18
|
+
- **`.pi-template/extensions/n-cursor-adr/index.ts`** — порядок імпортів виправлено (всі `node:*` імпорти підняті на самий верх, перед `interface PiContext`). Усувало `import/first` ESLint-помилку; функціонально без змін.
|
|
19
|
+
|
|
20
|
+
## [1.23.0] - 2026-05-25
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Pi.dev ADR hooks** — bundled TS-extension `npm/.pi-template/extensions/n-cursor-adr/index.ts` копіюється у `.pi/extensions/n-cursor-adr/index.ts` проєкту-споживача коли `adr` ∈ `.n-cursor.json#rules`. На pi `agent_end` event серіалізує `ctx.sessionManager.getEntries()` у Claude-сумісний JSONL у `tmpdir()`, спавнить існуючі `.claude/hooks/{capture,normalize}-decisions.sh` через `pi.exec` (async, `signal: ctx.signal`, timeouts 180s/600s). Жодного дублювання bash-логіки: skip/throttle/LLM-CLI-selection лишається у bash. Recursion guard через env-vars `CAPTURE_DECISIONS_RUNNING` / `ADR_NORMALIZE_RUNNING`, які bash виставляє перед спавном LLM CLI.
|
|
25
|
+
- `npm/scripts/sync-claude-config.mjs`: експорт `PI_DIR`, `PI_EXTENSIONS_DIR`, `PI_TEMPLATE_DIR_NAME`, `PI_EXTENSION_NAME`; нова функція `syncPiExtensions(projectRoot, bundledPackageRoot)` (copy) і `removeOrphanPiExtension(projectRoot)` (cleanup); поле `piExtension: boolean` у відповіді `syncClaudeConfig` (gated на `adr` ∈ rules).
|
|
26
|
+
- `npm/package.json` `files` array: додано `.pi-template` — bundled-директорія шипиться разом із пакетом.
|
|
27
|
+
- `npm/bin/n-cursor.js`: у `🤖 Claude-конфіг`-логу після sync додається `.pi/extensions/n-cursor-adr/index.ts` коли pi-extension згенерована.
|
|
28
|
+
|
|
7
29
|
## [1.22.0] - 2026-05-25
|
|
8
30
|
|
|
9
31
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1402,6 +1402,7 @@ async function runSync() {
|
|
|
1402
1402
|
if (result.adrHook) parts.push('.claude/hooks/capture-decisions.sh')
|
|
1403
1403
|
if (result.adrNormalizeHook) parts.push('.claude/hooks/normalize-decisions.sh')
|
|
1404
1404
|
if (result.gitignoreAdr) parts.push('.gitignore (adr fragment)')
|
|
1405
|
+
if (result.piExtension) parts.push('.pi/extensions/n-cursor-adr/')
|
|
1405
1406
|
if (parts.length > 0) {
|
|
1406
1407
|
console.log(`🤖 Claude-конфіг: ${parts.join(', ')}`)
|
|
1407
1408
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"cursor",
|
|
8
8
|
"cursor-rules",
|
|
9
9
|
"mdc",
|
|
10
|
-
"n"
|
|
10
|
+
"n",
|
|
11
|
+
"pi-package"
|
|
11
12
|
],
|
|
13
|
+
"pi": {
|
|
14
|
+
"skills": "skills",
|
|
15
|
+
"extensions": ".pi-template/extensions"
|
|
16
|
+
},
|
|
12
17
|
"homepage": "https://github.com/n/cursor#readme",
|
|
13
18
|
"bugs": {
|
|
14
19
|
"url": "https://github.com/n/cursor/issues"
|
|
@@ -31,6 +36,7 @@
|
|
|
31
36
|
"scripts",
|
|
32
37
|
"skills",
|
|
33
38
|
".claude-template",
|
|
39
|
+
".pi-template",
|
|
34
40
|
"AGENTS.template.md",
|
|
35
41
|
"CHANGELOG.md",
|
|
36
42
|
"!**/*.test.mjs",
|
|
@@ -41,7 +41,6 @@ export const LINT_SCRIPTS = /** @type {const} */ ([
|
|
|
41
41
|
/**
|
|
42
42
|
* Читає `scripts` з `package.json` у заданій теці. Повертає `null`, якщо файла немає, JSON
|
|
43
43
|
* некоректний або поля `scripts` нема. Не кидає — викликач сам вирішує, що робити.
|
|
44
|
-
*
|
|
45
44
|
* @param {string} root абсолютний шлях до теки з `package.json`
|
|
46
45
|
* @returns {Record<string, string> | null} мапа scripts або null
|
|
47
46
|
*/
|
|
@@ -75,7 +74,6 @@ function readRootScripts(root) {
|
|
|
75
74
|
/**
|
|
76
75
|
* Виконує лінт-ланцюжок з тайменгом. Повертає exit-код, не кидає винятків (для прямого
|
|
77
76
|
* присвоєння у `process.exitCode`).
|
|
78
|
-
*
|
|
79
77
|
* @param {RunLintCliOptions} [options] DI для тестів (мокаємо spawn / fs / clock)
|
|
80
78
|
* @returns {number} 0 = успіх, ненульовий = code першого впалого скрипта, або 1 при структурних проблемах
|
|
81
79
|
*/
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* Час виводиться як `<ціла>.<десята>s`, навіть для субсекундних інтервалів — щоб одиниця була стабільна.
|
|
10
10
|
*
|
|
11
11
|
* Маркер `❌` на рядку — якщо `ok === false`.
|
|
12
|
-
*
|
|
13
12
|
* @typedef {{ id: string, ms: number, ok: boolean }} TimingEntry
|
|
14
13
|
*/
|
|
15
14
|
|
|
@@ -20,7 +19,6 @@ const RULER = '─'
|
|
|
20
19
|
* Форматує мілісекунди як `<sec>.<десята>s`. Округлення до десятої — нижнє (floor), щоб
|
|
21
20
|
* однаковий ms давав однаковий вивід у різних таблицях незалежно від платформи (Number.prototype.toFixed
|
|
22
21
|
* робить round-half-to-even, що для 950ms дає `0.9s` — приймаємо).
|
|
23
|
-
*
|
|
24
22
|
* @param {number} ms тривалість у мілісекундах (>= 0)
|
|
25
23
|
* @returns {string} наприклад `0.0s`, `1.2s`, `12.3s`
|
|
26
24
|
*/
|
|
@@ -44,7 +42,6 @@ export function formatDurationMs(ms) {
|
|
|
44
42
|
*
|
|
45
43
|
* Ширина колонки id вирівнюється під найдовший id у списку. Мінімальна ширина riski — 14
|
|
46
44
|
* (узгоджено з типовою довжиною заголовків `fix-js-lint` / `lint-security`).
|
|
47
|
-
*
|
|
48
45
|
* @param {string} title заголовок таблиці (наприклад, `Fix timing` або `Lint timing`)
|
|
49
46
|
* @param {TimingEntry[]} timings записи в порядку запуску — друкуються як є, не сортуються
|
|
50
47
|
* @returns {string} готовий до stdout текст з кінцевим `\n`
|
|
@@ -61,7 +58,9 @@ export function formatTimingSummary(title, timings) {
|
|
|
61
58
|
const failMark = ok ? '' : ' ❌'
|
|
62
59
|
lines.push(` ${id.padEnd(idWidth)} ${formatDurationMs(ms)}${failMark}`)
|
|
63
60
|
}
|
|
64
|
-
lines.push(
|
|
65
|
-
|
|
61
|
+
lines.push(
|
|
62
|
+
` ${RULER.repeat(idWidth + 2 + 6)}`,
|
|
63
|
+
` ${'total'.padEnd(idWidth)} ${formatDurationMs(totalMs)}`
|
|
64
|
+
)
|
|
66
65
|
return `${lines.join('\n')}\n`
|
|
67
66
|
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* Опт-аут — `claude-config: false` у `.n-cursor.json`.
|
|
28
28
|
*/
|
|
29
29
|
import { existsSync } from 'node:fs'
|
|
30
|
-
import { chmod, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
|
30
|
+
import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
31
31
|
import { join } from 'node:path'
|
|
32
32
|
|
|
33
33
|
/** Маркер PostToolUse fix-hook'а (`npx --no \@nitra/cursor post-tool-use-fix`). */
|
|
@@ -62,6 +62,15 @@ const CURSOR_HOOKS_FILE = `${CURSOR_DIR}/hooks.json`
|
|
|
62
62
|
const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
63
63
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
64
64
|
const TEMPLATE_DIR_NAME = '.claude-template'
|
|
65
|
+
|
|
66
|
+
/** Корінь pi.dev артефактів у проєкті-споживачі. */
|
|
67
|
+
export const PI_DIR = '.pi'
|
|
68
|
+
/** Директорія pi.dev TS-extensions у проєкті-споживачі. */
|
|
69
|
+
export const PI_EXTENSIONS_DIR = `${PI_DIR}/extensions`
|
|
70
|
+
/** Назва bundled-директорії pi-template у пакеті `@nitra/cursor`. */
|
|
71
|
+
export const PI_TEMPLATE_DIR_NAME = '.pi-template'
|
|
72
|
+
/** Імʼя bundled pi-extension'а для ADR capture/normalize. */
|
|
73
|
+
export const PI_EXTENSION_NAME = 'n-cursor-adr'
|
|
65
74
|
/** Відносний шлях до канонічного фрагмента `.gitignore` для ADR Stop-hook'ів у tarball пакета. */
|
|
66
75
|
export const ADR_GITIGNORE_SNIPPET_REL = 'rules/adr/js/templates/hooks/.gitignore.snippet'
|
|
67
76
|
const GITIGNORE_FILE = '.gitignore'
|
|
@@ -405,6 +414,61 @@ export function syncAdrNormalizeHookScript(projectRoot, templateDir) {
|
|
|
405
414
|
return syncHookScript(projectRoot, templateDir, ADR_NORMALIZE_HOOK_SCRIPT_NAME)
|
|
406
415
|
}
|
|
407
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Копіює bundled pi.dev TS-extension `npm/.pi-template/extensions/n-cursor-adr/` (усі файли —
|
|
419
|
+
* `index.ts`, `tsconfig.json`, потенційні `package.json`/`.gitignore` тощо) у
|
|
420
|
+
* `.pi/extensions/n-cursor-adr/` проєкту-споживача. Тека fully-owned: при кожному sync-у
|
|
421
|
+
* перезаписується. Якщо bundled template відсутній (legacy-версії пакета без `.pi-template/`)
|
|
422
|
+
* або в ньому немає `index.ts` — повертаємо `{written: false}` без помилки.
|
|
423
|
+
*
|
|
424
|
+
* Розширення поверх `index.ts` (tsconfig тощо) потрібні, бо `.pi/extensions/` синкається як є
|
|
425
|
+
* у проєкти-споживачі, а IDE/TS-сервер мусить резолвити `node:*` модулі без додаткових
|
|
426
|
+
* project-wide конфігів.
|
|
427
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
428
|
+
* @param {string} bundledPackageRoot корінь установленого `@nitra/cursor` (із `.pi-template/`)
|
|
429
|
+
* @returns {Promise<{ written: boolean, path: string, files: string[] }>} чи писали; відносний шлях до теки розширення; список скопійованих базових імен (відсортований)
|
|
430
|
+
*/
|
|
431
|
+
export async function syncPiExtensions(projectRoot, bundledPackageRoot) {
|
|
432
|
+
const srcDir = join(bundledPackageRoot, PI_TEMPLATE_DIR_NAME, 'extensions', PI_EXTENSION_NAME)
|
|
433
|
+
const indexPath = join(srcDir, 'index.ts')
|
|
434
|
+
if (!existsSync(indexPath)) {
|
|
435
|
+
return { written: false, path: '', files: [] }
|
|
436
|
+
}
|
|
437
|
+
const destDir = join(projectRoot, PI_EXTENSIONS_DIR, PI_EXTENSION_NAME)
|
|
438
|
+
await mkdir(destDir, { recursive: true })
|
|
439
|
+
const entries = await readdir(srcDir, { withFileTypes: true })
|
|
440
|
+
const copied = []
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
if (!entry.isFile()) continue
|
|
443
|
+
const name = entry.name
|
|
444
|
+
const content = await readFile(join(srcDir, name), 'utf8')
|
|
445
|
+
await writeFile(join(destDir, name), content, 'utf8')
|
|
446
|
+
copied.push(name)
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
written: true,
|
|
450
|
+
path: `${PI_EXTENSIONS_DIR}/${PI_EXTENSION_NAME}`,
|
|
451
|
+
files: copied.toSorted((a, b) => a.localeCompare(b))
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Видаляє `.pi/extensions/n-cursor-adr/` директорію з проєкту-споживача.
|
|
457
|
+
* Викликається коли правило `adr` вимкнено у `.n-cursor.json` (симетрично до
|
|
458
|
+
* cleanup-у `.claude/hooks/{capture,normalize}-decisions.sh`).
|
|
459
|
+
*
|
|
460
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
461
|
+
* @returns {Promise<{ removed: boolean, path: string }>} чи було щось видалено та відносний шлях
|
|
462
|
+
*/
|
|
463
|
+
export async function removeOrphanPiExtension(projectRoot) {
|
|
464
|
+
const extDir = join(projectRoot, PI_EXTENSIONS_DIR, PI_EXTENSION_NAME)
|
|
465
|
+
if (!existsSync(extDir)) {
|
|
466
|
+
return { removed: false, path: '' }
|
|
467
|
+
}
|
|
468
|
+
await rm(extDir, { recursive: true, force: true })
|
|
469
|
+
return { removed: true, path: `${PI_EXTENSIONS_DIR}/${PI_EXTENSION_NAME}` }
|
|
470
|
+
}
|
|
471
|
+
|
|
408
472
|
/**
|
|
409
473
|
* Повертає змістовні (не коментар, не порожній) рядки з text-фрагмента `.gitignore`.
|
|
410
474
|
* @param {string} raw вміст snippet-файлу
|
|
@@ -494,7 +558,7 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
494
558
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
495
559
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
496
560
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
497
|
-
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, gitignoreAdr: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore
|
|
561
|
+
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, gitignoreAdr: boolean, piExtension: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore`/pi-extension та список slash-команд
|
|
498
562
|
*/
|
|
499
563
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
500
564
|
if (!enabled) {
|
|
@@ -504,7 +568,8 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
504
568
|
commands: [],
|
|
505
569
|
adrHook: false,
|
|
506
570
|
adrNormalizeHook: false,
|
|
507
|
-
gitignoreAdr: false
|
|
571
|
+
gitignoreAdr: false,
|
|
572
|
+
piExtension: false
|
|
508
573
|
}
|
|
509
574
|
}
|
|
510
575
|
const templateDir = join(bundledPackageRoot, TEMPLATE_DIR_NAME)
|
|
@@ -515,7 +580,8 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
515
580
|
commands: [],
|
|
516
581
|
adrHook: false,
|
|
517
582
|
adrNormalizeHook: false,
|
|
518
|
-
gitignoreAdr: false
|
|
583
|
+
gitignoreAdr: false,
|
|
584
|
+
piExtension: false
|
|
519
585
|
}
|
|
520
586
|
}
|
|
521
587
|
const includeAdrHook = Array.isArray(rules) && rules.includes('adr')
|
|
@@ -526,6 +592,9 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
526
592
|
const gitignoreAdr = includeAdrHook
|
|
527
593
|
? await syncGitignoreAdrFragment(projectRoot, bundledPackageRoot)
|
|
528
594
|
: { written: false, path: '' }
|
|
595
|
+
const piExtension = includeAdrHook
|
|
596
|
+
? await syncPiExtensions(projectRoot, bundledPackageRoot)
|
|
597
|
+
: await removeOrphanPiExtension(projectRoot).then(r => ({ written: false, path: r.path }))
|
|
529
598
|
const settings = await syncClaudeSettings(projectRoot, templateDir, { includeAdrHook })
|
|
530
599
|
const cursorHooks = await syncCursorHooksConfig(projectRoot, { includeAdrHook })
|
|
531
600
|
const commands = await syncClaudeCommands(projectRoot, templateDir)
|
|
@@ -535,6 +604,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
535
604
|
commands,
|
|
536
605
|
adrHook: adrHook.written,
|
|
537
606
|
adrNormalizeHook: adrNormalizeHook.written,
|
|
538
|
-
gitignoreAdr: gitignoreAdr.written
|
|
607
|
+
gitignoreAdr: gitignoreAdr.written,
|
|
608
|
+
piExtension: piExtension.written
|
|
539
609
|
}
|
|
540
610
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-coverage-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Автономна команда: запускає coverage, читає ## Recommendations у COVERAGE.md, ітеративно пише тести для вижилих мутантів до конвергенції
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /n-coverage-fix — ітеративне підвищення mutation score
|
|
8
|
+
|
|
9
|
+
## Важливо
|
|
10
|
+
|
|
11
|
+
⚠️ Не запускати паралельно з іншим `/n-coverage-fix` або `bun coverage` — Stryker пише `mutation.json` і `incremental.json` в одну директорію. `n-cursor coverage` всередині вже серіалізований через `withLock('coverage')`, але паралельний запуск двох ітерацій скілу зіпсує дані.
|
|
12
|
+
|
|
13
|
+
## Мета
|
|
14
|
+
|
|
15
|
+
Автономно підвищити mutation score: запустити `bun coverage`, записати тести для вижилих мутантів, повторити до конвергенції (score перестав зростати).
|
|
16
|
+
|
|
17
|
+
## Алгоритм
|
|
18
|
+
|
|
19
|
+
### Крок 1: Запусти coverage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun coverage
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
(або `bun run coverage` якщо команда у `package.json`)
|
|
26
|
+
|
|
27
|
+
Чекай завершення. Якщо команди немає у `package.json` — запусти `n-cursor coverage` з кореня.
|
|
28
|
+
|
|
29
|
+
### Крок 2: Прочитай вижилих мутантів
|
|
30
|
+
|
|
31
|
+
Прочитай `COVERAGE.md` — знайди секцію `## Recommendations`.
|
|
32
|
+
|
|
33
|
+
Якщо секції немає або вона порожня:
|
|
34
|
+
```
|
|
35
|
+
✓ Нема вижилих мутантів — mutation score повний
|
|
36
|
+
```
|
|
37
|
+
→ DONE
|
|
38
|
+
|
|
39
|
+
Запам'ятай поточний mutation score як `baseline_score` (рядок `| **Разом** |` з таблиці у COVERAGE.md).
|
|
40
|
+
|
|
41
|
+
### Крок 3: Для кожного файлу з Recommendations — пиши тести
|
|
42
|
+
|
|
43
|
+
Для кожного `### <file>` у секції:
|
|
44
|
+
|
|
45
|
+
**3a. Читай контекст:**
|
|
46
|
+
- Source-файл (`<file>` від кореня проєкту)
|
|
47
|
+
- Таблицю вижилих мутантів: рядок, оригінал, заміна, тип
|
|
48
|
+
- Блок `**Приклад наявного тесту:**` — style guide для нових тестів
|
|
49
|
+
|
|
50
|
+
**3b. Знайди тестовий файл:**
|
|
51
|
+
Перший що існує:
|
|
52
|
+
1. `<dir>/<basename>.test.js` — поруч із source
|
|
53
|
+
2. `<dir>/<basename>.spec.js`
|
|
54
|
+
3. `test/<basename>.test.js` від кореня
|
|
55
|
+
4. `tests/<basename>.test.js` від кореня
|
|
56
|
+
|
|
57
|
+
Якщо жоден не знайдено — створи `<dir>/<basename>.test.js` з правильними imports (орієнтуйся на сусідні файли).
|
|
58
|
+
|
|
59
|
+
**3c. Напиши тести що вбивають кожен мутант:**
|
|
60
|
+
|
|
61
|
+
Керуйся типом мутації:
|
|
62
|
+
- `ConditionalExpression` (`→ false` / `→ true`): протестуй обидва branch явно — значення що робить умову `true` і значення що робить її `false`
|
|
63
|
+
- `BooleanLiteral` (`true → false`): перевір початковий стан — `initialValue === false`
|
|
64
|
+
- `LogicalOperator` (`&&` ↔ `||`): передай `null` та `undefined` **окремо**, перевір що результат різний для кожного
|
|
65
|
+
- `StringLiteral` / `EqualityOperator`: перевір точний рядок/значення, а не лише happy-path
|
|
66
|
+
|
|
67
|
+
Правила:
|
|
68
|
+
- НЕ видаляй і НЕ змінюй наявні тести
|
|
69
|
+
- Стиль: той самий `describe`/`it`/`expect`, мова коментарів — як у прикладі тесту
|
|
70
|
+
- Якщо `**Приклад наявного тесту:**` відсутній — орієнтуйся на інші test-файли у тій самій директорії
|
|
71
|
+
|
|
72
|
+
**3d. Після написання тестів:**
|
|
73
|
+
```bash
|
|
74
|
+
bun test <testFile>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Якщо FAIL — виправи саме ті тести що впали (до 2 спроб). Якщо не вдалося — логуй і переходь до наступного файлу.
|
|
78
|
+
|
|
79
|
+
### Крок 4: Перевір що весь suite проходить
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun test
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Якщо FAIL:
|
|
86
|
+
- Не відкочувати зміни
|
|
87
|
+
- Показати: яка помилка, які файли змінені, що вже покращено
|
|
88
|
+
- Очікувати рішення від user: [виправити вручну → продовжити] / [пропустити файл] / [зупинити]
|
|
89
|
+
|
|
90
|
+
### Крок 5: Запусти coverage і перевір конвергенцію
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bun coverage
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Якщо CRASH (SIGURG, memory pressure): нагадати user — Stryker incremental зберіг прогрес, перезапустити `bun coverage`.
|
|
97
|
+
|
|
98
|
+
Прочитай новий COVERAGE.md. Візьми `new_score` з рядка `| **Разом** |`.
|
|
99
|
+
|
|
100
|
+
**Рішення:**
|
|
101
|
+
- Якщо `new_score > baseline_score` → `baseline_score = new_score` → перейти до Кроку 2 (наступна ітерація)
|
|
102
|
+
- Якщо `new_score <= baseline_score` → конвергенція:
|
|
103
|
+
```
|
|
104
|
+
✓ Конвергенція: mutation score більше не покращується.
|
|
105
|
+
Baseline: <baseline_score> → Фінал: <new_score>
|
|
106
|
+
```
|
|
107
|
+
→ DONE
|
|
108
|
+
|
|
109
|
+
## Нотатки
|
|
110
|
+
|
|
111
|
+
- Stryker `incremental` (`incrementalFile`) зберігає прогрес між запусками — crash ≠ перезапуск з нуля
|
|
112
|
+
- Не комітити зміни автоматично — user вирішує коли комітити
|
|
113
|
+
- Пріоритет файлів: більше вижилих мутантів = важливіший (першим у Recommendations = найважливіший)
|
|
114
|
+
- Якщо `COVERAGE.md` відсутній — запусти `bun coverage` спочатку
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[js-lint]
|