@jens_astrup/release-manager 0.1.0-alpha.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.
@@ -0,0 +1,244 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import TextInput from 'ink-text-input';
5
+ import { ErrorPanel } from '../components/ErrorPanel.js';
6
+ import { StepList } from '../components/StepList.js';
7
+ import { toFriendlyError } from '../errors.js';
8
+ import { loadProjectEnv } from '../lib/env.js';
9
+ import { getCurrentVersion, isValidExplicitVersion, computeNextVersion } from '../lib/version.js';
10
+ import { prepareVersionFlow, suggestBump } from '../lib/workflows.js';
11
+ export function BumpVersion({ config, initialSkipBuild, onDone }) {
12
+ const { exit } = useApp();
13
+ const [phase, setPhase] = useState('choosing-type');
14
+ const [bumpType, setBumpType] = useState('patch');
15
+ const [explicit, setExplicit] = useState('');
16
+ const [skipBuild, setSkipBuild] = useState(initialSkipBuild || !config.buildCheckEnabled);
17
+ const [steps, setSteps] = useState([]);
18
+ const [currentVersion, setCurrentVersion] = useState('');
19
+ const [previewVersion, setPreviewVersion] = useState('');
20
+ const [suggestion, setSuggestion] = useState(null);
21
+ const [suggestLoading, setSuggestLoading] = useState(false);
22
+ const [error, setError] = useState(null);
23
+ const [resultUrl, setResultUrl] = useState(null);
24
+ // Load .env.local from the project before any API calls happen.
25
+ useEffect(() => {
26
+ loadProjectEnv(config.projectDir);
27
+ getCurrentVersion(config.projectDir)
28
+ .then(setCurrentVersion)
29
+ .catch((e) => {
30
+ setError(toFriendlyError(e));
31
+ setPhase('error');
32
+ });
33
+ }, [config.projectDir]);
34
+ // Recompute preview when bump type or explicit version change
35
+ useEffect(() => {
36
+ if (!currentVersion)
37
+ return;
38
+ try {
39
+ if (bumpType === 'explicit') {
40
+ if (explicit && isValidExplicitVersion(explicit)) {
41
+ setPreviewVersion(computeNextVersion(currentVersion, 'explicit', explicit));
42
+ }
43
+ else {
44
+ setPreviewVersion('');
45
+ }
46
+ }
47
+ else {
48
+ setPreviewVersion(computeNextVersion(currentVersion, bumpType));
49
+ }
50
+ }
51
+ catch {
52
+ setPreviewVersion('');
53
+ }
54
+ }, [bumpType, explicit, currentVersion]);
55
+ const initialSteps = useMemo(() => [
56
+ { id: 'clean', label: 'Check working directory is clean', status: 'pending' },
57
+ { id: 'fetch', label: `Refresh ${config.github.developBranch} branch`, status: 'pending' },
58
+ {
59
+ id: 'build',
60
+ label: `Build check (${config.buildCommand})`,
61
+ status: 'pending',
62
+ progress: 0
63
+ },
64
+ { id: 'bump', label: 'Compute and write new version', status: 'pending' },
65
+ { id: 'cleanup', label: 'Remove stale version branches', status: 'pending' },
66
+ { id: 'push', label: 'Commit and push version branch', status: 'pending' },
67
+ { id: 'pr', label: 'Open pull request', status: 'pending' }
68
+ ], [config.buildCommand, config.github.developBranch]);
69
+ async function runSuggestion() {
70
+ setSuggestLoading(true);
71
+ try {
72
+ const { bumpType: suggested } = await suggestBump(config);
73
+ setSuggestion(suggested);
74
+ setBumpType(suggested);
75
+ }
76
+ catch (e) {
77
+ setError(toFriendlyError(e));
78
+ setPhase('error');
79
+ }
80
+ finally {
81
+ setSuggestLoading(false);
82
+ }
83
+ }
84
+ async function execute() {
85
+ setPhase('running');
86
+ setSteps(initialSteps);
87
+ try {
88
+ const { pullRequestUrl } = await prepareVersionFlow({
89
+ config,
90
+ bumpType,
91
+ explicitVersion: bumpType === 'explicit' ? explicit : undefined,
92
+ skipBuild,
93
+ onUpdate: (id, patch) => {
94
+ setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, ...patch } : s)));
95
+ }
96
+ });
97
+ setResultUrl(pullRequestUrl);
98
+ setPhase('success');
99
+ }
100
+ catch (e) {
101
+ setError(toFriendlyError(e));
102
+ // Mark the running step as failed for visual feedback
103
+ setSteps((prev) => {
104
+ const idx = prev.findIndex((s) => s.status === 'running');
105
+ if (idx === -1)
106
+ return prev;
107
+ const copy = prev.slice();
108
+ copy[idx] = { ...copy[idx], status: 'failed' };
109
+ return copy;
110
+ });
111
+ setPhase('error');
112
+ }
113
+ }
114
+ if (phase === 'choosing-type') {
115
+ return (React.createElement(Box, { flexDirection: "column" },
116
+ React.createElement(Text, null,
117
+ "Current version: ",
118
+ React.createElement(Text, { color: "cyan" }, currentVersion || '...')),
119
+ React.createElement(Text, { color: "gray" }, "Choose a bump type:"),
120
+ React.createElement(SelectInput, { items: [
121
+ { label: 'Patch (bug fixes)', value: 'patch' },
122
+ { label: 'Minor (new features)', value: 'minor' },
123
+ { label: 'Major (breaking changes)', value: 'major' },
124
+ { label: 'Alpha prerelease', value: 'alpha' },
125
+ { label: 'Beta prerelease', value: 'beta' },
126
+ { label: 'Specific version…', value: 'explicit' },
127
+ { label: 'Suggest with AI…', value: 'suggest' }
128
+ ], onSelect: async (item) => {
129
+ if (item.value === 'suggest') {
130
+ await runSuggestion();
131
+ setPhase('choosing-build');
132
+ return;
133
+ }
134
+ setBumpType(item.value);
135
+ if (item.value === 'explicit') {
136
+ setPhase('entering-version');
137
+ }
138
+ else {
139
+ setPhase('choosing-build');
140
+ }
141
+ } }),
142
+ suggestLoading ? React.createElement(Text, { color: "gray" }, "Asking AI for a suggestion\u2026") : null,
143
+ suggestion ? (React.createElement(Text, { color: "green" },
144
+ "AI suggests: ",
145
+ suggestion)) : null));
146
+ }
147
+ if (phase === 'entering-version') {
148
+ const valid = explicit.length > 0 && isValidExplicitVersion(explicit);
149
+ return (React.createElement(Box, { flexDirection: "column" },
150
+ React.createElement(Text, null,
151
+ "Current version: ",
152
+ React.createElement(Text, { color: "cyan" }, currentVersion)),
153
+ React.createElement(Text, null, "Enter the new version (e.g. 2.0.0 or 2.0.0-rc.1):"),
154
+ React.createElement(Box, null,
155
+ React.createElement(Text, null, "\u203A "),
156
+ React.createElement(TextInput, { value: explicit, onChange: setExplicit, onSubmit: () => {
157
+ if (valid)
158
+ setPhase('choosing-build');
159
+ } })),
160
+ explicit.length > 0 && !valid ? (React.createElement(Text, { color: "red" }, "Not a valid semver version")) : null));
161
+ }
162
+ if (phase === 'choosing-build') {
163
+ return (React.createElement(Box, { flexDirection: "column" },
164
+ React.createElement(Text, null,
165
+ "Will bump ",
166
+ React.createElement(Text, { color: "cyan" }, currentVersion),
167
+ " \u2192",
168
+ ' ',
169
+ React.createElement(Text, { color: "green" }, previewVersion || '?')),
170
+ React.createElement(Text, { color: "gray" }, "Run build check before opening the PR?"),
171
+ React.createElement(SelectInput, { items: [
172
+ {
173
+ label: `Yes — run "${config.buildCommand}"`,
174
+ value: 'yes'
175
+ },
176
+ { label: 'No — skip build check', value: 'no' }
177
+ ], onSelect: (item) => {
178
+ setSkipBuild(item.value === 'no');
179
+ setPhase('confirming');
180
+ } })));
181
+ }
182
+ if (phase === 'confirming') {
183
+ return (React.createElement(Box, { flexDirection: "column" },
184
+ React.createElement(Text, { bold: true }, "Review:"),
185
+ React.createElement(Text, null,
186
+ " Project: ",
187
+ React.createElement(Text, { color: "cyan" }, config.name)),
188
+ React.createElement(Text, null,
189
+ ' ',
190
+ "Version: ",
191
+ React.createElement(Text, { color: "cyan" }, currentVersion),
192
+ " \u2192",
193
+ ' ',
194
+ React.createElement(Text, { color: "green" }, previewVersion)),
195
+ React.createElement(Text, null,
196
+ ' ',
197
+ "Build check: ",
198
+ skipBuild ? React.createElement(Text, { color: "yellow" }, "skipped") : React.createElement(Text, { color: "green" }, "enabled")),
199
+ React.createElement(Text, null,
200
+ ' ',
201
+ "PR: ",
202
+ React.createElement(Text, { color: "gray" }, `v${previewVersion} → ${config.github.developBranch}`)),
203
+ React.createElement(Box, { marginTop: 1 },
204
+ React.createElement(SelectInput, { items: [
205
+ { label: 'Proceed', value: 'go' },
206
+ { label: 'Cancel', value: 'cancel' }
207
+ ], onSelect: (item) => {
208
+ if (item.value === 'cancel') {
209
+ onDone();
210
+ }
211
+ else {
212
+ void execute();
213
+ }
214
+ } }))));
215
+ }
216
+ if (phase === 'running') {
217
+ return (React.createElement(Box, { flexDirection: "column" },
218
+ React.createElement(Text, { bold: true }, "Creating version bump PR\u2026"),
219
+ React.createElement(Box, { marginTop: 1 },
220
+ React.createElement(StepList, { steps: steps }))));
221
+ }
222
+ if (phase === 'success') {
223
+ return (React.createElement(Box, { flexDirection: "column" },
224
+ React.createElement(StepList, { steps: steps }),
225
+ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column" },
226
+ React.createElement(Text, { color: "green", bold: true }, "\u2713 Version bump PR created"),
227
+ React.createElement(Text, null, resultUrl)),
228
+ React.createElement(Box, { marginTop: 1 },
229
+ React.createElement(SelectInput, { items: [
230
+ { label: 'Back to menu', value: 'back' },
231
+ { label: 'Quit', value: 'quit' }
232
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
233
+ }
234
+ // error
235
+ return (React.createElement(Box, { flexDirection: "column" },
236
+ steps.length > 0 ? React.createElement(StepList, { steps: steps }) : null,
237
+ React.createElement(Box, { marginTop: 1 },
238
+ React.createElement(ErrorPanel, { error: error })),
239
+ React.createElement(Box, { marginTop: 1 },
240
+ React.createElement(SelectInput, { items: [
241
+ { label: 'Back to menu', value: 'back' },
242
+ { label: 'Quit', value: 'quit' }
243
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
244
+ }
@@ -0,0 +1,109 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { ErrorPanel } from '../components/ErrorPanel.js';
5
+ import { StepList } from '../components/StepList.js';
6
+ import { toFriendlyError } from '../errors.js';
7
+ import { loadProjectEnv } from '../lib/env.js';
8
+ import { createReleaseFlow } from '../lib/workflows.js';
9
+ export function CreateRelease({ config, onDone }) {
10
+ const { exit } = useApp();
11
+ const [phase, setPhase] = useState('confirming');
12
+ const [steps, setSteps] = useState([]);
13
+ const [error, setError] = useState(null);
14
+ const [resultUrl, setResultUrl] = useState(null);
15
+ useEffect(() => {
16
+ loadProjectEnv(config.projectDir);
17
+ }, [config.projectDir]);
18
+ const initialSteps = useMemo(() => [
19
+ { id: 'fetch', label: `Refresh ${config.github.developBranch} branch`, status: 'pending' },
20
+ {
21
+ id: 'issues',
22
+ label: `Fetch issues from ${config.issueSource === 'linear' ? 'Linear' : 'GitHub Issues'}`,
23
+ status: 'pending'
24
+ },
25
+ {
26
+ id: 'notes',
27
+ label: 'Generate release notes with AI',
28
+ status: 'pending',
29
+ progress: 0
30
+ },
31
+ {
32
+ id: 'pr',
33
+ label: `Open release PR (${config.github.developBranch} → ${config.github.mainBranch})`,
34
+ status: 'pending'
35
+ }
36
+ ], [config.github.developBranch, config.github.mainBranch, config.issueSource]);
37
+ async function execute() {
38
+ setPhase('running');
39
+ setSteps(initialSteps);
40
+ try {
41
+ const { pullRequestUrl } = await createReleaseFlow({
42
+ config,
43
+ onUpdate: (id, patch) => setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, ...patch } : s)))
44
+ });
45
+ setResultUrl(pullRequestUrl);
46
+ setPhase('success');
47
+ }
48
+ catch (e) {
49
+ setError(toFriendlyError(e));
50
+ setSteps((prev) => {
51
+ const idx = prev.findIndex((s) => s.status === 'running');
52
+ if (idx === -1)
53
+ return prev;
54
+ const copy = prev.slice();
55
+ copy[idx] = { ...copy[idx], status: 'failed' };
56
+ return copy;
57
+ });
58
+ setPhase('error');
59
+ }
60
+ }
61
+ if (phase === 'confirming') {
62
+ return (React.createElement(Box, { flexDirection: "column" },
63
+ React.createElement(Text, { bold: true }, "Create release"),
64
+ React.createElement(Text, null,
65
+ " Issue source: ",
66
+ React.createElement(Text, { color: "cyan" }, config.issueSource)),
67
+ React.createElement(Text, null,
68
+ " Repo: ",
69
+ React.createElement(Text, { color: "cyan" },
70
+ config.github.owner,
71
+ "/",
72
+ config.github.repo)),
73
+ React.createElement(Text, null,
74
+ " Notes model: ",
75
+ React.createElement(Text, { color: "cyan" }, config.openaiModel)),
76
+ React.createElement(Box, { marginTop: 1 },
77
+ React.createElement(SelectInput, { items: [
78
+ { label: 'Proceed', value: 'go' },
79
+ { label: 'Cancel', value: 'cancel' }
80
+ ], onSelect: (item) => (item.value === 'cancel' ? onDone() : void execute()) }))));
81
+ }
82
+ if (phase === 'running') {
83
+ return (React.createElement(Box, { flexDirection: "column" },
84
+ React.createElement(Text, { bold: true }, "Creating release\u2026"),
85
+ React.createElement(Box, { marginTop: 1 },
86
+ React.createElement(StepList, { steps: steps }))));
87
+ }
88
+ if (phase === 'success') {
89
+ return (React.createElement(Box, { flexDirection: "column" },
90
+ React.createElement(StepList, { steps: steps }),
91
+ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column" },
92
+ React.createElement(Text, { color: "green", bold: true }, "\u2713 Release PR created"),
93
+ React.createElement(Text, null, resultUrl)),
94
+ React.createElement(Box, { marginTop: 1 },
95
+ React.createElement(SelectInput, { items: [
96
+ { label: 'Back to menu', value: 'back' },
97
+ { label: 'Quit', value: 'quit' }
98
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
99
+ }
100
+ return (React.createElement(Box, { flexDirection: "column" },
101
+ steps.length > 0 ? React.createElement(StepList, { steps: steps }) : null,
102
+ React.createElement(Box, { marginTop: 1 },
103
+ React.createElement(ErrorPanel, { error: error })),
104
+ React.createElement(Box, { marginTop: 1 },
105
+ React.createElement(SelectInput, { items: [
106
+ { label: 'Back to menu', value: 'back' },
107
+ { label: 'Quit', value: 'quit' }
108
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
109
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ const items = [
5
+ { label: 'Create a version bump PR', value: 'bump' },
6
+ { label: 'Create a release (develop → main, AI notes)', value: 'release' },
7
+ { label: 'Get a suggested version bump', value: 'suggest' },
8
+ { label: 'Edit project settings', value: 'config' },
9
+ { label: 'Quit', value: 'quit' }
10
+ ];
11
+ export function MainMenu({ onSelect }) {
12
+ const { exit } = useApp();
13
+ return (React.createElement(Box, { flexDirection: "column" },
14
+ React.createElement(Text, { color: "gray" }, "Choose an action:"),
15
+ React.createElement(SelectInput, { items: items, onSelect: (item) => {
16
+ if (item.value === 'quit') {
17
+ exit();
18
+ return;
19
+ }
20
+ onSelect(item.value);
21
+ } })));
22
+ }
@@ -0,0 +1,142 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import TextInput from 'ink-text-input';
5
+ import { saveConfig } from '../lib/config.js';
6
+ export function Settings({ config, onSave, onDone }) {
7
+ const { exit } = useApp();
8
+ const [draft, setDraft] = useState(config);
9
+ const [editing, setEditing] = useState(null);
10
+ const [textValue, setTextValue] = useState('');
11
+ const buildItems = () => [
12
+ { label: `Project name: ${draft.name ?? '(unset)'}`, value: 'name' },
13
+ { label: `Build command: ${draft.buildCommand}`, value: 'buildCommand' },
14
+ {
15
+ label: `Build check by default: ${draft.buildCheckEnabled ? 'on' : 'off'}`,
16
+ value: 'buildCheckEnabled'
17
+ },
18
+ { label: `Issue source: ${draft.issueSource}`, value: 'issueSource' },
19
+ { label: `GitHub owner: ${draft.github.owner || '(unset)'}`, value: 'githubOwner' },
20
+ { label: `GitHub repo: ${draft.github.repo || '(unset)'}`, value: 'githubRepo' },
21
+ { label: `Develop branch: ${draft.github.developBranch}`, value: 'developBranch' },
22
+ { label: `Main branch: ${draft.github.mainBranch}`, value: 'mainBranch' },
23
+ ...(draft.issueSource === 'github'
24
+ ? [
25
+ { label: `GitHub label filter: ${draft.github.issueLabel ?? '(none)'}`, value: 'githubLabel' },
26
+ { label: `GitHub milestone: ${draft.github.milestone ?? '(none)'}`, value: 'githubMilestone' }
27
+ ]
28
+ : [
29
+ {
30
+ label: `Linear ready-for-deploy state ID: ${draft.linear?.readyForDeployStateId || '(unset)'}`,
31
+ value: 'linearStateId'
32
+ },
33
+ {
34
+ label: `Linear team key: ${draft.linear?.teamKey ?? '(unset)'}`,
35
+ value: 'linearTeamKey'
36
+ }
37
+ ]),
38
+ { label: `OpenAI model: ${draft.openaiModel}`, value: 'openaiModel' },
39
+ { label: '— Save & exit —', value: 'save' },
40
+ { label: 'Cancel', value: 'cancel' }
41
+ ];
42
+ function startEdit(field) {
43
+ setEditing(field);
44
+ setTextValue(getFieldText(draft, field));
45
+ }
46
+ function commitEdit(field, value) {
47
+ setDraft((d) => applyField(d, field, value));
48
+ setEditing(null);
49
+ }
50
+ function toggleField(field) {
51
+ setDraft((d) => {
52
+ if (field === 'buildCheckEnabled') {
53
+ return { ...d, buildCheckEnabled: !d.buildCheckEnabled };
54
+ }
55
+ if (field === 'issueSource') {
56
+ const next = d.issueSource === 'linear' ? 'github' : 'linear';
57
+ return { ...d, issueSource: next };
58
+ }
59
+ return d;
60
+ });
61
+ }
62
+ if (editing) {
63
+ return (React.createElement(Box, { flexDirection: "column" },
64
+ React.createElement(Text, null,
65
+ "Edit ",
66
+ React.createElement(Text, { color: "cyan" }, editing),
67
+ ":"),
68
+ React.createElement(Box, null,
69
+ React.createElement(Text, null, "\u203A "),
70
+ React.createElement(TextInput, { value: textValue, onChange: setTextValue, onSubmit: () => commitEdit(editing, textValue) })),
71
+ React.createElement(Text, { color: "gray" }, "Press Enter to save, Esc to cancel.")));
72
+ }
73
+ return (React.createElement(Box, { flexDirection: "column" },
74
+ React.createElement(Text, { bold: true }, "Project settings"),
75
+ React.createElement(Text, { color: "gray" }, "Stored at ~/.config/release-manager/projects/<hash>.json"),
76
+ React.createElement(Text, { color: "gray" },
77
+ "Project: ",
78
+ draft.projectDir),
79
+ React.createElement(Box, { marginTop: 1 },
80
+ React.createElement(SelectInput, { items: buildItems(), onSelect: async (item) => {
81
+ if (item.value === 'cancel') {
82
+ onDone();
83
+ return;
84
+ }
85
+ if (item.value === 'save') {
86
+ await saveConfig(draft);
87
+ onSave(draft);
88
+ return;
89
+ }
90
+ const field = item.value;
91
+ if (field === 'buildCheckEnabled' || field === 'issueSource') {
92
+ toggleField(field);
93
+ }
94
+ else {
95
+ startEdit(field);
96
+ }
97
+ }, limit: 14 })),
98
+ React.createElement(Box, { marginTop: 1 },
99
+ React.createElement(Text, { color: "gray" }, "\u2191/\u2193 to navigate, Enter to edit. Toggle items flip on Enter."),
100
+ React.createElement(Text, { color: "gray" }, " Press Ctrl+C to abort."),
101
+ React.createElement(Text, null, " ")),
102
+ React.createElement(Box, null,
103
+ React.createElement(Text, null, " ")),
104
+ React.createElement(Box, null,
105
+ React.createElement(Text, null, " ")),
106
+ false ? React.createElement(Text, null, exit.name) : null));
107
+ }
108
+ function getFieldText(c, field) {
109
+ switch (field) {
110
+ case 'name': return c.name ?? '';
111
+ case 'buildCommand': return c.buildCommand;
112
+ case 'githubOwner': return c.github.owner;
113
+ case 'githubRepo': return c.github.repo;
114
+ case 'developBranch': return c.github.developBranch;
115
+ case 'mainBranch': return c.github.mainBranch;
116
+ case 'githubLabel': return c.github.issueLabel ?? '';
117
+ case 'githubMilestone': return c.github.milestone ?? '';
118
+ case 'linearStateId': return c.linear?.readyForDeployStateId ?? '';
119
+ case 'linearTeamKey': return c.linear?.teamKey ?? '';
120
+ case 'openaiModel': return c.openaiModel;
121
+ default: return '';
122
+ }
123
+ }
124
+ function applyField(c, field, value) {
125
+ const trimmed = value.trim();
126
+ switch (field) {
127
+ case 'name': return { ...c, name: trimmed || c.name };
128
+ case 'buildCommand': return { ...c, buildCommand: trimmed || c.buildCommand };
129
+ case 'githubOwner': return { ...c, github: { ...c.github, owner: trimmed } };
130
+ case 'githubRepo': return { ...c, github: { ...c.github, repo: trimmed } };
131
+ case 'developBranch': return { ...c, github: { ...c.github, developBranch: trimmed || c.github.developBranch } };
132
+ case 'mainBranch': return { ...c, github: { ...c.github, mainBranch: trimmed || c.github.mainBranch } };
133
+ case 'githubLabel': return { ...c, github: { ...c.github, issueLabel: trimmed || undefined } };
134
+ case 'githubMilestone': return { ...c, github: { ...c.github, milestone: trimmed || undefined } };
135
+ case 'linearStateId':
136
+ return { ...c, linear: { ...(c.linear ?? {}), readyForDeployStateId: trimmed } };
137
+ case 'linearTeamKey':
138
+ return { ...c, linear: { ...(c.linear ?? {}), teamKey: trimmed || undefined } };
139
+ case 'openaiModel': return { ...c, openaiModel: trimmed || c.openaiModel };
140
+ default: return c;
141
+ }
142
+ }
@@ -0,0 +1,57 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import SelectInput from 'ink-select-input';
5
+ import { ErrorPanel } from '../components/ErrorPanel.js';
6
+ import { toFriendlyError } from '../errors.js';
7
+ import { loadProjectEnv } from '../lib/env.js';
8
+ import { suggestBump } from '../lib/workflows.js';
9
+ export function SuggestBumpScreen({ config, onDone }) {
10
+ const { exit } = useApp();
11
+ const [loading, setLoading] = useState(true);
12
+ const [result, setResult] = useState(null);
13
+ const [error, setError] = useState(null);
14
+ useEffect(() => {
15
+ loadProjectEnv(config.projectDir);
16
+ suggestBump(config)
17
+ .then(({ bumpType, commitCount }) => setResult({ bump: bumpType, commits: commitCount }))
18
+ .catch((e) => setError(toFriendlyError(e)))
19
+ .finally(() => setLoading(false));
20
+ }, [config]);
21
+ if (loading) {
22
+ return (React.createElement(Box, null,
23
+ React.createElement(Text, { color: "cyan" },
24
+ React.createElement(Spinner, { type: "dots" })),
25
+ React.createElement(Text, null,
26
+ " Analyzing commits between ",
27
+ config.github.mainBranch,
28
+ " and ",
29
+ config.github.developBranch,
30
+ "\u2026")));
31
+ }
32
+ if (error) {
33
+ return (React.createElement(Box, { flexDirection: "column" },
34
+ React.createElement(ErrorPanel, { error: error }),
35
+ React.createElement(Box, { marginTop: 1 },
36
+ React.createElement(SelectInput, { items: [
37
+ { label: 'Back to menu', value: 'back' },
38
+ { label: 'Quit', value: 'quit' }
39
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
40
+ }
41
+ return (React.createElement(Box, { flexDirection: "column" },
42
+ React.createElement(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column" },
43
+ React.createElement(Text, { color: "green", bold: true },
44
+ "Suggested bump: ",
45
+ result?.bump),
46
+ React.createElement(Text, { color: "gray" },
47
+ "Based on ",
48
+ result?.commits,
49
+ " commit",
50
+ result?.commits === 1 ? '' : 's',
51
+ " between branches.")),
52
+ React.createElement(Box, { marginTop: 1 },
53
+ React.createElement(SelectInput, { items: [
54
+ { label: 'Back to menu', value: 'back' },
55
+ { label: 'Quit', value: 'quit' }
56
+ ], onSelect: (item) => (item.value === 'quit' ? exit() : onDone()) }))));
57
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@jens_astrup/release-manager",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "Interactive CLI for creating release version bump PRs and releases with AI-generated notes. Built with Ink.",
5
+ "type": "module",
6
+ "author": "jens_astrup",
7
+
8
+ "bin": {
9
+ "release-manager": "dist/bin/release-manager.js"
10
+ },
11
+ "main": "dist/bin/release-manager.js",
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "start": "node dist/bin/release-manager.js",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "release",
27
+ "version",
28
+ "ink",
29
+ "cli",
30
+ "github",
31
+ "linear",
32
+ "semver",
33
+ "ai"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@linear/sdk": "^39.0.0",
38
+ "execa": "^9.5.1",
39
+ "ink": "^5.1.0",
40
+ "ink-select-input": "^6.0.0",
41
+ "ink-spinner": "^5.0.0",
42
+ "ink-text-input": "^6.0.0",
43
+ "meow": "^13.2.0",
44
+ "octokit": "^4.0.2",
45
+ "openai": "^4.73.0",
46
+ "react": "^18.3.1",
47
+ "semver": "^7.6.3",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^25.6.2",
52
+ "@types/react": "^18.3.12",
53
+ "@types/semver": "^7.5.8",
54
+ "typescript": "^5.6.3"
55
+ }
56
+ }