@nitra/cursor 1.13.69 → 1.13.73

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
@@ -4,6 +4,30 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.73] - 2026-05-21
8
+
9
+ ### Fixed
10
+
11
+ - **Збір workspace-коренів** — `getMonorepoPackageRootDirs` / `getMonorepoProjectRootDirs` більше не трактують `package.json` у `node_modules/`, `.git/`, `.venv/`, `venv/` як воркспейси (glob ignore + `isIgnoredWorkspaceRoot`). Усуває хибні `check changelog` на транзитивних залежностях (наприклад `node-gyp/gyp`).
12
+
13
+ ## [1.13.72] - 2026-05-21
14
+
15
+ ### Changed
16
+
17
+ - **CLI скілів спрощено** — лише `npx @nitra/cursor skill list`, `skill <id> ["task"]` (промпт на stdout), `skill cursor <id> ["task"]`, `skill claude <id> ["task"]`. Прибрано `skill prompt`, bins `n-skills` / `n-claude`, підкоманду `claude` у `n-cursor`. Зачеплено: [skills-cli.mjs](scripts/skills-cli.mjs).
18
+
19
+ ## [1.13.71] - 2026-05-21
20
+
21
+ ### Added
22
+
23
+ - **Claude-first UX для скілів** — `npx @nitra/cursor claude taze "task"` і bin **`n-claude`** (замінено спрощеним `skill` у 1.13.72).
24
+
25
+ ## [1.13.70] - 2026-05-21
26
+
27
+ ### Added
28
+
29
+ - **CLI скілів без синку в проєкт** — `npx @nitra/cursor skill list|prompt|claude|cursor <id> "task"` і bin **`n-skills`** (`npx -p @nitra/cursor n-skills …`). Читає `skills/<id>/SKILL.md` з установленого пакета, збирає промпт із CWD (`package.json`, `tsconfig.json`, `.n-cursor.json`) і виводить на stdout або делегує в `claude -p` / `cursor-agent -p`. Id скілу — каталог у пакеті (`lint`, `fix`, …) або з префіксом `n-` (`n-lint` → `lint`). Зачеплено: [skills-cli.mjs](scripts/skills-cli.mjs), [n-skills.js](bin/n-skills.js), [n-cursor.js](bin/n-cursor.js).
30
+
7
31
  ## [1.13.69] - 2026-05-21
8
32
 
9
33
  ### Changed
package/bin/n-cursor.js CHANGED
@@ -19,6 +19,10 @@
19
19
  * `npx \@nitra/cursor lint-docker` — канонічний lint-docker (docker.mdc): `hadolint` по `Dockerfile`/`*.Dockerfile`
20
20
  * `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
21
21
  * `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
22
+ * `npx \@nitra/cursor skill list` — скіли пакета без синку в проєкт
23
+ * `npx \@nitra/cursor skill taze` — промпт на stdout
24
+ * `npx \@nitra/cursor skill cursor taze ["task"]` — Cursor CLI (`cursor-agent -p`)
25
+ * `npx \@nitra/cursor skill claude taze ["task"]` — Claude Code CLI (`claude -p`)
22
26
  *
23
27
  * Agent інтеграція: під час синку, окрім `.cursor/rules` і `.claude/commands` (з skills), CLI ще раз
24
28
  * синхронізує `.claude/settings.json` (hooks + permissions; merge — користувацькі поля зберігаються),
@@ -90,6 +94,7 @@ import { runRule } from '../scripts/utils/run-rule.mjs'
90
94
  import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
91
95
  import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
92
96
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
97
+ import { runSkillsCli } from '../scripts/skills-cli.mjs'
93
98
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
94
99
 
95
100
  const PACKAGE_NAME = '@nitra/cursor'
@@ -1304,6 +1309,11 @@ try {
1304
1309
 
1305
1310
  break
1306
1311
  }
1312
+ case 'skill': {
1313
+ process.exitCode = await runSkillsCli(args)
1314
+
1315
+ break
1316
+ }
1307
1317
  case undefined:
