@nitra/cursor 1.8.139 → 1.8.140

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.139",
3
+ "version": "1.8.140",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -9,7 +9,8 @@
9
9
  * - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
10
10
  * містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
11
11
  * `mirror.gcr.io/library/python:*`
12
- * - frontend: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
12
+ * - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` (preferred) або `mirror.gcr.io/library/nginx:*`,
13
+ * `mirror.gcr.io/openresty/openresty:*`
13
14
  *
14
15
  * Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
15
16
  * то очікується компіляція в один бінарник через `bun build --compile` у build stage, а у
@@ -18,8 +19,10 @@
18
19
  * Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
19
20
  * дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
20
21
  *
21
- * Для `mirror.gcr.io/library/nginx` у будь-якому `FROM` очікується тег `alpine-slim` (docker.mdc:
22
- * мінімальні образи), не `latest` / `alpine` / інші.
22
+ * Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`, `mirror.gcr.io/library/nginx`) у
23
+ * будь-якому `FROM` очікується тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
24
+ * `alpine` / інші. `nginx-unprivileged` запускається від не-root користувача (uid=101) без явного
25
+ * `USER` у Dockerfile — перевірка non-root для нього пропускається.
23
26
  *
24
27
  * Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
25
28
  * тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
@@ -40,6 +43,7 @@ const BUN_WORD_RE = /\bbun\b/iu
40
43
  const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
41
44
 
42
45
  const NGINX_MIRROR_PREFIX = 'mirror.gcr.io/library/nginx'
46
+ const NGINX_UNPRIVILEGED_MIRROR_PREFIX = 'mirror.gcr.io/nginxinc/nginx-unprivileged'
43
47
 
44
48
  /**
45
49
  * @typedef {{
@@ -94,6 +98,7 @@ const RUNTIME_IMAGES = /** @type {const} */ ([
94
98
  'mirror.gcr.io/library/php',
95
99
  'mirror.gcr.io/library/python',
96
100
  'mirror.gcr.io/library/nginx',
101
+ 'mirror.gcr.io/nginxinc/nginx-unprivileged',
97
102
  'mirror.gcr.io/openresty/openresty'
98
103
  ])
99
104
 
@@ -103,7 +108,7 @@ const DEBIAN_VIA_MIRROR_RE = /^mirror\.gcr\.io\/library\/debian:(.+)$/i
103
108
  /**
104
109
  * Чи ref фінального `FROM` відповідає дозволеним у docker.mdc (multistage / runtime).
105
110
  * @param {string} lastLower ref без digest, lower case
106
- * @returns {boolean}
111
+ * @returns {boolean} true, якщо образ дозволений як фінальний runtime
107
112
  */
108
113
  function isAllowedFinalRuntimeImage(lastLower) {
109
114
  if (lastLower === 'scratch' || lastLower.startsWith('scratch:')) {
@@ -189,7 +194,9 @@ export function getBunCompileHint(fileContent) {
189
194
  const hasBunInstall = BUN_INSTALL_RE.test(fileContent)
190
195
  const isFinalAlpine = lastLower.startsWith('mirror.gcr.io/library/alpine:')
191
196
  const isFinalFrontend =
192
- lastLower.startsWith('mirror.gcr.io/library/nginx:') || lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
197
+ lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
198
+ lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`) ||
199
+ lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
193
200
 
194
201
  if (!hasBunInstall) return null
195
202
  if (!isFinalAlpine) return null
@@ -209,24 +216,26 @@ export function getBunCompileHint(fileContent) {
209
216
  }
210
217
 
211
218
  /**
212
- * Перевіряє, що для `mirror.gcr.io/library/nginx` у `FROM` вказано тег `alpine-slim` (docker.mdc).
219
+ * Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged` або
220
+ * `mirror.gcr.io/library/nginx`) у `FROM` вказано тег `alpine-slim` (docker.mdc).
213
221
  *
214
222
  * @param {string} fileContent вміст Dockerfile/Containerfile
215
223
  * @returns {string | null} повідомлення помилки або null
216
224
  */
217
225
  export function getNginxAlpineSlimTagHint(fileContent) {
226
+ const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX, NGINX_MIRROR_PREFIX]
218
227
  for (const { line, image } of parseFromStages(fileContent)) {
219
228
  const noDigest = (image.split('@')[0] || '').trim()
220
229
  const d = noDigest.toLowerCase()
221
- if (!d.startsWith(`${NGINX_MIRROR_PREFIX}:`) && d !== NGINX_MIRROR_PREFIX) {
222
- continue
223
- }
224
- if (d === NGINX_MIRROR_PREFIX) {
225
- return `рядок ${line}: \`FROM mirror.gcr.io/library/nginx\` має явний тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз без тега (типово latest)`
226
- }
227
- const tag = noDigest.slice(NGINX_MIRROR_PREFIX.length + 1)
228
- if (tag.toLowerCase() !== 'alpine-slim') {
229
- return `рядок ${line}: для nginx потрібен тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз: \`${tag}\``
230
+ const prefix = prefixes.find(p => d.startsWith(`${p}:`) || d === p)
231
+ if (prefix) {
232
+ if (d === prefix) {
233
+ return `рядок ${line}: \`FROM ${prefix}\` має явний тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз без тега (типово latest)`
234
+ }
235
+ const tag = noDigest.slice(prefix.length + 1)
236
+ if (tag.toLowerCase() !== 'alpine-slim') {
237
+ return `рядок ${line}: для nginx потрібен тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз: \`${tag}\``
238
+ }
230
239
  }
231
240
  }
232
241
  return null
@@ -247,6 +256,8 @@ export function getNonRootRuntimeHint(fileContent) {
247
256
  if (stages.length === 0) return null
248
257
 
249
258
  const last = stages.at(-1)
259
+ const lastImage = (last?.from.image || '').split('@')[0] || ''
260
+ const lastLower = lastImage.toLowerCase()
250
261
  const lastStageContent = last?.stageContent || ''
251
262
 
252
263
  /** @type {string | null} */
@@ -259,6 +270,8 @@ export function getNonRootRuntimeHint(fileContent) {
259
270
  }
260
271
 
261
272
  if (!lastUserToken) {
273
+ // nginx-unprivileged вже запускається від uid=101 (nginx) без явного USER у Dockerfile
274
+ if (lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`)) return null
262
275
  return 'у фінальному stage має бути `USER <non-root>` (наприклад `USER app`) — принцип non-root (docker.mdc: не превілейований образ)'
263
276
  }
264
277
 
@@ -110,14 +110,15 @@ export function normalizeHubRepoPath(imageToken) {
110
110
  }
111
111
 
112
112
  const HUB_REPOS_REQUIRING_MIRROR = /** @type {const} */ (
113
- new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node'])
113
+ new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node', 'nginxinc/nginx-unprivileged'])
114
114
  )
115
115
 
116
116
  const EXPECTED_MIRROR = /** @type {const} */ ({
117
117
  'oven/bun': 'mirror.gcr.io/oven/bun',
118
118
  'library/alpine': 'mirror.gcr.io/library/alpine',
119
119
  'library/nginx': 'mirror.gcr.io/library/nginx',
120
- 'library/node': 'mirror.gcr.io/library/node'
120
+ 'library/node': 'mirror.gcr.io/library/node',
121
+ 'nginxinc/nginx-unprivileged': 'mirror.gcr.io/nginxinc/nginx-unprivileged'
121
122
  })
122
123
 
123
124
  /**