@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 +10 -0
- package/bin/cli.js +31 -0
- package/package.json +1 -1
- package/templates/jst.config.js +17 -0
- package/tools-gh/config.js +4 -0
- package/tools-gh/fix-releases.js +77 -0
- package/tools-gh/release-audit.js +103 -0
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
package/templates/jst.config.js
CHANGED
|
@@ -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
|
/**
|
package/tools-gh/config.js
CHANGED
|
@@ -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)
|