@nitra/cursor 1.8.130 → 1.8.132

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.
@@ -370,9 +370,9 @@ function pushStringPaths(arr, acc) {
370
370
  const KUSTOMIZE_CONFIG_API_PREFIX = 'kustomize.config.k8s.io/'
371
371
 
372
372
  /**
373
- * Чи послідовність непорожніх рядків зростаюча за `localeCompare` (en).
374
- * @param {string[]} paths
375
- * @returns {boolean}
373
+ * Чи послідовність непорожніх рядків відсортована за `localeCompare` (en, ascending).
374
+ * @param {string[]} paths рядки для перевірки
375
+ * @returns {boolean} `true` якщо послідовність відсортована
376
376
  */
377
377
  function stringPathsAreSortedEn(paths) {
378
378
  for (let i = 1; i < paths.length; i++) {
@@ -402,8 +402,7 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
402
402
  }
403
403
  /** @type {string[]} */
404
404
  const paths = []
405
- for (let i = 0; i < res.length; i++) {
406
- const item = res[i]
405
+ for (const [i, item] of res.entries()) {
407
406
  if (typeof item !== 'string') {
408
407
  return `Kustomization.resources[${i}] — очікується рядок-шлях (k8s.mdc)`
409
408
  }
@@ -412,7 +411,7 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
412
411
  }
413
412
  if (paths.length < 2) return null
414
413
  if (!stringPathsAreSortedEn(paths)) {
415
- const want = [...paths].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
414
+ const want = paths.toSorted((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
416
415
  return `Kustomization.resources має бути за алфавітом (en). Зараз: ${paths.join(', ')}; очікувано: ${want.join(', ')} (k8s.mdc)`
417
416
  }
418
417
  return null
@@ -422,17 +421,18 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
422
421
  * Усі **`kustomization.yaml`**: **`resources`**, відсортовані за en.
423
422
  * @param {string} root корінь репо
424
423
  * @param {string[]} yamlFilesAbs yaml під k8s
425
- * @param {(msg: string) => void} fail
426
- * @returns {Promise<void>}
424
+ * @param {(msg: string) => void} fail функція для фіксації порушення
425
+ * @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
427
426
  */
428
427
  async function validateKustomizationResourcesSortedAlphabetically(root, yamlFilesAbs, fail) {
429
428
  for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
430
429
  const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
431
430
  const kust = await readFirstYamlObject(kustAbs)
432
- if (kust === null) continue
433
- const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
434
- if (v !== null) {
435
- fail(`${rel}: ${v}`)
431
+ if (kust !== null) {
432
+ const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
433
+ if (v !== null) {
434
+ fail(`${rel}: ${v}`)
435
+ }
436
436
  }
437
437
  }
438
438
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Перевіряє вимоги правила php.mdc для PHP-проєктів.
3
+ *
4
+ * Очікування:
5
+ * - у корені є `composer.json`;
6
+ * - у `package.json` є скрипт `lint-php` (рекомендовано делегувати в `run-php.mjs`);
7
+ * - у `.github/workflows/lint-php.yml` є крок `run: bun run lint-php` (для Bun-репозиторіїв).
8
+ */
9
+ import { existsSync } from 'node:fs'
10
+ import { readFile } from 'node:fs/promises'
11
+
12
+ import { createCheckReporter } from './utils/check-reporter.mjs'
13
+ import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
14
+
15
+ /**
16
+ * Перевіряє наявність `composer.json`.
17
+ * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
18
+ */
19
+ function checkComposer(reporter) {
20
+ const { pass, fail } = reporter
21
+ if (existsSync('composer.json')) {
22
+ pass('composer.json існує')
23
+ } else {
24
+ fail('composer.json не знайдено в корені — додай (php.mdc)')
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Перевіряє кореневий `package.json` на скрипт `lint-php`.
30
+ * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
31
+ */
32
+ async function checkPackageJson(reporter) {
33
+ const { pass, fail } = reporter
34
+ if (!existsSync('package.json')) {
35
+ fail('package.json не знайдено в корені — додай (php.mdc)')
36
+ return
37
+ }
38
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'))
39
+ const lintPhp = pkg.scripts?.['lint-php']
40
+ if (lintPhp) {
41
+ pass('package.json містить скрипт lint-php')
42
+ } else {
43
+ fail('package.json: додай скрипт "lint-php" (php.mdc)')
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Перевіряє workflow `lint-php.yml`.
49
+ * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
50
+ */
51
+ async function checkWorkflow(reporter) {
52
+ const { pass, fail } = reporter
53
+ const wfPath = '.github/workflows/lint-php.yml'
54
+ if (!existsSync(wfPath)) {
55
+ fail(`${wfPath} не існує — створи згідно php.mdc`)
56
+ return
57
+ }
58
+ const content = await readFile(wfPath, 'utf8')
59
+ pass('lint-php.yml існує')
60
+ const root = parseWorkflowYaml(content)
61
+ const ok = root ? anyRunStepIncludes(root, 'bun run lint-php') : content.includes('bun run lint-php')
62
+ if (ok) {
63
+ pass('lint-php.yml викликає bun run lint-php')
64
+ } else {
65
+ fail('lint-php.yml має містити крок run: bun run lint-php (php.mdc)')
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Перевіряє відповідність проєкту правилам php.mdc.
71
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
72
+ */
73
+ export async function check() {
74
+ const reporter = createCheckReporter()
75
+ checkComposer(reporter)
76
+ await checkPackageJson(reporter)
77
+ await checkWorkflow(reporter)
78
+ return reporter.getExitCode()
79
+ }
80
+
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Запуск `lint-php` за правилом php.mdc: `composer audit` і, якщо встановлені пакети, запуск
3
+ * PHPStan, Psalm, PHP-CS-Fixer (dry-run) та PHPCS зі стандартом Security.
4
+ *
5
+ * Скрипт не вимагає, щоб усі інструменти були встановлені: якщо відповідного файла
6
+ * `vendor/bin/<tool>` немає, крок пропускається з повідомленням. Але якщо в корені є
7
+ * `composer.json`, то `composer` має бути доступний у PATH (інакше це помилка).
8
+ *
9
+ * Якщо `composer.json` у корені відсутній — вихід 0 без запуску інструментів.
10
+ */
11
+ import { spawnSync } from 'node:child_process'
12
+ import { existsSync, statSync } from 'node:fs'
13
+ import { join, resolve } from 'node:path'
14
+
15
+ import { isRunAsCli } from './cli-entry.mjs'
16
+ import { createCheckReporter } from './utils/check-reporter.mjs'
17
+ import { resolveCmd } from './utils/resolve-cmd.mjs'
18
+
19
+ const PHPCS_CODE_DIR_CANDIDATES = ['app', 'src', 'lib', 'public', 'www']
20
+
21
+ /**
22
+ * Каталоги коду для PHPCS (якщо типових директорій немає — перевіряємо `.`).
23
+ * @param {string} root корінь репозиторію
24
+ * @returns {string[]} перелік шляхів (відносно root), які варто передати у `phpcs`
25
+ */
26
+ export function getPhpcsCodePaths(root) {
27
+ const out = []
28
+ for (const d of PHPCS_CODE_DIR_CANDIDATES) {
29
+ const p = join(root, d)
30
+ if (existsSync(p) && statSync(p).isDirectory()) out.push(d)
31
+ }
32
+ return out.length > 0 ? out : ['.']
33
+ }
34
+
35
+ /**
36
+ * @param {string} root корінь репозиторію
37
+ * @param {string} name імʼя файла у vendor/bin
38
+ * @returns {string | null} абсолютний шлях або null, якщо файла немає
39
+ */
40
+ function vendorBin(root, name) {
41
+ const p = resolve(root, 'vendor', 'bin', name)
42
+ return existsSync(p) ? p : null
43
+ }
44
+
45
+ /**
46
+ * @param {string} label назва кроку для повідомлень
47
+ * @param {string} abs абсолютний шлях до CLI
48
+ * @param {string[]} args аргументи
49
+ * @param {(msg: string) => void} pass callback pass
50
+ * @param {(msg: string) => void} fail callback fail
51
+ * @returns {boolean} true якщо OK
52
+ */
53
+ function runTool(label, abs, args, pass, fail) {
54
+ const r = spawnSync(abs, args, { stdio: 'inherit', shell: false })
55
+ if (r.status === 0) {
56
+ pass(`lint-php: ${label} — OK`)
57
+ return true
58
+ }
59
+ const code = typeof r.status === 'number' ? r.status : 1
60
+ fail(`lint-php: ${label} — помилка (код ${code}, php.mdc)`)
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * Запускає `lint-php`.
66
+ * @returns {number} 0 — OK, 1 — є помилки
67
+ */
68
+ export function run() {
69
+ const reporter = createCheckReporter()
70
+ const { pass, fail } = reporter
71
+
72
+ const root = process.cwd()
73
+ if (!existsSync(join(root, 'composer.json'))) {
74
+ pass('lint-php: немає composer.json у корені — кроки PHP пропущено')
75
+ return reporter.getExitCode()
76
+ }
77
+
78
+ const composer = resolveCmd('composer')
79
+ if (!composer) {
80
+ fail('lint-php: `composer` не знайдено в PATH (потрібен при наявному composer.json, php.mdc)')
81
+ return reporter.getExitCode()
82
+ }
83
+
84
+ if (!runTool('composer audit', composer, ['audit', '--no-interaction'], pass, fail)) return reporter.getExitCode()
85
+
86
+ /**
87
+ * Запускає інструмент з `vendor/bin`, якщо він встановлений.
88
+ * @param {string} binName імʼя файла у vendor/bin
89
+ * @param {string} label назва кроку
90
+ * @param {string[]} args аргументи CLI
91
+ * @returns {boolean} true, якщо крок успішний або пропущений
92
+ */
93
+ function runOptionalVendorTool(binName, label, args) {
94
+ const abs = vendorBin(root, binName)
95
+ if (!abs) {
96
+ pass(`lint-php: vendor/bin/${binName} — відсутній, крок пропущено`)
97
+ return true
98
+ }
99
+ return runTool(label, abs, args, pass, fail)
100
+ }
101
+
102
+ if (!runOptionalVendorTool('php-cs-fixer', 'PHP-CS-Fixer (dry-run)', ['fix', '--dry-run', '--diff'])) {
103
+ return reporter.getExitCode()
104
+ }
105
+
106
+ const phpcsPaths = getPhpcsCodePaths(root)
107
+ if (
108
+ !runOptionalVendorTool('phpcs', 'phpcs (Security)', [
109
+ '--standard=Security',
110
+ '--ignore=*/vendor/*,*/node_modules/*,*/.git/*',
111
+ ...phpcsPaths
112
+ ])
113
+ ) {
114
+ return reporter.getExitCode()
115
+ }
116
+
117
+ if (!runOptionalVendorTool('phpstan', 'PHPStan', ['analyse', '--no-progress'])) return reporter.getExitCode()
118
+ if (!runOptionalVendorTool('psalm', 'Psalm', ['--no-cache'])) return reporter.getExitCode()
119
+
120
+ return reporter.getExitCode()
121
+ }
122
+
123
+ if (isRunAsCli()) {
124
+ process.exitCode = run()
125
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Знаходить небезпечні патерни використання `mssql`, які створюють підключення/пул
3
+ * всередині функцій (наприклад handler на кожен запит), замість того щоб мати один
4
+ * singleton `sql.ConnectionPool` на рівні модуля та повторно використовувати його.
5
+ *
6
+ * Також знаходить небезпечний виклик `query(\`...\`)` — це НЕ tagged template, а звичайний
7
+ * виклик з інтерполяцією рядка, який може призвести до SQL injection. Натомість має
8
+ * використовуватись tagged template `query\`...\`` (див. js-mssql.mdc).
9
+ *
10
+ * Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
11
+ * Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
12
+ * результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
13
+ */
14
+ import { parseSync } from 'oxc-parser'
15
+
16
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
17
+
18
+ /**
19
+ * Мова для Oxc за шляхом файлу (розширення).
20
+ * @param {string} filePath віртуальний або реальний шлях до файлу
21
+ * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
22
+ */
23
+ function langFromPath(filePath) {
24
+ const lower = filePath.toLowerCase()
25
+ if (lower.endsWith('.tsx')) return 'tsx'
26
+ if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
27
+ if (lower.endsWith('.jsx')) return 'jsx'
28
+ return 'js'
29
+ }
30
+
31
+ /**
32
+ * Номер рядка (1-based) за зміщенням у буфері.
33
+ * @param {string} content повний текст файлу
34
+ * @param {number} offset байтове зміщення початку фрагмента
35
+ * @returns {number} номер рядка від 1
36
+ */
37
+ function offsetToLine(content, offset) {
38
+ let line = 1
39
+ const n = Math.min(offset, content.length)
40
+ for (let i = 0; i < n; i++) {
41
+ if (content.codePointAt(i) === 10) line++
42
+ }
43
+ return line
44
+ }
45
+
46
+ /**
47
+ * Стискає пробіли для повідомлення про порушення.
48
+ * @param {string} s фрагмент коду
49
+ * @returns {string} скорочений однорядковий рядок
50
+ */
51
+ function normalizeSnippet(s) {
52
+ return s.replaceAll(/\s+/g, ' ').trim().slice(0, 180)
53
+ }
54
+
55
+ /**
56
+ * Чи є вузол функцією.
57
+ * @param {unknown} node AST node
58
+ * @returns {boolean} true, якщо це будь-який вузол-функція
59
+ */
60
+ function isFunctionNode(node) {
61
+ return (
62
+ !!node &&
63
+ typeof node === 'object' &&
64
+ typeof node.type === 'string' &&
65
+ (node.type === 'FunctionDeclaration' ||
66
+ node.type === 'FunctionExpression' ||
67
+ node.type === 'ArrowFunctionExpression')
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
73
+ * @param {unknown} node поточний вузол
74
+ * @param {unknown[]} ancestors масив предків від кореня до parent
75
+ * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
76
+ * @returns {void}
77
+ */
78
+ function walkAstWithAncestors(node, ancestors, visit) {
79
+ if (!node || typeof node !== 'object') return
80
+ if (Array.isArray(node)) {
81
+ for (const item of node) walkAstWithAncestors(item, ancestors, visit)
82
+ return
83
+ }
84
+
85
+ const rec = /** @type {Record<string, unknown>} */ (node)
86
+ if (typeof rec.type === 'string') {
87
+ visit(rec, ancestors)
88
+ ancestors = [...ancestors, rec]
89
+ }
90
+
91
+ for (const key of Object.keys(node)) {
92
+ if (key === 'parent') {
93
+ continue
94
+ }
95
+ const v = rec[key]
96
+ if (v && typeof v === 'object') {
97
+ walkAstWithAncestors(v, ancestors, visit)
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
104
+ * @param {unknown} node AST node
105
+ * @returns {boolean} true, якщо це створення ConnectionPool
106
+ */
107
+ function isNewConnectionPool(node) {
108
+ if (!node || node.type !== 'NewExpression') return false
109
+ const callee = node.callee
110
+ if (!callee || callee.type !== 'MemberExpression') return false
111
+ if (callee.computed) return false
112
+ const obj = callee.object
113
+ const prop = callee.property
114
+ if (!obj || obj.type !== 'Identifier') return false
115
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'ConnectionPool') return false
116
+ return obj.name === 'sql' || obj.name === 'mssql'
117
+ }
118
+
119
+ /**
120
+ * Чи це виклик `.query(...)` з TemplateLiteral як першим аргументом (`query(\`...\`)`).
121
+ * @param {unknown} node AST node
122
+ * @returns {boolean} true, якщо це небезпечний патерн `query(\`...\`)`
123
+ */
124
+ function isUnsafeQueryCallWithTemplateLiteral(node) {
125
+ if (!node || node.type !== 'CallExpression') return false
126
+ const callee = node.callee
127
+ if (!callee || callee.type !== 'MemberExpression') return false
128
+ if (callee.computed) return false
129
+ const prop = callee.property
130
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'query') return false
131
+ const args = node.arguments
132
+ if (!Array.isArray(args) || args.length === 0) return false
133
+ const first = args[0]
134
+ return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
135
+ }
136
+
137
+ /**
138
+ * Знаходить створення `ConnectionPool` всередині функцій.
139
+ * @param {string} content вихідний код
140
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
141
+ * @returns {{ line: number, snippet: string }[]} список порушень
142
+ */
143
+ export function findMssqlPerRequestConnectionInText(content, virtualPath = 'scan.ts') {
144
+ const lang = langFromPath(virtualPath || 'scan.ts')
145
+ let result
146
+ try {
147
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
148
+ } catch {
149
+ return []
150
+ }
151
+ if (result.errors?.length) return []
152
+
153
+ /** @type {{ line: number, snippet: string }[]} */
154
+ const out = []
155
+
156
+ walkAstWithAncestors(result.program, [], (node, ancestors) => {
157
+ const insideFunction = ancestors.some(n => isFunctionNode(n))
158
+ if (!insideFunction) return
159
+
160
+ if (isNewConnectionPool(node)) {
161
+ out.push({
162
+ line: offsetToLine(content, node.start),
163
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
164
+ })
165
+ }
166
+ })
167
+
168
+ return out
169
+ }
170
+
171
+ /**
172
+ * Знаходить небезпечні виклики `query(\`...\`)` (CallExpression з TemplateLiteral-аргументом).
173
+ * @param {string} content вихідний код
174
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
175
+ * @returns {{ line: number, snippet: string }[]} список порушень
176
+ */
177
+ export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 'scan.ts') {
178
+ const lang = langFromPath(virtualPath || 'scan.ts')
179
+ let result
180
+ try {
181
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
182
+ } catch {
183
+ return []
184
+ }
185
+ if (result.errors?.length) return []
186
+
187
+ /** @type {{ line: number, snippet: string }[]} */
188
+ const out = []
189
+ walkAstWithAncestors(result.program, [], node => {
190
+ if (isUnsafeQueryCallWithTemplateLiteral(node)) {
191
+ out.push({
192
+ line: offsetToLine(content, node.start),
193
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
194
+ })
195
+ }
196
+ })
197
+ return out
198
+ }
199
+
200
+ /**
201
+ * Чи сканувати цей файл за розширенням (JS/TS-сім'я).
202
+ * @param {string} relativePathPosix відносний шлях (posix)
203
+ * @returns {boolean} `true`, якщо розширення підходить для AST-скану
204
+ */
205
+ export function isMssqlScanSourceFile(relativePathPosix) {
206
+ return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
207
+ }
208
+