@nitra/cursor 1.13.69 → 1.13.72

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,24 @@
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.72] - 2026-05-21
8
+
9
+ ### Changed
10
+
11
+ - **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).
12
+
13
+ ## [1.13.71] - 2026-05-21
14
+
15
+ ### Added
16
+
17
+ - **Claude-first UX для скілів** — `npx @nitra/cursor claude taze "task"` і bin **`n-claude`** (замінено спрощеним `skill` у 1.13.72).
18
+
19
+ ## [1.13.70] - 2026-05-21
20
+
21
+ ### Added
22
+
23
+ - **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).
24
+
7
25
  ## [1.13.69] - 2026-05-21
8
26
 
9
27
  ### 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.72",
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
+ }