zames_pro 2.65.0 → 2.66.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.
@@ -0,0 +1,157 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import {
6
+ parseConventionalCommit,
7
+ groupCommits,
8
+ renderChangelogDraft,
9
+ } from '../src/changelog.js'
10
+
11
+ // One-command release prep (BACKLOG G4). The manual sequence — bump
12
+ // package.json, write a dated CHANGELOG section, commit `chore: release X.Y.Z`,
13
+ // tag — is easy to get wrong, so this script does the deterministic parts and
14
+ // STOPS before the push. Pushing the branch and the tag is deliberately a
15
+ // separate, explicit step: the tag triggers the npm publish workflow, and that
16
+ // must never happen by accident.
17
+ //
18
+ // Usage:
19
+ // npm run release patch # bug fixes
20
+ // npm run release minor # new features
21
+ // npm run release major # breaking changes
22
+ // npm run release patch --no-tests # skip the local test run (CI still runs)
23
+ // npm run release patch --dry-run # print the plan, change nothing
24
+ //
25
+ // Run via tsx so it can reuse src/changelog.ts (the conventional-commit
26
+ // parser) — a plain .mjs cannot import the TypeScript sources.
27
+
28
+ const NL = String.fromCharCode(10)
29
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
30
+
31
+ function git(args: string[], allowFail = false): string {
32
+ const r = spawnSync('git', args, { cwd: root, encoding: 'utf-8' })
33
+ if (r.status !== 0 && !allowFail) {
34
+ console.error('git ' + args.join(' ') + ' failed:' + NL + (r.stderr || ''))
35
+ process.exit(1)
36
+ }
37
+ return (r.stdout || '').toString()
38
+ }
39
+
40
+ function fail(msg: string): never {
41
+ console.error('release: ' + msg)
42
+ process.exit(1)
43
+ }
44
+
45
+ function run(cmd: string, args: string[]): void {
46
+ const r = spawnSync(cmd, args, { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' })
47
+ if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
48
+ }
49
+
50
+ const argv = process.argv.slice(2)
51
+ const flags = new Set(argv.filter((a) => a.startsWith('--')))
52
+ const bump = argv.find((a) => !a.startsWith('--'))
53
+ const dryRun = flags.has('--dry-run')
54
+ const skipTests = flags.has('--no-tests')
55
+
56
+ if (bump !== 'patch' && bump !== 'minor' && bump !== 'major') {
57
+ fail('usage: npm run release patch|minor|major [--no-tests] [--dry-run]')
58
+ }
59
+
60
+ // 1. Clean tree: a release commit must contain ONLY the version bump and the
61
+ // changelog section, never half-finished work.
62
+ const dirty = git(['status', '--porcelain']).trim()
63
+ if (dirty) {
64
+ fail('working tree is not clean:' + NL + dirty)
65
+ }
66
+
67
+ // 2. Gates. The git hooks run these on commit/push too, but a release should
68
+ // fail EARLY, before package.json/CHANGELOG are touched.
69
+ if (!skipTests) {
70
+ run('npm', ['run', 'typecheck'])
71
+ run('npm', ['test'])
72
+ }
73
+
74
+ // 3. Compute the next version from package.json.
75
+ const pkgPath = path.join(root, 'package.json')
76
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {
77
+ version: string
78
+ }
79
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(pkg.version)
80
+ if (!m) fail('package.json version is not a plain x.y.z: ' + pkg.version)
81
+ let [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])]
82
+ if (bump === 'major') {
83
+ major += 1
84
+ minor = 0
85
+ patch = 0
86
+ } else if (bump === 'minor') {
87
+ minor += 1
88
+ patch = 0
89
+ } else {
90
+ patch += 1
91
+ }
92
+ const next = major + '.' + minor + '.' + patch
93
+ const tag = 'v' + next
94
+
95
+ if (git(['tag', '--list', tag]).trim()) {
96
+ fail('tag ' + tag + ' already exists')
97
+ }
98
+
99
+ // 4. Build the dated CHANGELOG section from conventional commits since the
100
+ // last tag. English commit subjects become the section body; the operator can
101
+ // still reword the section afterwards.
102
+ const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
103
+ const ref = latestTag || git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() || 'HEAD'
104
+ const subjects = git(['log', '--no-merges', '--pretty=format:%s', ref + '..HEAD'])
105
+ .split(NL)
106
+ .map((s) => s.trim())
107
+ .filter(Boolean)
108
+ const parsed = subjects
109
+ .map(parseConventionalCommit)
110
+ .filter((c): c is NonNullable<typeof c> => c !== null)
111
+ const body = renderChangelogDraft(groupCommits(parsed))
112
+
113
+ // CHANGELOG groups are `### Added` (Keep a Changelog), the draft renders
114
+ // `## Added` — normalize here.
115
+ const sectionBody = body
116
+ .split(NL)
117
+ .map((line) => (line.startsWith('## ') ? '#' + line : line))
118
+ .join(NL)
119
+ const date = new Date().toISOString().slice(0, 10)
120
+ const section =
121
+ '## [' + next + '] - ' + date + NL + NL + (sectionBody || '### Changed' + NL + NL + '- Internal improvements.') + NL
122
+
123
+ if (dryRun) {
124
+ console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
125
+ console.log('tag: ' + tag)
126
+ console.log('since: ' + ref)
127
+ console.log('CHANGELOG section:' + NL + section)
128
+ process.exit(0)
129
+ }
130
+
131
+ // 5. Insert the section right after the `## [Unreleased]` heading.
132
+ const changelogPath = path.join(root, 'CHANGELOG.md')
133
+ const changelog = fs.readFileSync(changelogPath, 'utf-8')
134
+ const anchor = '## [Unreleased]'
135
+ const idx = changelog.indexOf(anchor)
136
+ if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
137
+ const afterAnchor = idx + anchor.length
138
+ const updated =
139
+ changelog.slice(0, afterAnchor) +
140
+ NL + NL + section +
141
+ changelog.slice(afterAnchor).replace(/^\n+/, NL)
142
+ fs.writeFileSync(changelogPath, updated)
143
+
144
+ // 6. Bump package.json, preserving its exact formatting (single-line replace
145
+ // of the version value only).
146
+ const pkgText = fs.readFileSync(pkgPath, 'utf-8')
147
+ fs.writeFileSync(pkgPath, pkgText.replace('"version": "' + pkg.version + '"', '"version": "' + next + '"'))
148
+
149
+ // 7. Commit + tag. The push is intentionally NOT done here.
150
+ git(['add', 'package.json', 'CHANGELOG.md'])
151
+ git(['commit', '-m', 'chore: release ' + next])
152
+ git(['tag', tag])
153
+
154
+ console.log(NL + 'release: committed and tagged ' + tag)
155
+ console.log('Next (explicit) step — publish via CI:')
156
+ console.log(' git push')
157
+ console.log(' git push origin ' + tag)