@nitra/cursor 1.13.2 → 1.13.11

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/bin/n-cursor.js +4 -2
  3. package/package.json +4 -2
  4. package/rules/ga/fix/workflows/check.mjs +6 -109
  5. package/rules/ga/ga.mdc +10 -15
  6. package/rules/ga/policy/package_json/package_json.rego +20 -0
  7. package/rules/ga/policy/package_json/target.json +8 -0
  8. package/rules/ga/policy/package_json/template/package.json.contains.json +1 -0
  9. package/rules/ga/policy/vscode_extensions/target.json +8 -0
  10. package/rules/ga/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
  11. package/rules/ga/policy/vscode_extensions/vscode_extensions.rego +18 -0
  12. package/rules/ga/policy/vscode_settings/target.json +8 -0
  13. package/rules/ga/policy/vscode_settings/template/settings.json.snippet.json +1 -0
  14. package/rules/ga/policy/vscode_settings/vscode_settings.rego +22 -0
  15. package/rules/ga/policy/zizmor_yml/target.json +8 -0
  16. package/rules/ga/policy/zizmor_yml/template/zizmor.yml.snippet.yml +5 -0
  17. package/rules/ga/policy/zizmor_yml/zizmor_yml.rego +27 -0
  18. package/rules/js-lint/fix/tooling/check.mjs +6 -83
  19. package/rules/js-lint/policy/jscpd/jscpd.rego +38 -0
  20. package/rules/js-lint/policy/jscpd/target.json +8 -0
  21. package/rules/js-lint/policy/vscode_extensions/target.json +8 -0
  22. package/rules/js-lint/policy/vscode_extensions/vscode_extensions.rego +25 -0
  23. package/rules/rego/lint/lint.mjs +5 -4
  24. package/rules/rego/policy/package_json/package_json.rego +8 -29
  25. package/rules/rego/policy/package_json/template/package.json.snippet.json +1 -0
  26. package/rules/rego/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
  27. package/rules/rego/policy/vscode_extensions/vscode_extensions.rego +7 -11
  28. package/rules/rego/policy/vscode_settings/template/settings.json.snippet.json +6 -0
  29. package/rules/rego/policy/vscode_settings/vscode_settings.rego +19 -27
  30. package/rules/rego/rego.mdc +10 -8
  31. package/rules/security/fix/gitleaks/check.mjs +8 -45
  32. package/rules/security/fix/gitleaks/template/.gitleaks.toml.snippet.toml +12 -0
  33. package/rules/security/policy/gitleaks/gitleaks.rego +17 -0
  34. package/rules/security/policy/gitleaks/target.json +8 -0
  35. package/rules/security/policy/package_json/package_json.rego +22 -59
  36. package/rules/security/policy/package_json/template/package.json.contains.json +1 -0
  37. package/rules/security/policy/package_json/template/package.json.deny.json +4 -0
  38. package/rules/security/policy/package_json/template/package.json.snippet.json +1 -0
  39. package/rules/security/security.mdc +7 -26
  40. package/rules/security/todo.MD +27 -0
  41. package/rules/vue/fix/packages/check.mjs +7 -64
  42. package/rules/vue/policy/package_json/package_json.rego +45 -2
  43. package/rules/vue/vue.mdc +15 -2
  44. package/scripts/ensure-nitra-cursor-dev-dependencies.mjs +41 -21
  45. package/scripts/utils/check-mdc-template-refs.mjs +47 -0
  46. package/scripts/utils/inline-template-links.mjs +60 -0
  47. package/scripts/utils/run-conftest-batch.mjs +60 -33
  48. package/scripts/utils/run-rule.mjs +16 -1
  49. package/scripts/utils/template.mjs +215 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Reads template/ for a concern directory and returns a merged structure indexed
