@vv0rkz/js-template 1.9.0 → 1.10.0

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/README.md CHANGED
@@ -58,6 +58,12 @@ export default {
58
58
  formats: ['gif', 'png'], // допустимые форматы
59
59
  style: 'click', // 'click' | 'side-by-side'
60
60
  },
61
+ // Pre-run хук: перед каждой jst-командой сравнивает теги ↔ Releases ↔
62
+ // демо ↔ README и выводит предупреждения (не блокирует команду).
63
+ audit: {
64
+ enable: true,
65
+ warnOnly: true,
66
+ },
61
67
  },
62
68
 
63
69
  // Автообновление зависимостей
@@ -115,6 +121,10 @@ npx jst <команда> # напрямую
115
121
  | `setup-deps` | Настроить dependabot/renovate (+ auto-merge workflow) |
116
122
  | `setup-ci` | Сгенерировать `.github/workflows/ci.yml` |
117
123
  | `setup-branch-protection` | Настроить защиту главной ветки через `gh api` |
124
+ | `audit` | Проверить теги/Releases/демо/README на пробелы |
125
+ | `fix-releases` | Создать GitHub Release для тегов без Release |
126
+
127
+ > Audit запускается автоматически перед каждой `jst`-командой (кроме `init`, `upgrade`, `audit`, `fix-releases`). Отключить разово — флагом `--no-audit`: `jst --no-audit create-task "..."`. Полностью — в `jst.config.js`: `release.audit.enable = false`.
118
128
 
119
129
  ### Разработка
120
130
 
package/bin/cli.js CHANGED
@@ -7,6 +7,13 @@ import { fileURLToPath } from 'url'
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
8
8
  const toolsDir = join(__dirname, '../tools-gh')
9
9
 
10
+ // Strip the global `--no-audit` flag from argv before anyone else looks at it,
11
+ // so downstream command handlers (some of which read process.argv directly)
12
+ // don't see it as a positional argument.
13
+ const noAuditIndex = process.argv.indexOf('--no-audit')
14
+ const noAudit = noAuditIndex !== -1
15
+ if (noAudit) process.argv.splice(noAuditIndex, 1)
16
+
10
17
  const args = process.argv.slice(2)
11
18
  const command = args[0]
12
19
  const commandArgs = args.slice(1)
@@ -15,6 +22,16 @@ const isWin = platform() === 'win32'
15
22
  const npxCmd = isWin ? 'npx.cmd' : 'npx'
16
23
  const ghCmd = isWin ? 'gh.exe' : 'gh'
17
24
 