1308
1318
  case '': {
1309
1319
  await runSync()
@@ -1313,7 +1323,7 @@ try {
1313
1323
  default: {
1314
1324
  console.error(`❌ Невідома команда: ${command}`)
1315
1325
  console.error(
1316
- ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, stop-hook, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text`
1326
+ ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, stop-hook, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, skill`
1317
1327
  )
1318
1328
  process.exitCode = 1
1319
1329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.69",
3
+ "version": "1.13.73",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,219 @@
1
+ /**
2
+ * CLI запуску скілів пакета `@nitra/cursor` без синку правил у проєкт.
3
+ *
4
+ * Скіли читаються з `npm/skills/<id>/SKILL.md` установленого пакета (або кешу `npx`).
5
+ * Промпт збирає інструкцію скілу + контекст поточного CWD (`package.json`, `tsconfig.json`,
6
+ * `.n-cursor.json`) — далі stdout або делегування в `cursor-agent` / `claude`.
7
+ *
8
+ * Підтримувані формати:
9
+ * `npx @nitra/cursor skill list`
10
+ * `npx @nitra/cursor skill taze`
11
+ * `npx @nitra/cursor skill cursor taze`
12
+ * `npx @nitra/cursor skill cursor taze "онови залежності"`
13
+ * `npx @nitra/cursor skill claude taze` — те саме через Claude Code CLI
14
+ */
15
+
16
+ import { spawnSync } from 'node:child_process'
17
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { cwd } from 'node:process'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const RUNNERS = new Set(['cursor', 'claude'])
23
+
24
+ const USAGE_LINES = [
25
+ 'Usage:',
26
+ ' npx @nitra/cursor skill list',
27
+ ' npx @nitra/cursor skill <skill-id> ["task"]',
28
+ ' npx @nitra/cursor skill cursor <skill-id> ["task"]',
29
+ ' npx @nitra/cursor skill claude <skill-id> ["task"]',
30
+ '',
31
+ 'Skill id: каталог у пакеті (lint, taze, …) або з префіксом n- (n-lint → lint).'
32
+ ]
33
+
34
+ /**
35
+ * @param {string} name
36
+ * @returns {boolean}
37
+ */
38
+ function isBinaryInPath(name) {
39
+ const probe = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' })
40
+ return probe.status === 0
41
+ }
42
+
43
+ /**
44
+ * @param {string} name ім'я скілу з CLI або каталогу `.cursor/skills/n-*`
45
+ * @returns {string} id каталогу в `npm/skills/<id>/`
46
+ */
47
+ export function normalizeSkillId(name) {
48
+ if (!name || typeof name !== 'string') {
49
+ return ''
50
+ }
51
+ return name.startsWith('n-') ? name.slice(2) : name
52
+ }
53
+
54
+ /**
55
+ * @param {string} skillsRoot абсолютний шлях до `skills/` пакета
56
+ * @returns {string[]}
57
+ */
58
+ export function listSkillIds(skillsRoot) {
59
+ if (!existsSync(skillsRoot)) {
60
+ return []
61
+ }
62
+
63
+ return readdirSync(skillsRoot, { withFileTypes: true })
64
+ .filter(entry => entry.isDirectory())
65
+ .map(entry => entry.name)
66
+ .filter(name => existsSync(join(skillsRoot, name, 'SKILL.md')))
67
+ .sort((a, b) => a.localeCompare(b))
68
+ }
69
+
70
+ /**
71
+ * @param {string} skillsRoot
72
+ * @param {string} skillId нормалізований id (без префікса n-)
73
+ * @returns {string}
74
+ */
75
+ function getSkillMdPath(skillsRoot, skillId) {
76
+ return join(skillsRoot, skillId, 'SKILL.md')
77
+ }
78
+
79
+ /**
80
+ * @param {string} path
81
+ * @returns {string | null}
82
+ */
83
+ function readIfExists(path) {
84
+ return existsSync(path) ? readFileSync(path, 'utf8') : null
85
+ }
86
+
87
+ /**
88
+ * @param {string} skillsRoot
89
+ * @param {string} rawSkillName
90
+ * @param {string} task
91
+ * @param {string} [projectDir]
92
+ * @returns {string}
93
+ */
94
+ export function buildSkillPrompt(skillsRoot, rawSkillName, task, projectDir = cwd()) {
95
+ const skillId = normalizeSkillId(rawSkillName)
96
+ const skillPath = getSkillMdPath(skillsRoot, skillId)
97
+
98
+ if (!skillId || !existsSync(skillPath)) {
99
+ const available = listSkillIds(skillsRoot).join(', ')
100
+ throw new Error(`Unknown skill "${rawSkillName}". Available skills: ${available || '(none)'}`)
101
+ }
102
+
103
+ const skill = readFileSync(skillPath, 'utf8')
104
+ const packageJson = readIfExists(join(projectDir, 'package.json'))
105
+ const tsconfig = readIfExists(join(projectDir, 'tsconfig.json'))
106
+ const nCursorJson = readIfExists(join(projectDir, '.n-cursor.json'))
107
+
108
+ return [
109
+ '# Task',
110
+ task || 'Execute the skill instructions for this project.',
111
+ '',
112
+ '# Skill',
113
+ skill,
114
+ '',
115
+ '# Current project',
116
+ `Directory: ${projectDir}`,
117
+ '',
118
+ packageJson ? `## package.json\n\n\`\`\`json\n${packageJson}\n\`\`\`` : '',
119
+ tsconfig ? `## tsconfig.json\n\n\`\`\`json\n${tsconfig}\n\`\`\`` : '',
120
+ nCursorJson ? `## .n-cursor.json\n\n\`\`\`json\n${nCursorJson}\n\`\`\`` : ''
121
+ ]
122
+ .filter(Boolean)
123
+ .join('\n\n')
124
+ }
125
+
126
+ /**
127
+ * @param {'claude' | 'cursor'} kind
128
+ * @param {string} prompt
129
+ * @param {string} projectDir
130
+ * @returns {number}
131
+ */
132
+ function runLlmCli(kind, prompt, projectDir) {
133
+ if (kind === 'claude') {
134
+ if (!isBinaryInPath('claude')) {
135
+ throw new Error('`claude` not found in PATH. Install Claude Code CLI or use `skill cursor`.')
136
+ }
137
+
138
+ const result = spawnSync('claude', ['-p'], {
139
+ input: prompt,
140
+ cwd: projectDir,
141
+ stdio: ['pipe', 'inherit', 'inherit'],
142
+ encoding: 'utf8'
143
+ })
144
+ return result.status ?? 1
145
+ }
146
+
147
+ if (!isBinaryInPath('cursor-agent')) {
148
+ throw new Error('`cursor-agent` not found in PATH. Install Cursor CLI or use `skill claude`.')
149
+ }
150
+
151
+ const result = spawnSync('cursor-agent', ['-p'], {
152
+ input: prompt,
153
+ cwd: projectDir,
154
+ stdio: ['pipe', 'inherit', 'inherit'],
155
+ encoding: 'utf8'
156
+ })
157
+ return result.status ?? 1
158
+ }
159
+
160
+ /**
161
+ * Корінь пакета `@nitra/cursor` (каталог з `skills/`, `rules/`, …).
162
+ * @param {string} [fromModuleUrl] для тестів — `import.meta.url` викликача
163
+ * @returns {string}
164
+ */
165
+ export function resolveBundledPackageRoot(fromModuleUrl = import.meta.url) {
166
+ return join(dirname(fileURLToPath(fromModuleUrl)), '..')
167
+ }
168
+
169
+ /**
170
+ * @param {string[]} argv аргументи після `skill` у `n-cursor`
171
+ * @param {{ packageRoot?: string, projectDir?: string, log?: (line: string) => void, logError?: (line: string) => void }} [options]
172
+ * @returns {Promise<number>} exit code
173
+ */
174
+ export async function runSkillsCli(argv, options = {}) {
175
+ const log = options.log ?? (line => console.log(line))
176
+ const logError = options.logError ?? (line => console.error(line))
177
+ const packageRoot = options.packageRoot ?? resolveBundledPackageRoot()
178
+ const skillsRoot = join(packageRoot, 'skills')
179
+ const projectDir = options.projectDir ?? cwd()
180
+
181
+ const [first, second, ...rest] = argv
182
+ const skillIds = listSkillIds(skillsRoot)
183
+
184
+ try {
185
+ if (!first) {
186
+ logError(USAGE_LINES.join('\n'))
187
+ return 1
188
+ }
189
+
190
+ if (first === 'list') {
191
+ log('Available skills:')
192
+ for (const id of skillIds) {
193
+ log(`- ${id}`)
194
+ }
195
+ return 0
196
+ }
197
+
198
+ if (RUNNERS.has(first)) {
199
+ if (!second) {
200
+ throw new Error(`Skill name is required after "${first}"`)
201
+ }
202
+ const task = rest.join(' ')
203
+ const prompt = buildSkillPrompt(skillsRoot, second, task, projectDir)
204
+ return runLlmCli(/** @type {'claude' | 'cursor'} */ (first), prompt, projectDir)
205
+ }
206
+
207
+ if (skillIds.includes(normalizeSkillId(first))) {
208
+ const task = [second, ...rest].filter(Boolean).join(' ')
209
+ log(buildSkillPrompt(skillsRoot, first, task, projectDir))
210
+ return 0
211
+ }
212
+
213
+ logError(USAGE_LINES.join('\n'))
214
+ return 1
215
+ } catch (error) {
216
+ logError(error instanceof Error ? error.message : String(error))
217
+ return 1
218
+ }
219
+ }
@@ -8,7 +8,7 @@ import { dirname, join, relative } from 'node:path'
8
8
 
9
9
  import { parse as parseToml } from 'smol-toml'
10
10
 
11
- import { getMonorepoPackageRootDirs } from './workspaces.mjs'
11
+ import { getMonorepoPackageRootDirs, isIgnoredWorkspaceRoot } from './workspaces.mjs'
12
12
 
13
13
  /**
14
14
  * @typedef {'npm' | 'python'} PackageKind
@@ -132,12 +132,12 @@ export async function getMonorepoProjectRootDirs(repoRoot = '.') {
132
132
  const absDir = dirname(join(repoRoot, relPy))
133
133
  const relRoot = relative(repoRoot, absDir)
134
134
  const ws = relRoot === '' ? '.' : relRoot
135
- if (!existsSync(join(repoRoot, ws, 'package.json'))) {
135
+ if (!isIgnoredWorkspaceRoot(ws) && !existsSync(join(repoRoot, ws, 'package.json'))) {
136
136
  roots.add(ws)
137
137
  }
138
138
  }
139
139
 
140
- const list = [...roots]
140
+ const list = [...roots].filter(ws => !isIgnoredWorkspaceRoot(ws))
141
141
  list.sort((a, b) => {
142
142
  if (a === '.') return -1
143
143
  if (b === '.') return 1
@@ -9,6 +9,32 @@ import { glob, readFile } from 'node:fs/promises'
9
9
  import { dirname, join, relative } from 'node:path'
10
10
 
11
11
  const TRAILING_SLASH_RE = /\/$/
12
+ const LEADING_DOTSLASH_RE = /^\.\//
13
+
14
+ /** Glob-ігнор для workspace-патернів із `*` (узгоджено з `package-manifest.mjs`). */
15
+ export const WORKSPACE_GLOB_IGNORE = Object.freeze([
16
+ '**/node_modules/**',
17
+ '**/.git/**',
18
+ '**/.venv/**',
19
+ '**/venv/**'
20
+ ])
21
+
22
+ /**
23
+ * Чи слід виключити каталог зі списку workspace-коренів (не стосується `.`).
24
+ * @param {string} ws відносний шлях воркспейсу
25
+ * @returns {boolean} true — пропустити
26
+ */
27
+ export function isIgnoredWorkspaceRoot(ws) {
28
+ if (ws === '.') return false
29
+ const p = ws.replaceAll('\\', '/').replace(LEADING_DOTSLASH_RE, '')
30
+ const segments = p.split('/')
31
+ return (
32
+ segments.includes('node_modules') ||
33
+ segments.includes('.git') ||
34
+ segments.includes('.venv') ||
35
+ segments.includes('venv')
36
+ )
37
+ }
12
38
 
13
39
  /**
14
40
  * Нормалізує workspace-патерн до POSIX-формату і прибирає хвостові `/`.
@@ -33,16 +59,22 @@ function normalizeWorkspacePattern(pattern) {
33
59
  async function addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern) {
34
60
  if (workspacePattern.includes('*')) {
35
61
  const globPat = `${workspacePattern}/package.json`
36
- for await (const relPkgJsonPath of glob(globPat, { cwd: repoRoot })) {
62
+ for await (const relPkgJsonPath of glob(globPat, {
63
+ cwd: repoRoot,
64
+ ignore: [...WORKSPACE_GLOB_IGNORE]
65
+ })) {
37
66
  const absPkgJsonPath = join(repoRoot, relPkgJsonPath)
38
67
  const relRoot = relative(repoRoot, dirname(absPkgJsonPath))
39
- roots.add(relRoot === '' ? '.' : relRoot)
68
+ const ws = relRoot === '' ? '.' : relRoot
69
+ if (!isIgnoredWorkspaceRoot(ws)) {
70
+ roots.add(ws)
71
+ }
40
72
  }
41
73
  return
42
74
  }
43
75
 
44
76
  const pkgJsonPath = join(repoRoot, workspacePattern, 'package.json')
45
- if (existsSync(pkgJsonPath)) {
77
+ if (existsSync(pkgJsonPath) && !isIgnoredWorkspaceRoot(workspacePattern)) {
46
78
  roots.add(workspacePattern)
47
79
  }
48
80
  }
@@ -77,7 +109,7 @@ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
77
109
  const workspacePattern = normalizeWorkspacePattern(raw)
78
110
  await addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern)
79
111
  }
80
- const list = [...roots]
112
+ const list = [...roots].filter(ws => !isIgnoredWorkspaceRoot(ws))
81
113
  list.sort((a, b) => {
82
114
  if (a === '.') return -1
83
115
  if (b === '.') return 1