3
+ * by target basename. For each <target>, returns whichever of snippet/deny/contains
4
+ * exist (parsed in native format by extension).
5
+ *
6
+ * @param {string} concernDir absolute path to fix/<concern>/ or policy/<concern>/
7
+ * @returns {Promise<Record<string, { snippet?: any, deny?: any, contains?: any }>>}
8
+ */
9
+ import { existsSync } from 'node:fs'
10
+ import { readdir, readFile, stat } from 'node:fs/promises'
11
+ import { basename as _basename, extname, join, relative } from 'node:path'
12
+
13
+ import { parse as parseToml } from 'smol-toml'
14
+
15
+ const SLOTS = ['snippet', 'deny', 'contains']
16
+
17
+ /** Parse file contents by extension; returns JS object for structured formats, string for text. */
18
+ async function parseByExt(path) {
19
+ const raw = await readFile(path, 'utf8')
20
+ const ext = extname(path).toLowerCase()
21
+ if (ext === '.json' || ext === '.jsonc') return JSON.parse(stripJsonComments(raw))
22
+ if (ext === '.toml') return parseToml(raw)
23
+ if (ext === '.yml' || ext === '.yaml') {
24
+ const { parse: parseYaml } = await import('yaml')
25
+ return parseYaml(raw)
26
+ }
27
+ return raw // text-only
28
+ }
29
+
30
+ function stripJsonComments(s) {
31
+ // Minimal: strip // line comments and /* */ block comments. JSON-with-comments format.
32
+ return s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
33
+ }
34
+
35
+ async function walk(dir, base = dir) {
36
+ const out = []
37
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
38
+ const full = join(dir, entry.name)
39
+ if (entry.isDirectory()) out.push(...(await walk(full, base)))
40
+ else out.push(relative(base, full))
41
+ }
42
+ return out
43
+ }
44
+
45
+ /**
46
+ * Parse "<target>.<slot>.<ext>" or "<target>" (text-only).
47
+ * Returns { target, slot } where slot is one of snippet|deny|contains|null (null = text-only target).
48
+ */
49
+ function classifyTemplateFile(relPath) {
50
+ // Try ".<slot>." suffix detection
51
+ for (const slot of SLOTS) {
52
+ const m = relPath.match(new RegExp(`^(?<target>.+)\\.${slot}\\.[^.]+$`))
53
+ if (m?.groups?.target) return { target: m.groups.target, slot }
54
+ }
55
+ // No slot suffix → text-only canon for the literal target name
56
+ return { target: relPath, slot: null }
57
+ }
58
+
59
+ function formatPath(parts) {
60
+ return parts
61
+ .map(p => (typeof p === 'number' ? `[${p}]` : /^[a-zA-Z_$][\w$]*$/.test(p) ? p : JSON.stringify(p)))
62
+ .reduce((acc, p) => (acc === '' ? p : p.startsWith('[') ? acc + p : acc + '.' + p), '')
63
+ }
64
+
65
+ function quote(v) {
66
+ return typeof v === 'string' ? JSON.stringify(v) : String(v)
67
+ }
68
+
69
+ /**
70
+ * Deep subset-of check. Every leaf in `snippet` must equal same path in `actual`.
71
+ * Arrays in snippet: every element must be present in actual array.
72
+ * Returns array of violation messages.
73
+ */
74
+ export function checkSnippet(actual, snippet, opts, path = []) {
75
+ if (snippet == null) return []
76
+ const { targetPath, source } = opts
77
+ const violations = []
78
+ if (Array.isArray(snippet)) {
79
+ if (!Array.isArray(actual)) {
80
+ violations.push(`${targetPath}: ${formatPath(path)} має бути масивом (${source})`)
81
+ return violations
82
+ }
83
+ for (const needle of snippet) {
84
+ const found = actual.some(a => JSON.stringify(a) === JSON.stringify(needle))
85
+ if (!found) {
86
+ violations.push(`${targetPath}: ${formatPath(path)} має містити ${quote(needle)} (${source})`)
87
+ }
88
+ }
89
+ return violations
90
+ }
91
+ if (snippet !== null && typeof snippet === 'object') {
92
+ if (actual == null || typeof actual !== 'object' || Array.isArray(actual)) {
93
+ violations.push(`${targetPath}: ${formatPath(path)} має бути об'єктом (${source})`)
94
+ return violations
95
+ }
96
+ for (const [k, v] of Object.entries(snippet)) {
97
+ violations.push(...checkSnippet(actual[k], v, opts, [...path, k]))
98
+ }
99
+ return violations
100
+ }
101
+ // Leaf (string/number/boolean)
102
+ if (actual !== snippet) {
103
+ violations.push(`${targetPath}: ${formatPath(path)} має бути ${quote(snippet)} (${source})`)
104
+ }
105
+ return violations
106
+ }
107
+
108
+ /**
109
+ * Walks deny tree; for any leaf path that exists in actual, returns violation
110
+ * with the deny's leaf string as reason.
111
+ */
112
+ export function checkDeny(actual, deny, opts, path = []) {
113
+ if (deny == null) return []
114
+ const { targetPath, source } = opts
115
+ if (deny !== null && typeof deny === 'object' && !Array.isArray(deny)) {
116
+ const out = []
117
+ for (const [k, v] of Object.entries(deny)) {
118
+ const childActual = actual && typeof actual === 'object' ? actual[k] : undefined
119
+ out.push(...checkDeny(childActual, v, opts, [...path, k]))
120
+ }
121
+ return out
122
+ }
123
+ // Leaf reached — if actual has this path at all (any value), it's a violation
124
+ if (actual !== undefined) {
125
+ const reason = typeof deny === 'string' ? deny : 'заборонено'
126
+ return [`${targetPath}: ${formatPath(path)} — ${reason} (${source})`]
127
+ }
128
+ return []
129
+ }
130
+
131
+ /**
132
+ * For each leaf path that has an array of strings in `contains`, every string
133
+ * must appear as substring in the same path of `actual` (string leaf).
134
+ */
135
+ export function checkContains(actual, contains, opts, path = []) {
136
+ if (contains == null) return []
137
+ const { targetPath, source } = opts
138
+ if (Array.isArray(contains)) {
139
+ const out = []
140
+ const haystack = typeof actual === 'string' ? actual : ''
141
+ for (const needle of contains) {
142
+ if (!haystack.includes(needle)) {
143
+ out.push(`${targetPath}: ${formatPath(path)} має містити ${quote(needle)} (${source})`)
144
+ }
145
+ }
146
+ return out
147
+ }
148
+ if (contains !== null && typeof contains === 'object') {
149
+ const out = []
150
+ for (const [k, v] of Object.entries(contains)) {
151
+ const childActual = actual && typeof actual === 'object' ? actual[k] : undefined
152
+ out.push(...checkContains(childActual, v, opts, [...path, k]))
153
+ }
154
+ return out
155
+ }
156
+ return []
157
+ }
158
+
159
+ /**
160
+ * For text-only targets (e.g. .stylelintignore): every non-empty, non-comment
161
+ * line in `template` must appear (trimmed) in `actual`.
162
+ */
163
+ export function checkTextSubset(actual, template, opts) {
164
+ if (template == null) return []
165
+ const { targetPath, source } = opts
166
+ const actualLines = new Set(String(actual ?? '').split(/\r?\n/).map(l => l.trim()))
167
+ const out = []
168
+ for (const raw of String(template).split(/\r?\n/)) {
169
+ const line = raw.trim()
170
+ if (line === '' || line.startsWith('#')) continue
171
+ if (!actualLines.has(line)) {
172
+ out.push(`${targetPath}: відсутній рядок ${quote(line)} (${source})`)
173
+ }
174
+ }
175
+ return out
176
+ }
177
+
178
+ export async function loadTemplate(concernDir) {
179
+ const tplDir = join(concernDir, 'template')
180
+ if (!existsSync(tplDir)) return {}
181
+ if (!(await stat(tplDir)).isDirectory()) return {}
182
+ const files = await walk(tplDir)
183
+ const result = {}
184
+ for (const rel of files) {
185
+ const { target, slot } = classifyTemplateFile(rel)
186
+ if (!result[target]) result[target] = {}
187
+ const value = await parseByExt(join(tplDir, rel))
188
+ if (slot === null) result[target].snippet = value // text-only treated as snippet
189
+ else result[target][slot] = value
190
+ }
191
+ return result
192
+ }
193
+
194
+ /**
195
+ * Resolves which template[<target>] to pass for a concern, based on its target.json.
196
+ * For `single` targets — basename. For `walkGlob` — basename of first non-negated entry.
197
+ * @param {string} concernAbsDir absolute path to fix/<concern>/ or policy/<concern>/
198
+ * @param {{ files?: { single?: string, walkGlob?: string|string[] } }} targetJson parsed target.json
199
+ * @returns {Promise<object|undefined>} template tree for the resolved target basename, or undefined
200
+ */
201
+ export async function resolveConcernTemplateData(concernAbsDir, targetJson) {
202
+ const tpl = await loadTemplate(concernAbsDir)
203
+ const single = targetJson?.files?.single
204
+ if (single) return tpl[_basename(single)]
205
+ const glob = targetJson?.files?.walkGlob
206
+ if (typeof glob === 'string') return tpl[_basename(glob.replace(/^!/, ''))]
207
+ if (Array.isArray(glob)) {
208
+ for (const g of glob) {
209
+ if (g.startsWith('!')) continue
210
+ const data = tpl[_basename(g)]
211
+ if (data) return data
212
+ }
213
+ }
214
+ return undefined
215
+ }