25
+ // Commands that should NOT trigger the release audit pre-run hook.
26
+ // `init` runs before tags exist; `audit` / `fix-releases` ARE the audit
27
+ // itself; `upgrade` runs right after `npm update` and may pre-date tags.
28
+ const SKIP_AUDIT = new Set(['audit', 'fix-releases', 'init', 'upgrade', undefined])
29
+
30
+ function maybeRunAudit() {
31
+ if (noAudit || SKIP_AUDIT.has(command)) return
32
+ spawnSync('node', [join(toolsDir, 'release-audit.js'), '--quiet-if-clean'], { stdio: 'inherit' })
33
+ }
34
+
18
35
  const commands = {
19
36
  init: () => {
20
37
  const initScript = join(__dirname, 'init.js')
@@ -109,6 +126,14 @@ const commands = {
109
126
  spawnSync('node', [join(toolsDir, 'setup-branch-protection.js')], { stdio: 'inherit' })
110
127
  },
111
128
 
129
+ audit: () => {
130
+ spawnSync('node', [join(toolsDir, 'release-audit.js')], { stdio: 'inherit' })
131
+ },
132
+
133
+ 'fix-releases': () => {
134
+ spawnSync('node', [join(toolsDir, 'fix-releases.js')], { stdio: 'inherit' })
135
+ },
136
+
112
137
  upgrade: () => {
113
138
  spawnSync('node', [join(toolsDir, 'upgrade.js')], { stdio: 'inherit' })
114
139
  },
@@ -172,6 +197,7 @@ const commands = {
172
197
  }
173
198
 
174
199
  if (commands[command]) {
200
+ maybeRunAudit()
175
201
  commands[command]()
176
202
  } else {
177
203
  console.log(`
@@ -188,6 +214,8 @@ if (commands[command]) {
188
214
  setup-deps Настроить dependabot/renovate
189
215
  setup-ci Сгенерировать .github/workflows/ci.yml
190
216
  setup-branch-protection Настроить защиту главной ветки (gh api)
217
+ audit Проверить теги/Releases/демо/README на пробелы
218
+ fix-releases Создать GitHub Release для тегов без Release
191
219
 
192
220
  🔧 РАЗРАБОТКА:
193
221
  update-readme Обновить README с версией
@@ -218,6 +246,9 @@ if (commands[command]) {
218
246
  ⚙️ КОНФИГУРАЦИЯ:
219
247
  jst.config.js Настройки веток, labels, коммитов, релизов
220
248
 
249
+ 🚩 ГЛОБАЛЬНЫЕ ФЛАГИ:
250
+ --no-audit Отключить release audit для этого запуска
251
+
221
252
  📝 ФОРМАТ КОММИТОВ:
222
253
  feat: #9 описание Фича (ссылка на issue)
223
254
  feat(scope): #9 описание Фича со scope
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vv0rkz/js-template",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Reusable setup for JS projects with husky, changelog, gh tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -103,6 +103,23 @@ export default {
103
103
  */
104
104
  style: 'click',
105
105
  },
106
+
107
+ /**
108
+ * Release audit — пассивная проверка перед каждой jst-командой.
109
+ * Сравнивает git tags ↔ GitHub Releases ↔ демо-файлы ↔ README-секции
110
+ * и печатает предупреждения, не блокируя команду.
111
+ *
112
+ * enable — включить хук (true | false)
113
+ * warnOnly — только предупреждать (true) или падать с кодом 1 (false)
114
+ *
115
+ * Отключить разово: `jst --no-audit <command>`
116
+ * Запустить явно: `jst audit`
117
+ * Починить пропуски: `jst fix-releases`
118
+ */
119
+ audit: {
120
+ enable: true,
121
+ warnOnly: true,
122
+ },
106
123
  },
107
124
 
108
125
  /**
@@ -25,6 +25,10 @@ const DEFAULTS = {
25
25
  formats: ['gif', 'png'],
26
26
  style: 'click',
27
27
  },
28
+ audit: {
29
+ enable: true,
30
+ warnOnly: true,
31
+ },
28
32
  },
29
33
  changelog: {
30
34
  types: {
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { execSync, spawnSync } from 'child_process'
3
+ import { platform } from 'os'
4
+
5
+ const isWin = platform() === 'win32'
6
+ const ghCmd = isWin ? 'gh.exe' : 'gh'
7
+
8
+ function safeExec(cmd) {
9
+ try {
10
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
11
+ } catch {
12
+ return ''
13
+ }
14
+ }
15
+
16
+ try {
17
+ execSync(`${ghCmd} auth status`, { stdio: 'ignore' })
18
+ } catch {
19
+ console.error('❌ GitHub CLI не установлен или не авторизован')
20
+ console.log('💡 Установи: https://cli.github.com/ и выполни: gh auth login')
21
+ process.exit(1)
22
+ }
23
+
24
+ const tagsRaw = safeExec('git tag --sort=creatordate')
25
+ const tags = tagsRaw
26
+ .split('\n')
27
+ .map((t) => t.trim())
28
+ .filter((t) => /^v\d+\.\d+\.\d+$/.test(t))
29
+
30
+ if (tags.length === 0) {
31
+ console.log('ℹ️ Тегов не найдено — нечего восстанавливать')
32
+ process.exit(0)
33
+ }
34
+
35
+ const releasesJson = safeExec(`${ghCmd} release list --json tagName --limit 200`)
36
+ let releaseTags = new Set()
37
+ try {
38
+ releaseTags = new Set(JSON.parse(releasesJson || '[]').map((r) => r.tagName))
39
+ } catch {
40
+ console.error('❌ Не удалось распарсить список GitHub Releases')
41
+ process.exit(1)
42
+ }
43
+
44
+ const missing = tags.filter((t) => !releaseTags.has(t))
45
+
46
+ if (missing.length === 0) {
47
+ console.log('✅ Все теги уже имеют GitHub Release')
48
+ process.exit(0)
49
+ }
50
+
51
+ console.log(`🔧 Создаю GitHub Release для ${missing.length} тег(ов):`)
52
+ missing.forEach((t) => console.log(` • ${t}`))
53
+ console.log()
54
+
55
+ let created = 0
56
+ let failed = 0
57
+
58
+ for (const tag of missing) {
59
+ process.stdout.write(` ${tag} ... `)
60
+ const result = spawnSync(ghCmd, ['release', 'create', tag, '--generate-notes', '--title', tag], {
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ encoding: 'utf8',
63
+ })
64
+
65
+ if (result.status === 0) {
66
+ console.log('✅')
67
+ created++
68
+ } else {
69
+ console.log('⚠️')
70
+ const errMsg = (result.stderr || '').trim().split('\n').slice(0, 2).join(' | ')
71
+ if (errMsg) console.log(` ${errMsg}`)
72
+ failed++
73
+ }
74
+ }
75
+
76
+ console.log(`\n📊 Создано: ${created}, ошибок: ${failed}`)
77
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process'
3
+ import { existsSync, readFileSync } from 'fs'
4
+ import { platform } from 'os'
5
+ import { loadConfig } from './config.js'
6
+
7
+ const isWin = platform() === 'win32'
8
+ const ghCmd = isWin ? 'gh.exe' : 'gh'
9
+
10
+ const config = await loadConfig()
11
+ const audit = (config.release && config.release.audit) || {}
12
+
13
+ if (audit.enable === false) process.exit(0)
14
+
15
+ // When invoked as a pre-run hook we stay silent on a clean repo. Stand-alone
16
+ // invocations (`jst audit`) print a positive confirmation.
17
+ const quietIfClean = process.argv.includes('--quiet-if-clean')
18
+ const warnOnly = audit.warnOnly !== false
19
+
20
+ function safeExec(cmd) {
21
+ try {
22
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
23
+ } catch {
24
+ return ''
25
+ }
26
+ }
27
+
28
+ const tagsRaw = safeExec('git tag --sort=-creatordate')
29
+ const tags = tagsRaw
30
+ .split('\n')
31
+ .map((t) => t.trim())
32
+ .filter((t) => /^v\d+\.\d+\.\d+$/.test(t))
33
+
34
+ if (tags.length === 0) process.exit(0)
35
+
36
+ let releaseTags = new Set()
37
+ const releasesJson = safeExec(`${ghCmd} release list --json tagName --limit 100`)
38
+ if (releasesJson) {
39
+ try {
40
+ releaseTags = new Set(JSON.parse(releasesJson).map((r) => r.tagName))
41
+ } catch {
42
+ // gh returned non-JSON — treat as "no info", skip the release check
43
+ releaseTags = null
44
+ }
45
+ }
46
+
47
+ const demoCfg = (config.release && config.release.demo) || {}
48
+ const demoEnabled = demoCfg.enable !== false
49
+ const demoDir = demoCfg.dir || 'docs'
50
+ const demoFormats = Array.isArray(demoCfg.formats) && demoCfg.formats.length ? demoCfg.formats : ['gif', 'png']
51
+
52
+ let readmeContent = ''
53
+ let hasAutoSection = false
54
+ if (existsSync('README.md')) {
55
+ readmeContent = readFileSync('README.md', 'utf8')
56
+ hasAutoSection = readmeContent.includes('<!-- AUTOGENERATED_SECTION START -->')
57
+ }
58
+
59
+ const noRelease = []
60
+ const noDemo = []
61
+ const noReadme = []
62
+
63
+ for (const tag of tags) {
64
+ if (releaseTags && !releaseTags.has(tag)) noRelease.push(tag)
65
+
66
+ if (demoEnabled) {
67
+ const hasDemo = demoFormats.some((fmt) => existsSync(`${demoDir}/${tag}.${fmt}`))
68
+ if (!hasDemo) noDemo.push(tag)
69
+ }
70
+
71
+ if (hasAutoSection && !readmeContent.includes(`### 🟢 ${tag}`)) {
72
+ noReadme.push(tag)
73
+ }
74
+ }
75
+
76
+ const total = noRelease.length + noDemo.length + noReadme.length
77
+
78
+ if (total === 0) {
79
+ if (!quietIfClean) console.log('✅ Release audit: всё в порядке')
80
+ process.exit(0)
81
+ }
82
+
83
+ // Compact list helper — first N tags then "+M more"
84
+ function fmtTags(list, limit = 5) {
85
+ if (list.length <= limit) return list.join(', ')
86
+ return list.slice(0, limit).join(', ') + ` +${list.length - limit} more`
87
+ }
88
+
89
+ if (noRelease.length) {
90
+ console.warn(`⚠️ Release audit: ${noRelease.length} тег(ов) без GitHub Release: ${fmtTags(noRelease)}`)
91
+ console.warn(` → Исправить: jst fix-releases`)
92
+ }
93
+ if (noDemo.length) {
94
+ console.warn(`⚠️ Release audit: ${noDemo.length} тег(ов) без демо: ${fmtTags(noDemo)}`)
95
+ console.warn(` → Создай файлы в ${demoDir}/<tag>.${demoFormats[0]}`)
96
+ }
97
+ if (noReadme.length) {
98
+ console.warn(`⚠️ Release audit: README не содержит секции: ${fmtTags(noReadme)}`)
99
+ console.warn(` → Исправить: jst update-readme`)
100
+ }
101
+
102
+ if (!warnOnly) process.exit(1)
103
+ process.exit(0)