@wyxos/zephyr 0.1.1 → 0.1.2

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 (2) hide show
  1. package/package.json +4 -3
  2. package/publish.mjs +222 -0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "test": "vitest"
6
+ "test": "vitest",
7
+ "release": "node publish.mjs"
7
8
  },
8
9
  "bin": {
9
- "zephyr": "./bin/zephyr.mjs"
10
+ "zephyr": "bin/zephyr.mjs"
10
11
  },
11
12
  "dependencies": {
12
13
  "chalk": "5.3.0",
package/publish.mjs ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { dirname, join } from 'node:path'
5
+ import { readFile } from 'node:fs/promises'
6
+
7
+ const ROOT = dirname(fileURLToPath(import.meta.url))
8
+ const PACKAGE_PATH = join(ROOT, 'package.json')
9
+
10
+ const STEP_PREFIX = '→'
11
+ const OK_PREFIX = '✔'
12
+ const WARN_PREFIX = '⚠'
13
+
14
+ function logStep(message) {
15
+ console.log(`${STEP_PREFIX} ${message}`)
16
+ }
17
+
18
+ function logSuccess(message) {
19
+ console.log(`${OK_PREFIX} ${message}`)
20
+ }
21
+
22
+ function logWarning(message) {
23
+ console.warn(`${WARN_PREFIX} ${message}`)
24
+ }
25
+
26
+ function runCommand(command, args, { cwd = ROOT, capture = false } = {}) {
27
+ return new Promise((resolve, reject) => {
28
+ const spawnOptions = {
29
+ cwd,
30
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
31
+ }
32
+
33
+ const child = spawn(command, args, spawnOptions)
34
+ let stdout = ''
35
+ let stderr = ''
36
+
37
+ if (capture) {
38
+ child.stdout.on('data', (chunk) => {
39
+ stdout += chunk
40
+ })
41
+
42
+ child.stderr.on('data', (chunk) => {
43
+ stderr += chunk
44
+ })
45
+ }
46
+
47
+ child.on('error', reject)
48
+ child.on('close', (code) => {
49
+ if (code === 0) {
50
+ resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
51
+ } else {
52
+ const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
53
+ if (capture) {
54
+ error.stdout = stdout
55
+ error.stderr = stderr
56
+ }
57
+ error.exitCode = code
58
+ reject(error)
59
+ }
60
+ })
61
+ })
62
+ }
63
+
64
+ async function readPackage() {
65
+ const raw = await readFile(PACKAGE_PATH, 'utf8')
66
+ return JSON.parse(raw)
67
+ }
68
+
69
+ async function ensureCleanWorkingTree() {
70
+ const { stdout } = await runCommand('git', ['status', '--porcelain'], { capture: true })
71
+
72
+ if (stdout.length > 0) {
73
+ throw new Error('Working tree has uncommitted changes. Commit or stash them before releasing.')
74
+ }
75
+ }
76
+
77
+ async function getCurrentBranch() {
78
+ const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true })
79
+ return stdout || null
80
+ }
81
+
82
+ async function getUpstreamRef() {
83
+ try {
84
+ const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
85
+ capture: true
86
+ })
87
+
88
+ return stdout || null
89
+ } catch {
90
+ return null
91
+ }
92
+ }
93
+
94
+ async function ensureUpToDateWithUpstream(branch, upstreamRef) {
95
+ if (!upstreamRef) {
96
+ logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
97
+ return
98
+ }
99
+
100
+ const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
101
+ capture: true
102
+ })
103
+ const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
104
+ capture: true
105
+ })
106
+
107
+ const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
108
+ const behind = Number.parseInt(behindResult.stdout || '0', 10)
109
+
110
+ if (Number.isFinite(behind) && behind > 0) {
111
+ throw new Error(
112
+ `Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
113
+ )
114
+ }
115
+
116
+ if (Number.isFinite(ahead) && ahead > 0) {
117
+ logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
118
+ }
119
+ }
120
+
121
+ function parseArgs() {
122
+ const args = process.argv.slice(2)
123
+ const positionals = args.filter((arg) => !arg.startsWith('--'))
124
+ const flags = new Set(args.filter((arg) => arg.startsWith('--')))
125
+
126
+ const releaseType = positionals[0] ?? 'patch'
127
+ const skipTests = flags.has('--skip-tests')
128
+
129
+ const allowedTypes = new Set([
130
+ 'major',
131
+ 'minor',
132
+ 'patch',
133
+ 'premajor',
134
+ 'preminor',
135
+ 'prepatch',
136
+ 'prerelease'
137
+ ])
138
+
139
+ if (!allowedTypes.has(releaseType)) {
140
+ throw new Error(
141
+ `Invalid release type "${releaseType}". Use one of: ${Array.from(allowedTypes).join(', ')}.`
142
+ )
143
+ }
144
+
145
+ return { releaseType, skipTests }
146
+ }
147
+
148
+ async function runTests(skipTests) {
149
+ if (skipTests) {
150
+ logWarning('Skipping tests because --skip-tests flag was provided.')
151
+ return
152
+ }
153
+
154
+ logStep('Running test suite (vitest run)...')
155
+ await runCommand('npx', ['vitest', 'run'])
156
+ logSuccess('Tests passed.')
157
+ }
158
+
159
+ async function ensureNpmAuth() {
160
+ logStep('Confirming npm authentication...')
161
+ await runCommand('npm', ['whoami'])
162
+ }
163
+
164
+ async function bumpVersion(releaseType) {
165
+ logStep(`Bumping package version with "npm version ${releaseType}"...`)
166
+ await runCommand('npm', ['version', releaseType, '--message', 'chore: release %s'])
167
+ const pkg = await readPackage()
168
+ logSuccess(`Version updated to ${pkg.version}.`)
169
+ return pkg
170
+ }
171
+
172
+ async function pushChanges() {
173
+ logStep('Pushing commits and tags to origin...')
174
+ await runCommand('git', ['push', '--follow-tags'])
175
+ logSuccess('Git push completed.')
176
+ }
177
+
178
+ async function publishPackage(pkg) {
179
+ const publishArgs = ['publish']
180
+
181
+ if (pkg.name.startsWith('@')) {
182
+ publishArgs.push('--access', 'public')
183
+ }
184
+
185
+ logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
186
+ await runCommand('npm', publishArgs)
187
+ logSuccess('npm publish completed.')
188
+ }
189
+
190
+ async function main() {
191
+ const { releaseType, skipTests } = parseArgs()
192
+
193
+ logStep('Reading package metadata...')
194
+ const pkg = await readPackage()
195
+
196
+ logStep('Checking working tree status...')
197
+ await ensureCleanWorkingTree()
198
+
199
+ const branch = await getCurrentBranch()
200
+ if (!branch) {
201
+ throw new Error('Unable to determine current branch.')
202
+ }
203
+
204
+ logStep(`Current branch: ${branch}`)
205
+ const upstreamRef = await getUpstreamRef()
206
+ await ensureUpToDateWithUpstream(branch, upstreamRef)
207
+
208
+ await runTests(skipTests)
209
+ await ensureNpmAuth()
210
+
211
+ const updatedPkg = await bumpVersion(releaseType)
212
+ await pushChanges()
213
+ await publishPackage(updatedPkg)
214
+
215
+ logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
216
+ }
217
+
218
+ main().catch((error) => {
219
+ console.error('\nRelease failed:')
220
+ console.error(error.message)
221
+ process.exit(1)
222
+ })