cherrypick-interactive 1.0.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/.yarnrc.yml +1 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/cli.backup.js +186 -0
- package/cli.js +511 -0
- package/package.json +28 -0
package/.yarnrc.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sulhadin Γney
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# πͺΆ cherrypick-interactive
|
|
2
|
+
|
|
3
|
+
### Cherry-pick missing commits from `dev` to `main` β interactively and safely.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## π§ Motivation
|
|
8
|
+
|
|
9
|
+
When you maintain long-lived branches like `dev` and `main`, keeping them in sync can get messy.
|
|
10
|
+
Sometimes you rebase, sometimes you cherry-pick, sometimes you merge release branches β and every time, itβs easy to lose track of which commits actually made it into production.
|
|
11
|
+
|
|
12
|
+
**This CLI solves that pain point:**
|
|
13
|
+
|
|
14
|
+
- It compares two branches (e.g. `origin/dev` vs `origin/main`)
|
|
15
|
+
- Lists commits in `dev` that are *not yet* in `main`
|
|
16
|
+
- Lets you choose which ones to cherry-pick interactively
|
|
17
|
+
- (Optionally) bumps your semantic version, creates a release branch, updates `package.json`, and opens a GitHub draft PR for review
|
|
18
|
+
|
|
19
|
+
No manual `git log` diffing. No risky merges. No guesswork.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## π§ What it does
|
|
24
|
+
|
|
25
|
+
- π Finds commits in `dev` not present in `main`
|
|
26
|
+
- ποΈ Lets you select which commits to cherry-pick (or pick all)
|
|
27
|
+
- πͺ Cherry-picks in the correct order (oldest β newest)
|
|
28
|
+
- πͺ Detects **semantic version bump** (`major`, `minor`, `patch`) from conventional commits
|
|
29
|
+
- π§© Creates a `release/x.y.z` branch from `main`
|
|
30
|
+
- π§Ύ Generates a Markdown changelog from commits
|
|
31
|
+
- π§° Optionally:
|
|
32
|
+
- updates `package.json` version
|
|
33
|
+
- commits and pushes it
|
|
34
|
+
- opens a **GitHub PR** (draft or normal)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π¦ Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g cherrypick-interactive
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
(You can also run it directly without installing globally using `npx`.)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## π Quick Start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cherrypick-interactive --semantic-versioning --version-file ./package.json --create-release --push-release --draft-pr
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
β
This will:
|
|
55
|
+
1. Fetch `origin/dev` and `origin/main`
|
|
56
|
+
2. List commits in `dev` missing from `main`
|
|
57
|
+
3. Let you select which to cherry-pick
|
|
58
|
+
4. Compute the next version from commit messages
|
|
59
|
+
5. Create `release/<next-version>` from `main`
|
|
60
|
+
6. Cherry-pick the selected commits
|
|
61
|
+
7. Update your `package.json` version and commit it
|
|
62
|
+
8. Push the branch and open a **draft PR** on GitHub
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## π§© Common Use Cases
|
|
67
|
+
|
|
68
|
+
### 1. Compare branches manually
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cherrypick-interactive
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Lists commits in `origin/dev` that arenβt in `origin/main`, filtered by the last week.
|
|
75
|
+
|
|
76
|
+
### 2. Cherry-pick all missing commits automatically
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cherrypick-interactive --all-yes
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Preview changes without applying them
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cherrypick-interactive --dry-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## βοΈ Options
|
|
91
|
+
|
|
92
|
+
| Flag | Description | Default |
|
|
93
|
+
|------|--------------|----------|
|
|
94
|
+
| `--dev` | Source branch (commits to copy) | `origin/dev` |
|
|
95
|
+
| `--main` | Target branch (commits already merged here will be skipped) | `origin/main` |
|
|
96
|
+
| `--since` | Git time window filter (e.g. `"2 weeks ago"`) | `1 week ago` |
|
|
97
|
+
| `--no-fetch` | Skip `git fetch --prune` | `false` |
|
|
98
|
+
| `--all-yes` | Cherry-pick all missing commits without prompt | `false` |
|
|
99
|
+
| `--dry-run` | Show what would happen without applying changes | `false` |
|
|
100
|
+
| `--semantic-versioning` | Detect semantic version bump from commits | `false` |
|
|
101
|
+
| `--current-version` | Current version (if not reading from file) | β |
|
|
102
|
+
| `--version-file` | Path to `package.json` (to read & update version) | β |
|
|
103
|
+
| `--create-release` | Create `release/x.y.z` branch from `main` | `false` |
|
|
104
|
+
| `--push-release` | Push release branch to origin | `true` |
|
|
105
|
+
| `--draft-pr` | Create the GitHub PR as a draft | `false` |
|
|
106
|
+
| `--version-commit-message` | Template for version bump commit | `chore(release): bump version to {{version}}` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## π§ How Semantic Versioning Works
|
|
111
|
+
|
|
112
|
+
The tool analyzes commit messages using **Conventional Commits**:
|
|
113
|
+
|
|
114
|
+
| Prefix | Example | Bump |
|
|
115
|
+
|---------|----------|------|
|
|
116
|
+
| `BREAKING CHANGE:` | `feat(auth): BREAKING CHANGE: require MFA` | **major** |
|
|
117
|
+
| `feat:` | `feat(ui): add dark mode` | **minor** |
|
|
118
|
+
| `fix:` / `perf:` | `fix(api): correct pagination offset` | **patch** |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## πͺ΅ Example Run
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
$ cherrypick-interactive --semantic-versioning --version-file ./package.json --create-release --draft-pr
|
|
126
|
+
|
|
127
|
+
Fetching remotes (git fetch --prune)...
|
|
128
|
+
Comparing subjects since 1 week ago
|
|
129
|
+
Dev: origin/dev
|
|
130
|
+
Main: origin/main
|
|
131
|
+
|
|
132
|
+
Select commits to cherry-pick (3 missing):
|
|
133
|
+
β―β― (850aa02) fix: crypto withdrawal payload
|
|
134
|
+
β― (2995cea) feat: OTC offer account validation
|
|
135
|
+
β― (84fe310) chore: bump dependencies
|
|
136
|
+
|
|
137
|
+
Semantic Versioning
|
|
138
|
+
Current: 1.20.0 Detected bump: minor Next: 1.21.0
|
|
139
|
+
|
|
140
|
+
Creating release/1.21.0 from origin/main...
|
|
141
|
+
β Ready on release/1.21.0. Cherry-picking will apply here.
|
|
142
|
+
β package.json updated and committed: chore(release): bump version to 1.21.0
|
|
143
|
+
β
Pull request created for release/1.21.0 β main
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## π§Ή Why This Helps
|
|
149
|
+
|
|
150
|
+
If your team:
|
|
151
|
+
- Rebases or cherry-picks from `dev` β `main`
|
|
152
|
+
- Uses temporary release branches
|
|
153
|
+
- Tracks semantic versions via commits
|
|
154
|
+
|
|
155
|
+
β¦this CLI saves time and reduces errors.
|
|
156
|
+
It automates a tedious, error-prone manual process into a single command that behaves like `yarn upgrade-interactive`, but for Git commits.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## π§° Requirements
|
|
161
|
+
|
|
162
|
+
- Node.js β₯ 18
|
|
163
|
+
- GitHub CLI (`gh`) installed and authenticated
|
|
164
|
+
- A clean working directory (no uncommitted changes)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## π§Ύ License
|
|
169
|
+
|
|
170
|
+
**MIT** β free to use, modify, and distribute.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## π§βπ» Contributing
|
|
175
|
+
|
|
176
|
+
1. Clone the repo
|
|
177
|
+
2. Run locally:
|
|
178
|
+
```bash
|
|
179
|
+
node cli.js --dry-run
|
|
180
|
+
```
|
|
181
|
+
3. Test edge cases before submitting PRs
|
|
182
|
+
4. Please follow Conventional Commits for your changes.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
> Created to make release management simpler and safer for teams who value clean Git history and predictable deployments.
|
package/cli.backup.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import simpleGit from 'simple-git'
|
|
6
|
+
import yargs from 'yargs'
|
|
7
|
+
|
|
8
|
+
import { hideBin } from 'yargs/helpers'
|
|
9
|
+
|
|
10
|
+
const git = simpleGit()
|
|
11
|
+
|
|
12
|
+
const argv = yargs(hideBin(process.argv))
|
|
13
|
+
.scriptName('cherrypick-interactive')
|
|
14
|
+
.usage('$0 [options]')
|
|
15
|
+
.option('dev', {
|
|
16
|
+
type: 'string',
|
|
17
|
+
default: 'origin/dev',
|
|
18
|
+
describe: 'Source branch (contains commits you want).'
|
|
19
|
+
})
|
|
20
|
+
.option('main', {
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: 'origin/main',
|
|
23
|
+
describe: 'Comparison branch (commits present here will be filtered out).'
|
|
24
|
+
})
|
|
25
|
+
.option('since', {
|
|
26
|
+
type: 'string',
|
|
27
|
+
default: '1 week ago',
|
|
28
|
+
describe: 'Time window passed to git --since (e.g. "2 weeks ago", "1 month ago").'
|
|
29
|
+
})
|
|
30
|
+
.option('no-fetch', {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
describe: "Skip 'git fetch --prune'."
|
|
34
|
+
})
|
|
35
|
+
.option('all-yes', {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
default: false,
|
|
38
|
+
describe: 'Non-interactive: cherry-pick ALL missing commits (oldest β newest).'
|
|
39
|
+
})
|
|
40
|
+
.option('dry-run', {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
default: false,
|
|
43
|
+
describe: 'Print what would be cherry-picked and exit.'
|
|
44
|
+
})
|
|
45
|
+
.help()
|
|
46
|
+
.alias('h', 'help')
|
|
47
|
+
.alias('v', 'version').argv
|
|
48
|
+
|
|
49
|
+
const log = (...a) => console.log(...a)
|
|
50
|
+
const err = (...a) => console.error(...a)
|
|
51
|
+
|
|
52
|
+
async function gitRaw(args) {
|
|
53
|
+
const out = await git.raw(args)
|
|
54
|
+
return out.trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getSubjects(branch) {
|
|
58
|
+
const out = await gitRaw(['log', '--no-merges', '--pretty=%s', branch])
|
|
59
|
+
if (!out) {
|
|
60
|
+
return new Set()
|
|
61
|
+
}
|
|
62
|
+
return new Set(out.split('\n').filter(Boolean))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getDevCommits(branch, since) {
|
|
66
|
+
const out = await gitRaw(['log', '--no-merges', '--since=' + since, '--pretty=%H %s', branch])
|
|
67
|
+
|
|
68
|
+
if (!out) {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
return out.split('\n').map((line) => {
|
|
72
|
+
const firstSpace = line.indexOf(' ')
|
|
73
|
+
const hash = line.slice(0, firstSpace)
|
|
74
|
+
const subject = line.slice(firstSpace + 1)
|
|
75
|
+
return { hash, subject }
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function filterMissing(devCommits, mainSubjects) {
|
|
80
|
+
return devCommits.filter(({ subject }) => !mainSubjects.has(subject))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function selectCommitsInteractive(missing) {
|
|
84
|
+
const choices = [
|
|
85
|
+
new inquirer.Separator(chalk.gray('ββ Newest commits ββ')),
|
|
86
|
+
...missing.map(({ hash, subject }, idx) => {
|
|
87
|
+
// display-only trim to avoid accidental leading spaces
|
|
88
|
+
const displaySubject = subject.replace(/^[\s\u00A0]+/, '')
|
|
89
|
+
return {
|
|
90
|
+
name: `${chalk.dim(`(${hash.slice(0, 7)})`)} ${displaySubject}`,
|
|
91
|
+
value: hash,
|
|
92
|
+
short: displaySubject,
|
|
93
|
+
idx // we keep index for oldestβnewest ordering later
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
new inquirer.Separator(chalk.gray('ββ Oldest commits ββ'))
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
const { selected } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'checkbox',
|
|
102
|
+
name: 'selected',
|
|
103
|
+
message: `Select commits to cherry-pick (${missing.length} missing):`,
|
|
104
|
+
choices,
|
|
105
|
+
pageSize: Math.min(20, Math.max(8, missing.length))
|
|
106
|
+
}
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
return selected
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function cherryPickSequential(hashes) {
|
|
113
|
+
for (const hash of hashes) {
|
|
114
|
+
try {
|
|
115
|
+
await gitRaw(['cherry-pick', hash])
|
|
116
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', hash])
|
|
117
|
+
log(`${chalk.green('β')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
err(chalk.red(`β Cherry-pick failed on ${hash}`))
|
|
120
|
+
err(chalk.yellow('Resolve conflicts, then run:'))
|
|
121
|
+
err(chalk.yellow(' git add -A && git cherry-pick --continue'))
|
|
122
|
+
err(chalk.yellow('Or abort:'))
|
|
123
|
+
err(chalk.yellow(' git cherry-pick --abort'))
|
|
124
|
+
throw e
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main() {
|
|
130
|
+
try {
|
|
131
|
+
if (!argv['no-fetch']) {
|
|
132
|
+
log(chalk.gray('Fetching remotes (git fetch --prune)...'))
|
|
133
|
+
await git.fetch(['--prune'])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const currentBranch = (await gitRaw(['rev-parse', '--abbrev-ref', 'HEAD'])) || 'HEAD'
|
|
137
|
+
|
|
138
|
+
log(chalk.gray(`Comparing subjects since ${argv.since}`))
|
|
139
|
+
log(chalk.gray(`Dev: ${argv.dev}`))
|
|
140
|
+
log(chalk.gray(`Main: ${argv.main}`))
|
|
141
|
+
|
|
142
|
+
const [devCommits, mainSubjects] = await Promise.all([getDevCommits(argv.dev, argv.since), getSubjects(argv.main)])
|
|
143
|
+
|
|
144
|
+
const missing = filterMissing(devCommits, mainSubjects)
|
|
145
|
+
|
|
146
|
+
if (missing.length === 0) {
|
|
147
|
+
log(chalk.green('β
No missing commits found in the selected window.'))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Prepare bottomβtop ordering support
|
|
152
|
+
const indexByHash = new Map(missing.map((c, i) => [c.hash, i])) // 0=newest, larger=older
|
|
153
|
+
|
|
154
|
+
let selected
|
|
155
|
+
if (argv.yes) {
|
|
156
|
+
selected = missing.map((m) => m.hash)
|
|
157
|
+
} else {
|
|
158
|
+
selected = await selectCommitsInteractive(missing)
|
|
159
|
+
if (!selected.length) {
|
|
160
|
+
log(chalk.yellow('No commits selected. Exiting.'))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Bottom β Top (oldest β newest)
|
|
166
|
+
const bottomToTop = [...selected].sort((a, b) => indexByHash.get(b) - indexByHash.get(a))
|
|
167
|
+
|
|
168
|
+
if (argv.dry_run || argv['dry-run']) {
|
|
169
|
+
log(chalk.cyan('\n--dry-run: would cherry-pick (oldest β newest):'))
|
|
170
|
+
for (const h of bottomToTop) {
|
|
171
|
+
const subj = await gitRaw(['show', '--format=%s', '-s', h])
|
|
172
|
+
log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`)
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
log(chalk.cyan(`\nCherry-picking ${bottomToTop.length} commit(s) onto ${currentBranch} (oldest β newest)...\n`))
|
|
178
|
+
await cherryPickSequential(bottomToTop)
|
|
179
|
+
log(chalk.green(`\nβ
Done on ${currentBranch}`))
|
|
180
|
+
} catch (e) {
|
|
181
|
+
err(chalk.red(`\nβ Error: ${e.message || e}`))
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main()
|
package/cli.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { promises as fsPromises } from 'fs'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import simpleGit from 'simple-git'
|
|
7
|
+
import yargs from 'yargs'
|
|
8
|
+
|
|
9
|
+
import { hideBin } from 'yargs/helpers'
|
|
10
|
+
|
|
11
|
+
const git = simpleGit()
|
|
12
|
+
|
|
13
|
+
const argv = yargs(hideBin(process.argv))
|
|
14
|
+
.scriptName('cherrypick-interactive')
|
|
15
|
+
.usage('$0 [options]')
|
|
16
|
+
.option('dev', {
|
|
17
|
+
type: 'string',
|
|
18
|
+
default: 'origin/dev',
|
|
19
|
+
describe: 'Source branch (contains commits you want).'
|
|
20
|
+
})
|
|
21
|
+
.option('main', {
|
|
22
|
+
type: 'string',
|
|
23
|
+
default: 'origin/main',
|
|
24
|
+
describe: 'Comparison branch (commits present here will be filtered out).'
|
|
25
|
+
})
|
|
26
|
+
.option('since', {
|
|
27
|
+
type: 'string',
|
|
28
|
+
default: '1 week ago',
|
|
29
|
+
describe: 'Time window passed to git --since (e.g. "2 weeks ago", "1 month ago").'
|
|
30
|
+
})
|
|
31
|
+
.option('no-fetch', {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
34
|
+
describe: "Skip 'git fetch --prune'."
|
|
35
|
+
})
|
|
36
|
+
.option('all-yes', {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
default: false,
|
|
39
|
+
describe: 'Non-interactive: cherry-pick ALL missing commits (oldest β newest).'
|
|
40
|
+
})
|
|
41
|
+
.option('dry-run', {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: false,
|
|
44
|
+
describe: 'Print what would be cherry-picked and exit.'
|
|
45
|
+
})
|
|
46
|
+
.option('semantic-versioning', {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
default: false,
|
|
49
|
+
describe: 'Compute next semantic version from selected (or missing) commits.'
|
|
50
|
+
})
|
|
51
|
+
.option('current-version', {
|
|
52
|
+
type: 'string',
|
|
53
|
+
describe: 'Current version (X.Y.Z). Required when --semantic-versioning is set.'
|
|
54
|
+
})
|
|
55
|
+
.option('create-release', {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
default: false,
|
|
58
|
+
describe: 'Create a release branch from --main named release/<computed-version> before cherry-picking.'
|
|
59
|
+
})
|
|
60
|
+
.option('push-release', {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: true,
|
|
63
|
+
describe: 'After creating the release branch, push and set upstream (origin).'
|
|
64
|
+
})
|
|
65
|
+
.option('draft-pr', {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
default: false,
|
|
68
|
+
describe: 'Create the release PR as a draft.'
|
|
69
|
+
})
|
|
70
|
+
.option('version-file', {
|
|
71
|
+
type: 'string',
|
|
72
|
+
describe: 'Path to package.json (read current version; optional replacement for --current-version)'
|
|
73
|
+
})
|
|
74
|
+
.option('version-commit-message', {
|
|
75
|
+
type: 'string',
|
|
76
|
+
default: 'chore(release): bump version to {{version}}',
|
|
77
|
+
describe: 'Commit message template for version bump. Use {{version}} placeholder.'
|
|
78
|
+
})
|
|
79
|
+
.help()
|
|
80
|
+
.alias('h', 'help')
|
|
81
|
+
.alias('v', 'version').argv
|
|
82
|
+
|
|
83
|
+
const log = (...a) => console.log(...a)
|
|
84
|
+
const err = (...a) => console.error(...a)
|
|
85
|
+
|
|
86
|
+
async function gitRaw(args) {
|
|
87
|
+
const out = await git.raw(args)
|
|
88
|
+
return out.trim()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getSubjects(branch) {
|
|
92
|
+
const out = await gitRaw(['log', '--no-merges', '--pretty=%s', branch])
|
|
93
|
+
if (!out) {
|
|
94
|
+
return new Set()
|
|
95
|
+
}
|
|
96
|
+
return new Set(out.split('\n').filter(Boolean))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getDevCommits(branch, since) {
|
|
100
|
+
const out = await gitRaw(['log', '--no-merges', '--since=' + since, '--pretty=%H %s', branch])
|
|
101
|
+
|
|
102
|
+
if (!out) {
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
return out.split('\n').map((line) => {
|
|
106
|
+
const firstSpace = line.indexOf(' ')
|
|
107
|
+
const hash = line.slice(0, firstSpace)
|
|
108
|
+
const subject = line.slice(firstSpace + 1)
|
|
109
|
+
return { hash, subject }
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function filterMissing(devCommits, mainSubjects) {
|
|
114
|
+
return devCommits.filter(({ subject }) => !mainSubjects.has(subject))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function selectCommitsInteractive(missing) {
|
|
118
|
+
const choices = [
|
|
119
|
+
new inquirer.Separator(chalk.gray('ββ Newest commits ββ')),
|
|
120
|
+
...missing.map(({ hash, subject }, idx) => {
|
|
121
|
+
// display-only trim to avoid accidental leading spaces
|
|
122
|
+
const displaySubject = subject.replace(/^[\s\u00A0]+/, '')
|
|
123
|
+
return {
|
|
124
|
+
name: `${chalk.dim(`(${hash.slice(0, 7)})`)} ${displaySubject}`,
|
|
125
|
+
value: hash,
|
|
126
|
+
short: displaySubject,
|
|
127
|
+
idx // we keep index for oldestβnewest ordering later
|
|
128
|
+
}
|
|
129
|
+
}),
|
|
130
|
+
new inquirer.Separator(chalk.gray('ββ Oldest commits ββ'))
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
const { selected } = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'checkbox',
|
|
136
|
+
name: 'selected',
|
|
137
|
+
message: `Select commits to cherry-pick (${missing.length} missing):`,
|
|
138
|
+
choices,
|
|
139
|
+
pageSize: Math.min(20, Math.max(8, missing.length))
|
|
140
|
+
}
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
return selected
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function cherryPickSequential(hashes) {
|
|
147
|
+
for (const hash of hashes) {
|
|
148
|
+
try {
|
|
149
|
+
await gitRaw(['cherry-pick', hash])
|
|
150
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', hash])
|
|
151
|
+
log(`${chalk.green('β')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`)
|
|
152
|
+
} catch (e) {
|
|
153
|
+
err(chalk.red(`β Cherry-pick failed on ${hash}`))
|
|
154
|
+
err(chalk.yellow('Resolve conflicts, then run:'))
|
|
155
|
+
err(chalk.yellow(' git add -A && git cherry-pick --continue'))
|
|
156
|
+
err(chalk.yellow('Or abort:'))
|
|
157
|
+
err(chalk.yellow(' git cherry-pick --abort'))
|
|
158
|
+
throw e
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Semantic version bumping
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
function parseVersion(v) {
|
|
168
|
+
const m = String(v || '')
|
|
169
|
+
.trim()
|
|
170
|
+
.match(/^(\d+)\.(\d+)\.(\d+)$/)
|
|
171
|
+
if (!m) {
|
|
172
|
+
throw new Error(`Invalid --current-version "${v}". Expected X.Y.Z`)
|
|
173
|
+
}
|
|
174
|
+
return { major: +m[1], minor: +m[2], patch: +m[3] }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function incrementVersion(version, bump) {
|
|
178
|
+
const cur = parseVersion(version)
|
|
179
|
+
if (bump === 'major') {
|
|
180
|
+
return `${cur.major + 1}.0.0`
|
|
181
|
+
}
|
|
182
|
+
if (bump === 'minor') {
|
|
183
|
+
return `${cur.major}.${cur.minor + 1}.0`
|
|
184
|
+
}
|
|
185
|
+
if (bump === 'patch') {
|
|
186
|
+
return `${cur.major}.${cur.minor}.${cur.patch + 1}`
|
|
187
|
+
}
|
|
188
|
+
return `${cur.major}.${cur.minor}.${cur.patch}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeMessage(msg) {
|
|
192
|
+
// normalize whitespace; keep case-insensitive matching
|
|
193
|
+
return (msg || '').replace(/\r\n/g, '\n')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Returns "major" | "minor" | "patch" | null for a single commit message
|
|
197
|
+
function classifySingleCommit(messageBody) {
|
|
198
|
+
const body = normalizeMessage(messageBody)
|
|
199
|
+
|
|
200
|
+
// Major
|
|
201
|
+
if (/\bBREAKING[- _]CHANGE(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
202
|
+
return 'major'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Minor
|
|
206
|
+
if (/(^|\n)\s*(\*?\s*)?feat(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
207
|
+
return 'minor'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Patch
|
|
211
|
+
if (/(^|\n)\s*(\*?\s*)?(fix|perf)(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
212
|
+
return 'patch'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Given many commits, collapse to a single bump level
|
|
219
|
+
function collapseBumps(levels) {
|
|
220
|
+
if (levels.includes('major')) {
|
|
221
|
+
return 'major'
|
|
222
|
+
}
|
|
223
|
+
if (levels.includes('minor')) {
|
|
224
|
+
return 'minor'
|
|
225
|
+
}
|
|
226
|
+
if (levels.includes('patch')) {
|
|
227
|
+
return 'patch'
|
|
228
|
+
}
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Fetch full commit messages (%B) for SHAs and compute bump
|
|
233
|
+
async function computeSemanticBumpForCommits(hashes, gitRawFn) {
|
|
234
|
+
if (!hashes.length) {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const levels = []
|
|
239
|
+
for (const h of hashes) {
|
|
240
|
+
const msg = await gitRawFn(['show', '--format=%B', '-s', h])
|
|
241
|
+
const level = classifySingleCommit(msg)
|
|
242
|
+
if (level) {
|
|
243
|
+
levels.push(level)
|
|
244
|
+
}
|
|
245
|
+
if (level === 'major') {
|
|
246
|
+
break
|
|
247
|
+
} // early exit if major is found
|
|
248
|
+
}
|
|
249
|
+
return collapseBumps(levels)
|
|
250
|
+
}
|
|
251
|
+
async function main() {
|
|
252
|
+
try {
|
|
253
|
+
if (!argv['no-fetch']) {
|
|
254
|
+
log(chalk.gray('Fetching remotes (git fetch --prune)...'))
|
|
255
|
+
await git.fetch(['--prune'])
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const currentBranch = (await gitRaw(['rev-parse', '--abbrev-ref', 'HEAD'])) || 'HEAD'
|
|
259
|
+
|
|
260
|
+
log(chalk.gray(`Comparing subjects since ${argv.since}`))
|
|
261
|
+
log(chalk.gray(`Dev: ${argv.dev}`))
|
|
262
|
+
log(chalk.gray(`Main: ${argv.main}`))
|
|
263
|
+
|
|
264
|
+
const [devCommits, mainSubjects] = await Promise.all([getDevCommits(argv.dev, argv.since), getSubjects(argv.main)])
|
|
265
|
+
|
|
266
|
+
const missing = filterMissing(devCommits, mainSubjects)
|
|
267
|
+
|
|
268
|
+
if (missing.length === 0) {
|
|
269
|
+
log(chalk.green('β
No missing commits found in the selected window.'))
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const indexByHash = new Map(missing.map((c, i) => [c.hash, i])) // 0=newest, larger=older
|
|
274
|
+
|
|
275
|
+
let selected
|
|
276
|
+
if (argv['all-yes']) {
|
|
277
|
+
selected = missing.map((m) => m.hash)
|
|
278
|
+
} else {
|
|
279
|
+
selected = await selectCommitsInteractive(missing)
|
|
280
|
+
if (!selected.length) {
|
|
281
|
+
log(chalk.yellow('No commits selected. Exiting.'))
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const bottomToTop = [...selected].sort((a, b) => indexByHash.get(b) - indexByHash.get(a))
|
|
287
|
+
|
|
288
|
+
if (argv.dry_run || argv['dry-run']) {
|
|
289
|
+
log(chalk.cyan('\n--dry-run: would cherry-pick (oldest β newest):'))
|
|
290
|
+
for (const h of bottomToTop) {
|
|
291
|
+
const subj = await gitRaw(['show', '--format=%s', '-s', h])
|
|
292
|
+
log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`)
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (argv['version-file'] && !argv['current-version']) {
|
|
298
|
+
const currentVersionFromPkg = await getPkgVersion(argv['version-file'])
|
|
299
|
+
argv['current-version'] = currentVersionFromPkg
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let computedNextVersion = argv['current-version']
|
|
303
|
+
if (argv['semantic-versioning']) {
|
|
304
|
+
if (!argv['current-version']) {
|
|
305
|
+
throw new Error(' --semantic-versioning requires --current-version X.Y.Z (or pass --version-file)')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Bump is based on the commits you are about to apply (selected).
|
|
309
|
+
const bump = await computeSemanticBumpForCommits(bottomToTop, gitRaw)
|
|
310
|
+
|
|
311
|
+
computedNextVersion = bump ? incrementVersion(argv['current-version'], bump) : argv['current-version']
|
|
312
|
+
|
|
313
|
+
log('')
|
|
314
|
+
log(chalk.magenta('Semantic Versioning'))
|
|
315
|
+
log(
|
|
316
|
+
` Current: ${chalk.bold(argv['current-version'])} ` +
|
|
317
|
+
`Detected bump: ${chalk.bold(bump || 'none')} ` +
|
|
318
|
+
`Next: ${chalk.bold(computedNextVersion)}`
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (argv['create-release']) {
|
|
323
|
+
if (!argv['semantic-versioning'] || !argv['current-version']) {
|
|
324
|
+
throw new Error(' --create-release requires --semantic-versioning and --current-version X.Y.Z')
|
|
325
|
+
}
|
|
326
|
+
if (!computedNextVersion) {
|
|
327
|
+
throw new Error('Unable to determine release version. Check semantic-versioning inputs.')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const releaseBranch = `release/${computedNextVersion}`
|
|
331
|
+
await ensureBranchDoesNotExistLocally(releaseBranch)
|
|
332
|
+
const startPoint = argv.main // e.g., 'origin/main' or a local ref
|
|
333
|
+
|
|
334
|
+
const changelogBody = await buildChangelogBody({
|
|
335
|
+
version: computedNextVersion,
|
|
336
|
+
hashes: bottomToTop,
|
|
337
|
+
gitRawFn: gitRaw
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
await fsPromises.writeFile('RELEASE_CHANGELOG.md', changelogBody, 'utf8')
|
|
341
|
+
log(chalk.gray(`β
Generated changelog for ${releaseBranch} β RELEASE_CHANGELOG.md`))
|
|
342
|
+
|
|
343
|
+
log(chalk.cyan(`\nCreating ${chalk.bold(releaseBranch)} from ${chalk.bold(startPoint)}...`))
|
|
344
|
+
|
|
345
|
+
await git.checkoutBranch(releaseBranch, startPoint)
|
|
346
|
+
|
|
347
|
+
log(chalk.green(`β Ready on ${chalk.bold(releaseBranch)}. Cherry-picking will apply here.`))
|
|
348
|
+
} else {
|
|
349
|
+
// otherwise we stay on the current branch
|
|
350
|
+
log(chalk.bold(`Base branch: ${currentBranch}`))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
log(chalk.cyan(`\nCherry-picking ${bottomToTop.length} commit(s) onto ${currentBranch} (oldest β newest)...\n`))
|
|
354
|
+
|
|
355
|
+
await cherryPickSequential(bottomToTop)
|
|
356
|
+
|
|
357
|
+
if (argv['push-release']) {
|
|
358
|
+
const baseBranchForGh = stripOrigin(argv.main) // 'origin/main' -> 'main'
|
|
359
|
+
const prTitle = `Release ${computedNextVersion}`
|
|
360
|
+
const releaseBranch = `release/${computedNextVersion}`
|
|
361
|
+
|
|
362
|
+
const onBranch = await gitRaw(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
363
|
+
if (!onBranch.startsWith(releaseBranch)) {
|
|
364
|
+
throw new Error(`Version update should happen on a release branch. Current: ${onBranch}`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
log(chalk.cyan(`\nUpdating ${argv['version-file']} version β ${computedNextVersion} ...`))
|
|
368
|
+
await setPkgVersion(argv['version-file'], computedNextVersion)
|
|
369
|
+
await git.add([argv['version-file']])
|
|
370
|
+
const msg = argv['version-commit-message'].replace('{{version}}', computedNextVersion)
|
|
371
|
+
await git.commit(msg)
|
|
372
|
+
|
|
373
|
+
log(chalk.green(`β package.json updated and committed: ${msg}`))
|
|
374
|
+
|
|
375
|
+
await git.push(['-u', 'origin', releaseBranch, '--no-verify'])
|
|
376
|
+
|
|
377
|
+
const ghArgs = [
|
|
378
|
+
'pr',
|
|
379
|
+
'create',
|
|
380
|
+
'--base',
|
|
381
|
+
baseBranchForGh,
|
|
382
|
+
'--head',
|
|
383
|
+
releaseBranch,
|
|
384
|
+
'--title',
|
|
385
|
+
prTitle,
|
|
386
|
+
'--body-file',
|
|
387
|
+
'RELEASE_CHANGELOG.md'
|
|
388
|
+
]
|
|
389
|
+
if (argv['draft-pr']) {
|
|
390
|
+
ghArgs.push('--draft')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await runGh(ghArgs)
|
|
394
|
+
log(chalk.gray(`Pushed ${onBranch} with version bump.`))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const finalBranch = argv['create-release']
|
|
398
|
+
? await gitRaw(['rev-parse', '--abbrev-ref', 'HEAD']) // should be release/*
|
|
399
|
+
: currentBranch
|
|
400
|
+
|
|
401
|
+
log(chalk.green(`\nβ
Done on ${finalBranch}`))
|
|
402
|
+
} catch (e) {
|
|
403
|
+
err(chalk.red(`\nβ Error: ${e.message || e}`))
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
main()
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Utils
|
|
412
|
+
*/
|
|
413
|
+
|
|
414
|
+
async function ensureBranchDoesNotExistLocally(branchName) {
|
|
415
|
+
const branches = await git.branchLocal()
|
|
416
|
+
if (branches.all.includes(branchName)) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Release branch "${branchName}" already exists locally. ` + `Please delete it or choose a different version.`
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function buildChangelogBody({ version, hashes, gitRawFn }) {
|
|
424
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
425
|
+
const header = version ? `## Release ${version} β ${today}` : `## Release β ${today}`
|
|
426
|
+
|
|
427
|
+
const breakings = []
|
|
428
|
+
const features = []
|
|
429
|
+
const fixes = []
|
|
430
|
+
const others = []
|
|
431
|
+
|
|
432
|
+
for (const h of hashes) {
|
|
433
|
+
const msg = await gitRawFn(['show', '--format=%B', '-s', h])
|
|
434
|
+
const level = classifySingleCommit(msg)
|
|
435
|
+
|
|
436
|
+
const subject = msg.split(/\r?\n/)[0].trim() // first line of commit message
|
|
437
|
+
const shaDisplay = shortSha(h)
|
|
438
|
+
|
|
439
|
+
switch (level) {
|
|
440
|
+
case 'major':
|
|
441
|
+
breakings.push(`${shaDisplay} ${subject}`)
|
|
442
|
+
break
|
|
443
|
+
case 'minor':
|
|
444
|
+
features.push(`${shaDisplay} ${subject}`)
|
|
445
|
+
break
|
|
446
|
+
case 'patch':
|
|
447
|
+
fixes.push(`${shaDisplay} ${subject}`)
|
|
448
|
+
break
|
|
449
|
+
default:
|
|
450
|
+
others.push(`${shaDisplay} ${subject}`)
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const sections = []
|
|
456
|
+
if (breakings.length) {
|
|
457
|
+
sections.push(`### β¨ Breaking Changes\n${breakings.join('\n')}`)
|
|
458
|
+
}
|
|
459
|
+
if (features.length) {
|
|
460
|
+
sections.push(`### β¨ Features\n${features.join('\n')}`)
|
|
461
|
+
}
|
|
462
|
+
if (fixes.length) {
|
|
463
|
+
sections.push(`### π Fixes\n${fixes.join('\n')}`)
|
|
464
|
+
}
|
|
465
|
+
if (others.length) {
|
|
466
|
+
sections.push(`### π§Ή Others\n${others.join('\n')}`)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return `${header}\n\n${sections.join('\n\n')}\n`
|
|
470
|
+
}
|
|
471
|
+
function shortSha(sha) {
|
|
472
|
+
return String(sha).slice(0, 7)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function stripOrigin(ref) {
|
|
476
|
+
return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function runGh(args) {
|
|
480
|
+
return new Promise((resolve, reject) => {
|
|
481
|
+
const p = spawn('gh', args, { stdio: 'inherit' })
|
|
482
|
+
p.on('error', reject)
|
|
483
|
+
p.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`gh exited ${code}`))))
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
async function readJson(filePath) {
|
|
487
|
+
const raw = await fsPromises.readFile(filePath, 'utf8')
|
|
488
|
+
return JSON.parse(raw)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function writeJson(filePath, data) {
|
|
492
|
+
const text = JSON.stringify(data, null, 2) + '\n'
|
|
493
|
+
await fsPromises.writeFile(filePath, text, 'utf8')
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Read package.json version; throw if missing */
|
|
497
|
+
async function getPkgVersion(pkgPath) {
|
|
498
|
+
const pkg = await readJson(pkgPath)
|
|
499
|
+
const v = pkg && pkg.version
|
|
500
|
+
if (!v) {
|
|
501
|
+
throw new Error(`No "version" field found in ${pkgPath}`)
|
|
502
|
+
}
|
|
503
|
+
return v
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Update package.json version in-place */
|
|
507
|
+
async function setPkgVersion(pkgPath, nextVersion) {
|
|
508
|
+
const pkg = await readJson(pkgPath)
|
|
509
|
+
pkg.version = nextVersion
|
|
510
|
+
await writeJson(pkgPath, pkg)
|
|
511
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cherrypick-interactive",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactively cherry-pick commits that are in dev but not in main, using subject-based comparison.",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cherrypick-interactive": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chalk": "^5.6.2",
|
|
15
|
+
"inquirer": "^12.9.6",
|
|
16
|
+
"simple-git": "^3.28.0",
|
|
17
|
+
"yargs": "^18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"git",
|
|
21
|
+
"cli",
|
|
22
|
+
"cherry-pick",
|
|
23
|
+
"interactive",
|
|
24
|
+
"devops"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"packageManager": "yarn@4.6.0"
|
|
28
|
+
}
|